From 4f8a8fe3e74f29215cf9668e8c91275a5b99d959 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 4 Aug 2024 20:56:15 +0200 Subject: [PATCH 001/540] Update tools submodule --- tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools b/tools index 1039c9e..0b25471 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit 1039c9e9d8617702061c0071ee54303823319f72 +Subproject commit 0b254717e338cb3a93458b6737280df72d4473f2 From b75c18a24e5c56116784562416a6438d1660a10a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 4 Aug 2024 20:59:17 +0200 Subject: [PATCH 002/540] Update package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 13f078a..a1caf89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3029,7 +3029,7 @@ }, "tools": { "name": "@project-gauntlet/tools", - "version": "0.5.0", + "version": "0.6.0", "dependencies": { "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", From 638b79f44585e005bed03906d600011a5ec9c1c9 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 4 Aug 2024 21:07:52 +0200 Subject: [PATCH 003/540] Update settings ui screenshot --- docs/settings_ui.png | Bin 57813 -> 532057 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/settings_ui.png b/docs/settings_ui.png index 998f843cbd6141babcd38d2554f4df2bdcf461d0..c7882d915278c32096d335729dc8deb600863b17 100644 GIT binary patch literal 532057 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 z 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%0A}X+=D^C6LgM+&N7DE)r^j}nZt2n@-E+F~ynU7L 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
EB-@@(EMlhi$ID{=5EsQB86`F!JZ)9t+ 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 G{eJ<>@=?41 literal 57813 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

ybHSA zK)j}yzcLLGK8oI@ApzdkZ{Bo$U6lOs^9%?ZVK25II*E8V*wBkvm=wOk{a-Ua-(`zL zoskNMw+L6?%2)JIcU>G9hY3H5dJ3mY4}mfZKKlB`I4*id$Z_7{;4U|@)SY{?a_%40 z;xE{=B4vTF8Y&K>%kt8b^(9!z2$@zek>N5IaIhK~wSN4VqX{VVTm*ucA2om_nkB5* z+O%HRqd5&KTQaLh=A``*)G`QU0)Io;T^ueuXG)`RGDTxX0GNV@Q$<1%V*{@XczAXT zdMATQibB=F7FPgqyO+}FJGK37c4(9*4z}My3U;mV)*yrVvCv8zC2+LHIq@@Y9V_Oi z(@xv-8BkHruBCa-0CXd$ikvlC(Pl^pXI+yMP&QsaFglp6*lt++@LKClSFtjcFoxdo zdp!arG*c!L!)+)ECRV>DrM{w1gXb4sOVQSovT~~V#?$OP&c_hgbD;jqKp((GCd+5# z@i^-cG1m{Q=5a&ZZ(R}|9tVY!%2XPh7Xx<)dZhrGYrH(P2S{STWsM6}v4>leCa~x> zEz<;2B{tH*QBZ0j$4@IdF7?U~8m+n?B~kg~FMl?uS;dP5iCEY8l~Gmfh4cwHR(h!B zn)Qdyj4>_L`|n(sl{px135;T0m|Z~squc}88^$?dF`-iBD`UL4Jzh?CLjTHbPWSp| zWdZK`{Ev7&zW=MP;{PGs{xBi@jqwd}P0l~^4Xbe0Zf1hL2K@Yc_4kw`Q6_j%#+r0? z5Lf45I5D7NSU}0xdh>@bz(3)C@XtkW9_#1#O#QxV4U9H^x*26m;vlQTFvSFLm>n3( z5s2@AF(@-XixFgnk#bJ`dcAUqPds6atVbN(A)0|C1L9hAudnau{g9p5U*J<9A^)X# z{W=g^9~v3Gp+m1p69KDAiwGUtLUSxbdSKsdl=>TxTBX|$L6~Gw&F-`<5V60P8=-wt za(wwVbpv950qN1ADxTIm!1e=;Dqvz=;kpEhfF|$)mxu?}Z39HTn(J}$+Kbff#-LHj z$}T&;{Numhfx(VOj+$H!*g!06h^iu2=)^vDEVjf18dU7(i8w61sSf$8At409O(ECU z5|&H97cBV~G7GGgwYA>hK7H?5ujyi|UM4*sMiu_=FH&arA80z zE14xGnR(8CLETVCH4DNd_?Eqj5$(12{nAvnM>g^ds`I=3;W4$fG2FT_ny0@au2E7Z zEK-Y3ON4d=$>3d{s~a|p%`QOCUhxFj!657!9T_^ySYOGBfAT=;$t0lH9u3Im@Hp9T zyK^5{C$^;aOZb73Xgwt!1a>2kD8@Jb)j1+c_Yx+j(q8VBI6sMXiV@+9#JKO%e!A)o z43N766Xt5QdSqDGKhE~&c|y4E3+MWzkW|^hotMOD>)^CR({A99i~t96DWJH-uhxDo z;s7@mnJhJVlkXw@XK$R#rTQS8Rif4*WaH~~dR>f0J7bd9lG+b@0SNpeB2;$!2l%GY zN#Sio#s*T4!3|qEF#~k{p*xeZK>LvFj>sQ2r*HfydXgtHR7`KE`J%~j zqZZYnF@0f>=fe<26?ylUw%*Se-9Xyd-hO>%py}`d;$?=P60#J$UpuokLRz?uBp(Cg zj@nsEYtA;Shs@n937jIULC4@^!GIlskr+T8?jhBpYb#8rO;q7%Q%61X=tKc_)=0a-(JjBF06bQJ*Jpxk+6)lc5fDdVjB=W7w zC{X-#ZCG>8Ev_Ai-A;R__`%=?=eZeumTV~2|EU;xA_~Y6?|jeWtBl?R-@hY1f*o=q z&*?uYaYUW|Rf!`qG64IxcGDTr#PcSi$?;;sq3{_DA=VDO_S3$MVr)ohQIFjw-}k$H!_R38FAT#wV{`)GKJ;MgEs=WSeF`m~3HX ztHr!eO!4J&OcJ32pxnQMJH39k}7f13l(macf zismB-1Gg3;7#Y(+BqNOem@Y(S?E)MWr|-w(NG!&MIkhTif-6cGFBI-h-wDsO`( z7+x$V0^|k=wc95VR9=lw-^rtz`~h-xdxZ0Ip1K<_KS!)<+%gw1oLd_90oJ$DiDSJ5 zsWj}u`L3RbC{-n`H*MA3iEouvc^p3+{C@jiPFsxe4k9H5v3-t#@C|flA~`;PUX}TQ z3=R^>uWYvUTNsn&_Y+VocR!dqv8WYNbX?fsRGb$K)@N=m8hvPsM^1G$hQqbs?hn~- z(;Ft)Mlkq|YhE|64&5$j6S60?UztX^^!Xmdy1>ON66VMD2G#N$dl$)y^pVfN#VjKN zkr&iIds^*Cp(;zCxpYFs)E~vwJuj+nRoiWIu&^>S zh~k5|M=Wnb*>nUV6xv0-Ce?Ax9>@q7GoZM^$)*IJu5E{ZOP$QJ6 zGo)c;TC59lg9C4Dyq+WjE*~Z3HyeC`q~B6Y!(uXTV1du(B6k;5oxOnBK@(dR_P&Z{ zGU)~f`86+@S=E`lK6{adTmh-bgPo7T#B2Uu=B3=vyA-r9C7lIbj01!%1|#jA&{cZw z9t8LllVKKwF9TC@Yr}vx?CydlX|wE0-$sikcIB_^<-oH@F+i>$zP#r-HTU4rqi=FY ziM{1H%s{qiDBJr|I(0Y%LU>KMVgx#w&ntaTEvsWQk%86=osi7NiRsY((K z-06avuOz{oc@nPxwjuAf1$q(W2cM%Fa7G?y0kb35Bkm+%=w|rYG**InM3@mC$lc`U z7yN$r+qHN3M*EyKRLPFYMN` z{L+WN;i=W-w9z@SJL04KS+xm(1DUhg_i=(C*2g$jY=kw4yXCH*#M{srDzMT8{XkOWeb?vyv<1-q1Vv6|I`8mc9FMa&Sl}# zMm%*fBngTN<{WTXnk0Cor1U~w{z20q&ccT(!AMo#wuM%3d=@fK_H6qB^5Or9IsSCTvnea5__>A`OiMBH zQeolq`78*z$SCEzBox7?#SbnK*6ei+QQCG9O?ms0_@Tz>TM#fZ`iF!NeQ8OJ8Itf#C zCaVgn5L~c@02L*${{9#(-=LI9$lsP5t0^gnAAS>Hy^z(moso|St(d-ufnRhKxHzrZ z^^!!r($F5ZqRQyHPD`ZX*7v|wd4=paJ7?nzwn-X5;xjmIx_=t43VV9CySNbyJu65; z*Sn+)()@zmM-Hij`;;PD=rSiZ*dRGIKR@*A?9;HjdP16N&n5KV_-wH&sMy){eT@~r znls$G7k&!jqHfcXeHk}k5JBRXIUJF@yOYO@Cu}Yl`-TMQ9UuBx=EsbV@PX?-7_>r^ z>s+u-H6$^tfqerh7Kk*=&7Xq|OdrrG*OSdH~Q(t7N&6U=Q#I z>GCqnY;2$THi|u?=u5}jpxmwolwgKU=w`jo&EXB)iApNkEm;Q>Wz;=FJty7$-SK>1 zfYNcm#FRn*kZWG=N5NbHP@i1v_mOaO=<_-Y>s*jlJ8t6$hU>JKb}Y!e3^Ya}n)0^# zNTRqrItAsW7*AJ(lguA)p*E^Tx8oB7$DKV!=eHCESj$O2ym>2-VY(pAWj*rNaR<^p zfkC;LpXPfpk2&m0`fRDVS9y`>Y_Aqo-jvJP#fs#!+<9NOyD9P6`}1ze%y~;r?f0TA zhg20eBSp%o>ZL^dcVZXt1F4K_rO9eFwz!5Q%7GL@5ur`q?|CUrt*lxHh9pg2C}#jt z-aB_nEm!5qpfs2My7MGBw>d%jLhL9{T4Beqm{pl{#cqRbdGB>L68C)Mvn&M za~SPiROs(=ztU9gi>4|sdl-cUvR7NSZMaO`alp%sb2c{lg`X8Pk)&woYvr4*?#PL9k!FE#jK-IjNh;^Qz!;=5N0%>=W|J2-UU5JW~a?P=~l9cr4-NO_o z9$r3*lJU>OM#gR1Uhb9d9`VO81P<(ce$>}LaCX%Cg46hBEoY`kLXRMgqJLL4qEgpV zOJZa9tjl8wamtneA@+oyZjWLK+CEOkxGcc0#BdqiK+bsF!n5TD7c|hs_e4`U722-M zIij%1xgUI#-mVF%95Vsk2(U0E2u8N;Olx^hY?bPCCUpO&yfvgWJ)8_=CruYg}fna<2UF{00P3xKE^j5Ol zIc;lt$o>2GXU})n#oWssaUZ5|7GTbGgxq=v zLFPR`woql8QPrK%2?$5-z&B*kC)3Q6RW|$p1gxC0>P=!b>hNKGlw$_3$<{OuUh$JA z+iOFL>!Q#BC?WXKM@=^<^uQI z7N&=pPNO|_Zw&++eVHKZegHZQ5Pq|EeiePv=s$w6CVlx76)GVOwOKW3Q$33+m>e@j zX}N_In(u@X>gZIskXcMGDLZafB1z7w&!ogP%F`7zYnnQK2dO$S8DB(AX~$>iyi!u` znt|vzwH%8aQyaA<1w*TU74I)fca4^_K*I9qNEilC+2$KA3yDM%9(kp;{xgWFJV3Q9|YiB2bdpE~lcct;2BW<@OvaCLTOj1IhIAEtHQ3t)vNiE{C z*L&(qkLP}19`fqd+n?6x-FlFavR&;!3Kgxsp=!^g?iO4&VXUlxt(Xx271xwI?kE8~ zWU$+IOrPp@j=4g0vh%LWa!=CBkz+ByABNL+?6Dzs9J^P@9BOC3Sc^WldwsP_HGid{ zKU|&y;t!hfi8N&0`|S^eG(x@R)(D@j;qJi@_W3!~&g|-5?MUiQO`oWY(b)}gOq4*-lD9I$Bl35ViJ;zAD-ylKW_xzWvv zo}DkY*uPV8!u#_z`_Z4{xhHK>!OP1$E}Jr>GjI_dk6r$0S9ps{rMONVP|eCLuSV5{>N85jayjzi60H>4HCcf-bVjD| z0m7ck`0T+q!|F6V&1;cC3I`+-}%9-rb$D zkA0%c#)>Zb$X*lTScOSOqwLvEkK``@0pEuAcZo2~WX%2&9Fadpyr9pLllfEch!)`n z%KJ%WOwKxJ_sgcI0)z0GfT5%$n#n@bk*3k!_A^I{PsaQLb{j{04NSPe{2-pQ9;K@G zdtX$v)DXJRu^^=f7(b!8DL9Dw*+|-#wpX0nFn@pcxgRSv4gbft!U+2pO!(sUY7>jy zx}aO8hV-Al@cJ?Tq+kU_Sb?8I6X)s?0BLEFtT+`Jr&i3)HWx8<=ntjdx#LQft~v>N zxIbRCv(FP-jaDVZly}{yJg%RhoT{;YUB9)`hjm|+Z87@{)2E6VJKG5+rLR_b7#_MF zS1v0bKI~sm@X`0zeY16TOYP*Cq}CrI%Xi4Ag2pX=+Q7n*AfE-q9#pvJD=19c<7@R; z71TI>T-gyG8FDQ+(bOC*6B#TgJbqtaxZ(~pS6Di3=T{KNC)m?Pl^V-Gw#Dpjg2rf9 z*Q-O!J{auEBbL*CJx0ds%|)q1P^0d4UeE)J9It$!>|1i{P)(S*$<#12sePpAyhD2D z_WN|i_pR)9N1qfqKLc5piTa`ts0azJ;6F~WG0;28`i3y^3eHxO>K9E+%$T3Q26q&z zinW2H#!Y);1^N|9T2ycnVp-pj43Ks8@QG^=A8~zMHWF~yVw0i({DnZ0hP;-R7@hl} zP?hn`tZd80NIH*_GR3LW)Uvw8)q%R9ycOd1-B&v#u3lbO^S21JTJ2!a#Tm|1DmTL9 z%|dMu`y#t%+Z_|}v1v`OE5E5SZb}qgm?*m-6&{u#W9rgEvT{p@)=9&zYjWV0TaQk! zSH{%-x2HROJ!T~pu_CwL?O_s~2mgwG*LZ+?zjel9KJ>+{{G4Qag()Sj=;KA6`_sVUtWnoYe{ zumS)zoY}=S`>m1s!dJ@9&Y7w%dY_Qm2PBch&8?DmR^i-pyc8aOmAQ!@6#sy{zK5}# zCCB!w?Y2hpi47IW)ghs#e%H?8$%O@jUsNbE3ejiFUF43JUV@giwZ`;=ts(aT9>2CA zUHtxRjNgG>MMMsw^+HC5YsP&aofCXoA%})3G+9HJNapvPW5hfdK-ZD_pnOvcJ-*J) zD~;h(b#=&eE&H&9r#y(y@0E6+)C#Q(-LS4p0wdJKNgKqGL=1_fQSWmSR7_o z?96iG5WQUN@FBfRMbR12tt;x>m!~ajxgD#w-$(kyLGR%$W9WThhR-6)ihA>vhdHp2 zEtdRDL(F%JbOqWrMr1b=$K$}}7#mV~x~{1TdCR;Cj1334^XOSRv=2n>p-W)2k$dk6 zk9HI$7w3&^aq6Q7e-Ilk#bexR;P2lxW}kXdE|hW8#G>@Frb?wVJ$WP^mO+O|eC9Xp zx4C{a!9{U#GF?K%WAF;U=?$^-+_=wP{rdMh0fEbW1b~2xAH_pFetJ3e>UDmW*FGN! zkCA`)u7?BE^n~8}eHu>Bhw)xIm^thuHFq){RObbFd6N)#!g1TY0*y$?7_Zi+J1~YW zO@A3Yb=N}qX*&tOdDYvRs|6D7qDEn75{e0(3&R4C)NiT&GZ$+GqV6-6=LZ!@4WqMi z$_@nZ3)Ex&Z4^Mx)pAgpNSL?0v<{obS&kCHn=Gd|ENPV8_?&TZ)%jRukz zZul`TB|9m<5g)q5=U8p5(D<1OKCv5LMXv$CQJuLt|D)kTh< z*iMpf*E$gPY#V`-xwn#*?4&j`XJNFKkyDB6y?(wc0`e9U(ImkMkRubekMmHT9N?fm1@`-|rbd4YaB6WWT^ z&7ZkVr>L?3vC*!u{ZfLQ(-||!IT0aNc2e%x^#TVxcIM3~x*};Y#V|*`&c(Wb2Dt>k zz;PC>J?~XXuA;YmK3ZcFtk5ZV64+ejXdrOzg=m=G_?w-IPhD48oLk=^^Hx%j z&*7-f=@(Uk_w1h_V&8X-&4;{3@)T`J*lq2ckC+|^diw0tMS5cKP!%5^Iyej9|E;A! zy!*?b41}vRjndZBm2~({EPSu7j)JP>F3D_Y9sLF!uk` z5*4AXNyVZ(Y1_pI-)I4KvhQR>b@h!H39~9z4@F zG=AWMCmfG}Uq_YhD9BNKLi?A(9Y0@u_rKg+`T+(Z5Wje`Yqe&6+-9D)LY7^ zzlgxCKM>YL((Y{7fEU&Nz9_6)^xNfVt53abi5yE#eEM@nh(16UmHxUOt|%@>{xT_Y z0lCD4usQ>hE`;cfry8%Xal$dW?q`?o=_^nFl)1^n^3C44?>p$IXdTSwGbvUqaM=h< zYjzDp{x9C%0xHVCYZnGZEJO*BMp~pn8U_PFlu$srq(QnH1SOZUdMPsF zH8(JT{!5IA8q;dzc<~zzJucx5H1{^sWQd#+Tl&p8sxaKo*6CTzk7tNZ_ote%kBI+D zaS+OtQGL<7>UPG`i2M&Kb5ipHXQx+#?~xJyjNkmQL1zei36Ltv$yF&y@FkL^uiu*jIX>hg#U$iRj zDxa0k6~DC@>D^7)8ZoAWv{RX!^b zT2to>gk{zSFo=gn#V>qe`&pM<>F@%&cpOT-amQH_tY^OK$V-n!Fuh!Z-78N{El=0| zI&R?I95Sqcv~=9jOev_&;>d%jIC_jp>F{y*(%D+oStydeV`GHYZ9bg!jPunON;Fik zh;}@^Lz`jq=#dA=HUyu^JS1&1<}4U$ zuR<&~Qy1#bvye0pOt~x6_X3^6l`T%V5pgB(_$W z#OvbhtjMNM+)}C7;8wncNs?#FZs^S4A#AbEwS$^W%pa$z9abBpE%rvOPmh;)h=lH? zW`Wj~==tH}b63m@i$Bs*Fx4(v7dW&nTIbAo>c%irnxgz{YQq>rXE@A!U-YoKbbkVBb^0;wx+VX+*O9FbSId+6LKBgyu*DWB* zl6%6J?}$l%KsZ0w@TQ{TnEd5OS$vDMxoJKtK65%XYjpl)<*%Ml_>c7iG-u~w8uPPX zzfL3~BZdy02gV;APq_(KEoENyds-1U(8r_6w$FA4@g7bmvizmNoKeB7dHt*Ji8zf2 zwY_Mqe*G*-Ta*5?fhF&x0YM$vUi;SD+k8FtD%!sDjwtu4U^lYCrc7wi*!H1^XL+;R z*|`4kX+;OxKJxXMMcWCF@-PMe3eqZ3AZ)ct{e{~4&S-aUx0`|a0?AG~2c z3hn2z77UXAb9Jmc(R`o7RSI6&Wzm5zyWLvD#6)~Dc?mLVn5S%v5NlvJSiY|p{RTPt z(#yDr&eJ&U$OLakA!$kRAEtKsnco@VO32b)mmglZ=#&6%EpIY4#?e<9RctAAXR`6Z zO!@wk=QorD_ClPJZY0FB8hYX_81SxKc-dEgB~BzV*NA`jyHSqfsv5$!J@p(h33F>C z!nfz(onn;nD2Ee=C+-N#$=YUhxsq&3dM53j9J*CffcDQ$N`U4>MzT?RM9p=#f?zjS zYtjekNy|$OS~eSpV&0_;V4U(s`tpMA_<=d?g~z(B5&VJrm4|2m`oMM*gH4|}dlO%^ zFrqUNapFwY;ktufxp$1T7bJJBnlV#DWY!L~qH<=qUHemZahTUXeG1zSvQbgAZg9>j zoxS;Th0px0HvuyZ4KHEzSo^rAn8SQ+=Bfr*3|i*p4MrcV@9pi`5K>X`#O|6d9>lm2 z8i+p+fIg#_-p06F-l3CYvGIIU=1SWC2 za1)!U;OQ5_}3u?#!}ptoauN!9-aQFF`p&aM*DG8@s_COI-=_`rNCv|>ZT$B+ z-R9(ti)*kHR{2rO)86VJ)f8+Dbw2+|Zb}xd6}E=PphqZ{^`8Uwp9KZqt`akIOU;Gv z{vPyuf5t+gQ!{6)XTr7FbHX7u!zpObh8OP0UnVE9*@~;>hr9FX!qfop2fx!?1sSj#pR96;U0~W@Z-G4#zQ$(Ss=Aqym@|AaC}TplBD5njLZ@SX?+61n*VoLfckU*9PDvk_-q?HtC6^d%{i0ucH4Thqi4-`&rl466|)ZTPuSKSAF?z`^w=!-uKTt(h_EMf{l+R>ZM$OLo# z7~?UA&pnRUNAu9i#%dPJAL>bwNI!pm8%$fM1d4PO2V#iuh!FSQp}BDegwbJ>HqV*w zruOgFdeLwi9uq!%^oZ^7pi8H~a^HaE=z3B5++IP$Iz0{VCHGsE2QSeO$2zn?1Hr(E zDA8?~`-qE#HgJihNnz<^M|bX~F0hTy$E!<>mry~+oXgeUEE2>igkmN?-yb#Q2_U!% zhAVS3k>|kQ0SbkbV8eE*hX-)vAGBFY&4n8N`&I7-<*{p%n0iC;S?0mpw;0=16fRxL zVF;r;cntxRa3S73O{qQl(65k{JcE#uO6XhKyoEa$sYdXcV!irOQ>@wsMdDnC5%sLH zZymxF5$CWfa(-KPl& z)Pymg+~eaZpei`uF+Z=o{8;QAr`s;RRzkvt{V0CuCs`F0w+afIAaQ@cNd;D+y6fr= z@eovHH0g`@L>8$ZVAje^AEA6v-aNQ$AbjH%i6qo0+uGJx`oIE3zcx%wPOg8_wkW^E zW=+oP`ZLOp`$qQAJy%tJVCZmj z-F2Tx8d2)wY}8&!&tJ-3tX^5fdqNiTbIC>tz6tjhiMVp}?WHA>tff3sF2T?XR!No{ zjTYFX?UZ~Baq!bLAY}lDEA)l?JW;UO5sZU-vOO;`+Zl``<&T2=S=dTnYWj6e)S}5kXWscfaYNsEHaiykD#TmoJDX5)VJltSOKD zxdT?xbDNu6T=&Yxm|}QhRpulUQohB|rCkf@YILz>`@vmFA;^ae=k^tH3aBQQgJ?RT3>q@ z+0n-4CM93$c3LTP1)=1>>jBOxvT`yH)I8tWMB7b6SM~kY7qNvEx4G$XuRoVj_N`q6 z3WS$Q8McgC&sGBcU6hjRG2BL|vwM8y02i47hve0Wdx%&t7%W^9bWdzTl`v!488!E_ z<&K?QvPy+_zeD+7hdz3(HP2CtUeJ?|-} zLpk1RR}}{kZR4?$`04JaFyMu2qd_&AxA_<{RfcVMoPKoo{)EC^Z!g#L3C&kWYn~DQ zqL{RWSTEWLg+4)B`ogtAF$+W5oojznI+TUy)I;zonqCXLzAu|k+MZXaYFhXr?n3qv zg5a-EC1fKk-Ay*gR|JvM?p+cNUK|j+mi)lY+WJ2rXaJ?coTtoT=8ZrxY%V`)Ju1{Y zSntpQ!kbK92B-uF8&QDT(w+=mLU1FOzNp~eTmT=or_JTw7s3_fUj5pp=D&OV{Rkgt z@jGK|!};U3x9_~3%g8Uj9M>s$0Xk{tn7!ujV1lmRk-;rMejPjw=Z<$5{FWxZlbkGbMAlskSkAqhE+?j(pMS>|J^Ay5+vy0{;Wo;^ZlqMj!ZgK^=o zh@mWIWYsj*u`BYm$w=S2@9v?uU$Ra@mrw0bVPK!$x}Z4@4$;OZ_uccMFZ+Nf^J;1G z>d{l<-7wZ)x*F-*rKK*Zz(0gEDYVDo0;<{b(y$|NLn@C{N4{-Y+IpY&u5F?iKiU7T zT_y)J$L&l@s0(;o(Sl)MBn!n@<3^*%=twK7pcp)dZ;3SQCR>Vh(v83t3d2k8cyRIR zrAWzJ>z|aPls`_nTnvKs_htD#q~b=_vx4KAhUco!oT(fc-107q@nqb0&B&a0nzsBy z5s}6USzHo>wwaC9Unz@E?(c5)L`aGzNi`QI?Hp` z9VnfX<*$&boy0A?huqfR)iuNFA*;{(Ub#yExy0iBAvjn{&#|*u;RD7;cb5Z8SVM#A z$HPWKu1_B(tEEO%qS9w(;wU*?Fz^}zso1LHp=G8QHqpHM>})}%^MPtd58tk)1nNtKp6O%Hoqx!6ogAg z)>MvCxnk3RuNzP)JbM;(CTdm?F4un#Py%1FVeW7Q&+99jIWNF4n9aqK&Vs#5!F2T5 z)4N&a9!?s+XsI|l!M$HN#}PN-z6rF<~OOLe9Z%@ zw$@)bkgbpxHF)+JAr+-s5TCsRV`-p~SSlVT_$*RESmJb@l-8w}Ez7lVmA{&-s2z2L z)p~l*YNRJaAs1%9{&;#5kbrKwxpB5->Jbw&8;_J!JzLKz8ET7o@hFmm{X9e#A$_8@ zYYeul0$1`!+hVv4Nw@VPH|+&Mijm9k+HM(rNOPD=yFO`N3KSE{`F#dl zJeKiDAy}&1N&9z01Zn;cv!k#D9A6NmDlf;W(s~p!egjxnE zDKQQ-CZs9-+Ulmty%@nH9tUlhL;-1a`sy?71FmzY_ix{@Jl&M9{~4(*ux$S$B7g0V zNjzORyQ4w)QpMR~xJh%Iva?FrEZo!S{{o0gwmfA~Pb)F_3ljYo+01_i1^;E*^MCQi zA6d!#rUu4KshNN58)q;el5G-*)~C|1zaG0oY~;R+ZX#CiQ!~o##?R2?i-C!OrkOXAnx3w=n9BuF z?IH@N6Zs}!G89{Dr|udrFz-Jltf44jVE!*4R79XWV2FLJxShA+o!++k2dkAv+0z_h zR5>0cko%!Z7)sXj&OrZ|2Be+Oahhq9eafFW>_OUuLWv9s28&x1oi__B7*quuG{wD>vDz z+Cb8%6&5rt>T?HU(($z5neVbArDJo(u{BJo9!) z=Nqu%DlP_Q|H=!LHdSHs%xcL$+Wk3Pn>Nw)o6h9w`qTe ziDkCqnZk29`JQ1{El1+(qb^r1a=IX1fsA2uO+z?2KR5FTaT~@A{qDa3x8_`WVe$yi z>~xVD_A#=4U|DU<#Us8}b`5J=z(Faas47~kd-aC3Ud`y@;EJuf)3%W7?yvyRh``ld zz{l^(`D&eCv)*UFFgA0#%FhOF9`EZ{X&q5v1I zM#rsRDBE>N&Ua+FhIBm&Ipn8-+m|>&^BE1wItRoSZy#xF1xurJ28nmfGa2;5vcAL5-d} z(`214T<-S1zO?*YL7TePH14&<0wh8BC$N0>t7`IT zn_3A&WN@pLiEG*`wvcs;KI^fes5P|%H@OyRjiYUwJzDrX;4hIq?7K1gEMqvkOd&+t zJwsp*xxU^Z8#HP$+pPA;gjIqQhVQzT9-ICiuY1adW|BV{w%T~cudiGxyD#Kq>Rs%{ zezb}w`1)Nz20P7tWY@!Rf-YHwQ-|yBu9bRM_xCqE&0OAXCV1b#o#|Nk;%Pr!qvCtz zdgap`ht5uPDjD-Jph%0`mx=nnQ^Y;15)6mC)n;Sw9s6p)0I9wUN zCw!8tqJ@AWG08g9?3c$NHPa`DAZgk>AFD;J!a|`4)t_4D_JP~8>mTtQVzO@|ZFXe! zEo9`zc2-9K-m+aC>)z%&W9$6*nHb&e_Kiz?XZDQR)2%rbH@~4qr5cF7%Q6+aESADh zKs7DfXQr~WKHIIWE_&m&9Yj}+$%}q#5>s%VBI6(a&5rMWpK&Oa@XddXJpzjR|JfU- z24ugHf(Cm2;6Q0L(R2UNYt^&cSf))=+?{=Z*8P#n{^00( zg044cEdzdELCGTOKz}rG!pvHAdU+>69u4XqX5~d;iHv(_Y`h5n0S#BL8s9D#RKu8S zZCP38i-%lJd@o+YzOu5h1O2Q{3ymei4;@bz>tdQ%QMl09O>#7brpu_5FU7|k3zGsq zu%KWkeWcuTxU}9>NM0j~n=b^yZij7Yx5Sd6F$iNCuig3YK;OgBbVFnWn=O`Ed0u|j zBSYlBg|^Dc_u-P=bgkp3qB;Tf9b$L>dRWn^(=Q)N;MjUXJfDzVTz);F^bQ@rN10i4 zOXTr6`K*zrR#u#s!NWO`PFBGT0O!L3$ca9Dy3wM4HrP7=EhJeZ5PTuxcpSVAcjI9* zfeP9aV+k5+uS6NykX6K*|1BK(!*A5| zx~ZMj?};Y)$$k7b74^eN0d3qDFHpjeK{6?bC~{o4r3A@T7f7S{eFRKn+qz=#Ko~*c z4pfDr*2t$aS}5fk*!==unBp<*%~iYU^B~Pn@p%s9X?}m!>WS9lp%9`^gh5R@KiAAr zAadCr#q1j=0?i@;-dKgkRzoDfDk#yM&U_)3STv_LRjqf@jY+eluI`_29u1MUBWC-~ zLYD;3erchQM}B^f8u813n!K3kY|O0i=5v@M?c72Tv__(sPseR^KwJZUfBH~%Z0TXx zm!IT8-d|EZBlr!O01DYz6f0(zR{6~%t`s=c+TcYr6?>8y);2M?K`RIG{Vpnp|4M4E!aesWEPEz&<-HP086 z_j4U_KY(JG_p9e6I0Tc)s61DEIv0U6YSkcExGGqkTJFJZ9j1|AM-yw$Y`3s0L^Q)p(2AHE(b3M1DT?M zyRAJb!|6?!ANt-{fqQrE6~uutGj_5ctW@*)3@gO~TTX}h_YQlt9rjyYkMG^PXLKnO z0xlNrsIBJjjO`TC6kMMU0M-V!8#Y-REg&f+ob;QW6-7C}LSEieV$H1P2V$;RuGLVv z3G37=N?@yFqXKS-nqKo5LtGyW?`MB~C<1a885eO&y9^T{d$?%I*9{1xCE^HJOq|J* zpNsQ7K+%wODj+Zs_!-6LkZO+Rt+SN(qTuIrk752{lxMZEa2bGkpP<)exEdn%m+U6E!{5I*f(}Ui z$_S(V8(<_jSZE~-3$m9>QL)H@-qp3zzk{@I%|d$qmL>FWf}(iIav`7K!2%VXj^mj- zAty%wSf4@n%V(|+E`#T{tiuI$VR;Wv55*$Gi}P2W0Wl0VgmdRw{fUF?!9=PlaK(Y9qDcB}1{P*|%C+gQc(U|*XxUpWOi;W|O*7Mhvjk8rc3qjd zn`dEgxbE+}R}G2ChOEA~p%0*V<4D%uDntyzs3{ zD>1t`CU5@zrAr%TCHLi(#Y6h)LD9obhy`~3HqS=(x0?eFKW zc!WW4+tMd7J~WMe^siy`Qq_5n7Pmpw;yfa2*U+hB53PrfJRFvCoFcgy&WCgz<&}N@ zoYK2o(q{zXrO`t{I{x?Hqm)%y)(?1S6GMNBJkB3U21XYd^A)QH?p2YHb3SdpThSby z&TVA0JgoXWJ#{B;SF>Pf(|$wwTV|IQC?anV5_X!HR17Pvf~Jx^T2g&hfq$aDUZl{O zHD>!%JtgJWDftaRGK0J($8X~z6Cx41q%VSTA|&hkXR^MvI<3uR=i>tl+%Q*plZ&v} z`ZDQ%kTjmCr1wh&X(*~ny|UnY5&UYD-;7`Uat?_5YHFxUptO;75WH%^uV&H=A{X@H z`CSJSbt^6&yOQ<`yVwNBd}l8#CO0gMxdos$_-g4J{32;vuy)DHpw(cStG4!h{5w#y zl-dUB=8eGZhwymCRxH^CpyH{`{1`V64}Phc&$Q?lhJ}P-wtJjA8Tq7^sr#;n(G1Tf z!=y*-ny&HTr5_I;frZX%AvfDZw_^KBkXEj`vMQSFwx7QRvjA8T*M57_1s*_-tXSsC z2;1b~3O2dZMmx97@)C4ODu}+gRuT%Bfo9!OTSnFp88#_4y_@|W@^xV1_tL}UJn3bU{X|A=i8qfMr0yRV?7)J$;L$DjF4+L3~ko*?U!im5C% z_wsHJ&$8nsi;zs>o&}CER9UgTKS~EjOX|Ml0;zih|C4ew;`R=|Dr=Ft3s7tgM5uE) z{R^Gu?$_B+-dGm@`a)rL7K#FDs9957tEoVzscQg@>7d;PA@m9WumAo1`{w4F(J0ym zD3f!n)+uE$>Det->Ks1)RwCAOTfNH+8=Lxxv;fz6%jE3*a$L~u$%JH?hE^Y2(E`U8 z)O=ZK?FGD6g+9{q&b}i^N-Du$--pBg)-F=WT;9w9{X!wOlWp0U0_jJ3s`HQjf$J}1 z2lgy}g~GwleZs6QfWo1)$dHz@^jUWuRkyvUnDo&P)aQOhS_L~-wg(SIeV?t(H`AJ|_f`il*0oilO#&(@j*p=6|Wy3!=_*lTKj`5E+?B}(wZwLJBhiaQbW z4Gh#7Zxx3KC{8%Jy=KumcU{vqzS0}2@>mE2sF4Gk40JX=+Y_|MThxWRj9A^9zX#_b z2j-0vqpqXB<{h9znBhl7hQvht&;IH}O@n5i)(yMky71@agnVwo4hT>v4+DY z4bMjC>bW)Z2z9vMDE_3o@zDLo?R>Mu?=mk_<+i;E((>KWUOb%M(TP8b1DeKnP)+)Xu*06PKV^tvxka0(i+?j-+gP>L{m5h@v|}L7)#sx8+(x^X z^rrprfqBpT3`-NuUL})<2b2Aq*)I&tujcQso%RAl1CX{Btl4A{B{ds}xw zj=E}5d4x@{Rekczek-`oesgQ-aL*KaCvoH&kAzCUlq74)OX6fSyh|5yIjF%C#Z^th ze(e`)b*q`3_?k{q@&^um%++z*?`g|}#RpdD%)2BMGuZs<)`1`(%q94iL*sXLl3&gEkSS=CHj-@pjXLYV_wPd?ZV{ujSJ&^an~i_@D2g5G@Bh-_v508M z>0BQnKMyq-dLIS`I^}))`CX$WkC>8y*StapbE3&8T9%Fheq3c|^2$N7{PvvJYHcS8 z4pD#<*_hk8{j1f(B|~rItFPswueujmEhjM)|3+`9rf*&Of=9mMpse2Ev3d2ve|_dB@W30;k4Zt7j+IqGrHzjx z#BzrW?mz)=6q>)N5iKp+{Fc&i`AOH!(*c17uW!sa!Cg|azl#HLoN`lEDI-AMT*bft zr*iR=^*=wu{8HE7T$0;7wFaK>K1pb(4!oA<<)c00PaV%oJI~+TC1)XDTl4YpSNt<{ z1Fx)j4-Q_(XY1PGpe;{y)C#n^z*nUH`C#{tegg(M2Z)}$zQyU;9hH4^rv`2LBwEo; zWR)v%t-1Ac5gq*JXdqt*b{b#v;1oO<)7VBTAU>DWp}Y6Y&heNTBV*gV(IP(GXkrvT zp}C z!S;eDBRg@x@n%nj-AZv0@}#ZI!EJkOZT)OMsL~TOy?{;^y;=!HUCicnw*&l%qal(Q zghpLbdB{quu;pbN_Al_@7EI52jZa8o4-H~E`HS2VRgho<|uzk(f$Di1) z?$W|*Gt-$W*Y#z8jdsZ`~`3V$x=+$PW2YPh1nEzbR6ZAPUEDsNBkplw#S{cmNl6Q_F==HN8$ zTH$&g?fOK#^tD{Neia!M zU6`kv%#t>KeeNqwHQ(39z6`&K*0t@Qu*H3pU*508X z*BRWq!A1Ac%d37Z_w0B&q%|`1=Uj#ZK7QP*c7JgIM~z%JV(0faS^H(JlRm|yTj%!7 zXKR~LqH!ik*U#E=+HAdDv*nNpfh|6Ux#Cjw@Tf$bz5UIj>jb$D4Tw2f`pp^Yv;$ks z+f^~Hpq3CE8%INL*CcJ|K^^+L3Ye%w|RPtC^mfoI|*!Y`b$gw=`VdU|=# z>3ke4|PrORO~3er0kO#GH_nL%wf03OeGjogcy`?ol-I)Nx!GO>dBv6(2_?; za;uhtXLYhVCr_@AE9X@UxfFgZt$-Ezml}dAf#J^q;3@A_8asN25gEzB5S*8&izXr# zR5PrFbauGex6@`kA=@ZGv0A=5pbF(wQ#MM62|%_z-7u_xReZvMv`MuE5$Yr7)pz_i8k5)24*69;x;pC1_xt90*BOdZ9ek z4-emffVu9_*N9d1)D%=AYPA%upc$PZFNLpBx@Z;6t$WVYPGzsengL;l6TxD$xZC)X4*waN_NRt2-}z$s~&EpFCMR~pT>C0>y23pqfSndnQ?0ciJDcz zfj4;mM0oAguNBZXn)J(#5BEQ0zBIpxOKOb%`AceDa(P=%;K(c;&7Di`ezOWnQ6b__ zK19MDOU=wg!VC$0$<-~QAnY2O5_HE7nAUmP^tW`sawWQm4PMP}=0kdXbH`e$GZ&3bBWH0dDH8A%3s*W#q2$e+nA20}> zED+1jvgmCWvhF)i11~rz3lNa;tIPd(pE#t`9^3TI7s;z6ckcdTonrxM-RRc><>%(9>-zDtaY$p2iP&Rt=L+i6PWzz5= zCY^M-l^Slwf4B=^_l3$Dpy--?62didljc@g0J!ld!3 zemL}5vGR+$%wxPUn=E;`U_k-jBt1s@4qJ?#>8lEG>gdUL%VoZlw-Z*sFu7fRULuAq z(5z~Mu!p`5?#75OSyc+>bgh3>bmnP z&dHICz>f%mT%n`usIU^TqpW87v_pf6S(onMLZ$3$>K~5|nvt(_6kChOOyv4{HxEZh zywii=itUEGx|#H-v)vy$}bGX)719{Js8;B^~u%5<#cGwP8*U))@k6v zKuL4y%I9fhqOilKUOrC4!out+q)E(O28uP7-{r{0J73W5BGgY$j!@Al>Oqufef`gL z_St{;*TnvRd-ixT)`(ZgVK~-E`S%_qB6>Rj6~>|-8h8H5v(X%<-%;zDo>j6J9(FOt|*VAEo;~_PS>chS54Q#7;_=>xKzT)%DG?%h*20}MO>K1F(#1HS0Aapin z_V*NLJ3WM637)VF-G6`1=(g>xn5m}q(!i8PHw!4TCHp`@`zO;gvH8C+cp4eHx_9$o#3p$+@sXKeszDsTtwmt{W zb8KO=I~K`ab^FN9 ztBld5d}kAZh8VGms&+?n-RQ0@KiNd6pJgNWo@l8ORvk3?Nr?NCw41*7@wdP7+3s@I zfRn=ML-KU7{yCYKYR_uAzCKvjW+SwB9(xJ}9tx9jac!%s7VO#)d@dYq{b6g4;N~T` zHF-r@*?4A^VY0cv=%EJ_O-EciY*jz&L_>=9%;R_UCE{C$y8S+dYLul~Srio&Bk&&aZ4y0jn8?s~->F0CC)GR?wEfi{% zsEe{Lf&jd}{xK(~;gxFv{Zngkqt(y^K-PI(-^~2b`DXN%R4(TkmaTrn`kR~=l*hZt zZuR-T+TJDMFcss^rHpd>4gD;B%M094GovcQ5Io;Y*O5Kvol?H1UYvaTm`maAcbBCM z)v$n()j7|gqMrVKoaU%elNk#B2d~pYX#F^f<$4sb8tdJV!EV?nS0j`hafM~?rfi-pFp6~Lioj_o*iNT%AM0W72y&E{_+5_EV0l5HzZ!qeMt7+ycP z%Nddc*tiF@%Ss(;uJnATTIw9|)51dVxY`mjhoQ*r#`YL*D2|*^Aj+rsi1-!E3Gs+r z$F{+=kE&`?(Qbbwbz?@Ud!9BVOt6;T{T%pedgwkQ$hF?|a20!U@gp0z7wU@091da_ zN(N6(=;K$Zv|X=gYin;fR`mAsd??zxQD75rQV0}b@#%g0Kyub4?0C;Ur@O1jWqD9- zynw=Qa|{uN5)-Hm;nnG%i!$*p0ix7Ff*+p2FPuB)4 zwfxKtJRvXCTDRME+Wb*M(-9^0{P_*;vRLYd_XbtIe&|%xb;tQb#cHOz$8t5-^H#+u0ot3sja=eTG?wSr{#RF>Tqh?`7ECQp}~Ji=o{ zayQLre=@ecwko5I{-3n}a?qb?t!hB0Az`V!-zSD%#lHL>2A zc{ud)W0E9|oj5^<%j_$Nb+&67M$qid`()%%!hr0kJ9z578n68mZ8WQT?Y&GrnY1zgX2W z7xLV~OG`1Qr?s4$&-sf6-ip=M>z0$zqqo>qB^vl{YWlXYsE;wse1Ab4F_IGw!``2+ z`s4fP%r)dYjEkF{OS+`OFD1kmY0sS(7?+2Wph-e{ zwlRk)FrNRJgmr`Wi@Z8Pnk?_eF&bB6roI9jo*0v04k!MWa6OA-9rgz4STHN_(d8o& zJZLl;KhV5t+Yru}&7%6DzNtt5Kx^Vvy@*$yZJ!Len*pt@7$X=~Bmm|L?`KlVci&@1r)4aqVzuOU4)_6oxG;L_6|8k8=eooJ7 zN0|#g3fk(54P;i2_1D+82^i$?MTGN*lrZFZ?T1o0`d;oj-tz9)ZeEVxN|v7G(080R zw-Xo9k?Xo!OntxKLkyzjR^u8VRUb}itS4tRwZ2lDEtdlbvpYip_QBb_uMuv$^wOxzz;CEm4JDhKEZvhHBFn(o zxx$61-LPRaK7OuTT`}ign$yo6d9_NJ2UAUXgDV*zz>-n5d=n9I$G`s6&Hv#e0rlEa zucQ$Hb9xZc6sjGiroR3*WTZWk=?pgCR{)s0 z_KMJpReH+Wf&sE73*H#jJ7rH@YuM{)e}d*BoT9LBwK+LuUl0NYxILdVQbDHFJZ}$4 zF`V=kThRzf)9Mfz2k*VT2qq$g2XZ^)@+$gU|wkkeT};9Bw6YuA!5Yw|7A!@K~1_ zZZDAT0kYv>XLo(FE9AC$MPLvA`3oh#+9@5oQyfqT;^Da@#}BXA0;m4-m!_wAef6gv z+E5Ur^ItC(9$tX;m`3IM^y?txodb-yU%%T0m+z#wx zd%G?zsG+*VhP$1nODi4BUO^n*3o0HYTp}hul6X)duhbHw)Z^T&Y+Sf@#Nd>cy2ET& zs%l@M9$#m=5<5l7L6tA?<-L$QjheeeILR zu3F5Z@gl7}vqtOB%a>tWuyIH5vPU0Qn&8KmEo{4(XTL*Lc3D-o7=$%!9~Y%Gr*L)F zrLfdIAfTfDQSy9FfbqwgK_XPp-`Y=zIsH0Svd!n;blfbFuB@A@Uf#j&Mt+@IU0cqJ zuk~%{2}~S#Tsi(AcUg$--aQQW9E+~}0*AvR|?u-?#d`}CkE%;^WrEfFc`{eHah2ox>k?AX# z)J$R2Gjlp@uj}P3bGwIfU8LNqel7yafzwP&WYV3ujs_7ufLAES;IC} znVt|94Q~okzh$W7D70r5)MzQvN@lC8tLHQ0gz%4&TI1SBKShd&zQrRXyilqfztP_o0?Kmbg?hYe9v^{lOpw^y8pCsTe=5z_J+X{G5vcim#EWNcgT4_OFC zp1>r}XYduwcO3da(E^?gCJG2BgpOUsQ-c^N-M>U4ifvpUCzb^rT~zBURcd+rm2yZ6 zj<8g zHH;%z`_RLM;5i3zK!n7bF^a%}45g0B*X<>Vq-P7gJpUX$A?)Bg7XAY}$ zxxAwPQ(5O*T1FZ@3peIAS`E~0_?&1`IDK~Ub=dcuHXGxUq{utoCo08M*++I%@!ogQ z<}@)OuRLnY8naeG0b!4`;trkMaG|2$w}wszj+;ilzT`+NGn(q+w>c1zjCU~vAaUzu zz+dZ4Exn^3ClywryZeRT1&A|}qsi`7zz}B=KtRPZFD~B)SBzUM>T?oJb!#~7jXrM_ znAV!~%BTPpdSbG&E%UptpW?0ZaAmE2V`N2`)>!l_33z)VOc*BW%M?L1b!`^GttIrET z>@>U>GI_~-qfuONbv0cj&%Zc=9VT1OU7qO1Mst9MN)s9il~sH$UDwvq5+v|LVSY~{ zLuO=?D=Nrd`vowB;9g#(r@2Ps&@V@jl+1>p#0J=2$No+UACX9a@1%X#(3)e}F8+-h zH>zcW(zTj@TEKjOQ!m$kN|iv*I{lZTyz45z*b*u(W+^I@{!kTSyUKtzx!52N^+pbY zJYN9l@altf<-2F1P0iZglR~rf%h-k4dUYsr3ZLs%x^(PD_H%g_wI@0vggNZmiWSc6 zEaB0}*dS%|e11MP)n?d$BcfwOVBcDlrOP&$68!#s`Od0p!GK%QYY@HYU8g9E5fH-DVophlwcO%Iq&Nt;Hhp-F310WirRPe(!9zVRG zD@4$k`aM;@R&1oP`HA*y?qiG#N*nfq_qdgYD%Nj2uDgY4!_p>C(jXN>$*8?1S%qEn zJ-0OQI$~VZwq7-iNxAy_tY3|0^{lnU>9lTpn0l!Nb*#}caY}~SWbj1=&76pwg24l= zb;|ZVgbf(B0^q2ha@4t(>@Q9<?7Dqil8IJ#*RLP{=k}N%-#OJ>FCY< z(wmSxZTBCk0PX?s-8uZYH7ASRf2Y)q^SlHJO9WJcGdgVztELGnmURJkA6*v?B7Y-+ zxeV^jWP6iKM=)2v3=RL7*O)nyxPbDLgALRhKm`j5=WmOMqSwedwZG87;E|-4sERT? z{%ZmG|MmaQt;+#!#X6drg!+ax@x`Vdud=Iv|30SvZ%g%dD}TDBm%U7(b+0;X7JTU0 zR@4{RHZ>i(7pQIJ=#`=mKwI=5pv}tl;N+%=*M(~@RJxp(-Bt)c{)Pl4Q9gRXPVLX% zb$l^Jp@y=k$~5Xx^}9y>AD$7tB~W3UZ@%kYMgrhV)E^x1nxYA|>*4Y>!Od8OL8oH- z!NaK?NAGWeK8@y(G{A6Mb{s4PagY5_5B|o|IK@Nq?4Yf_n{vHJQ#B) z!l^<%Q@;POM1hLRGx>J`p5tNfJ;hYRY-~FoP)$T8P5!qysWIA)ttsl>!uOe#EjrsL zpe?QL6xfxZpjCe~Gt)uDD71+0(*5jjKbFoLfzugxt-$Udzp>ox?Rh1hZJ(i7s5V)j z?HbDD4XURpvx(FsyVzd_xH_%wSDY9l(0}!IozO2_%2td1@|V966}5(2WHzJuF81e& zBE;cUKQ&#^+wOrMj4(aiHE&~FT0JHnRWe;mzf~Qc@cqiy-5Evw+ZLj2bFyedH2 zXe;g++i3}TqkT$Qj1jxW7Z=+*s_^@ zAU)q7dO<~?s&w;OOobTY$ln9|-i4z18b_&$)hkhI=y_?aKf@E^|K@s7YpkP-^N=OP zs8N5(Y|lMSn%-S@M9-gpFtYfhu)f{XI@B(}$2P##_HT%o=)e)PGxNqUr%$1csi(4! zfv~YE8X7vDxX1&!1yT6T4{2Yv?@va%pYM7o^5?HgkaW9x!{&mNT~LOgb`&!GRX7FvD@r8`#hn7Pk^~DwI5I&Pjm3K3DmuUh>8bh$Su z-AHI8-23p)f5+DkD=xsqq(feSXBWUZ0+X1y`}Ti!@t&erw`A6RWdH(CS3j3^P6 Date: Sun, 4 Aug 2024 21:28:25 +0200 Subject: [PATCH 004/540] Upload new demo video --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b0b4906..e27fd43 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Web-first cross-platform application launcher with React-based plugins. > > There will probably be breaking changes which will be documented in [changelog](CHANGELOG.md). -https://github.com/project-gauntlet/gauntlet/assets/16986685/c63402f4-b0ca-49c8-8125-5b51f1420ee3 +https://github.com/user-attachments/assets/1aa790dc-fce9-4ac5-97d8-3b81b83acf2e ## Features From 3a43280b933cf7692350accce6145b32ee0f3a73 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:49:54 +0200 Subject: [PATCH 005/540] Allow for icon in GeneratedCommand to be undefined --- dev_plugin/src/command-generator.ts | 6 +----- js/api/src/helpers.ts | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/dev_plugin/src/command-generator.ts b/dev_plugin/src/command-generator.ts index 4d23108..50e8449 100644 --- a/dev_plugin/src/command-generator.ts +++ b/dev_plugin/src/command-generator.ts @@ -1,8 +1,4 @@ -interface GeneratedCommand { // TODO Add this type to api - id: string - name: string - fn: () => void -} +import { GeneratedCommand } from "@project-gauntlet/api/helpers"; export default function CommandGenerator(): GeneratedCommand[] { return [ diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index 25776a0..26ab642 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -20,7 +20,7 @@ export function entrypointPreferences>(): T { export interface GeneratedCommand { id: string name: string - icon: ArrayBuffer | undefined + icon?: ArrayBuffer fn: () => void } From 7b6c1b91bfdacbc8d51775c24b7beceb21779877 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:50:15 +0200 Subject: [PATCH 006/540] Add npm script to build dev plugin from workspace root --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e6b855c..dd62cdd 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "scripts": { "build-all": "npm run build --workspace tools && npm run build --workspaces --if-present", "build-scenarios": "npm run build --workspace tools && npm run build --workspace scenarios --if-present", + "build-dev-plugin": "npm run build --workspace tools && npm run build --workspace dev_plugin", "build": "npm run build --workspace tools && npm run build --workspace js --workspace bundled_plugins --if-present", "run-scenarios": "npm run run-scenarios --workspace js/scenario_runner_cli", "run-screenshot-gen": "npm run run-screenshot-gen --workspace js/scenario_runner_cli" From 5cd3d53c47e6c8253b8d217be851ca49e1ab98f2 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:50:42 +0200 Subject: [PATCH 007/540] Tweak plugin event stopped loop --- js/core/src/init.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/core/src/init.tsx b/js/core/src/init.tsx index 9e4fd45..1f175d5 100644 --- a/js/core/src/init.tsx +++ b/js/core/src/init.tsx @@ -210,7 +210,7 @@ async function runLoop() { break; } case "StopPlugin": { - InternalApi.op_log_info("plugin_loop", "Received plugin stop command") + // break the loop return; } case "ReloadSearchIndex": { @@ -235,4 +235,5 @@ try { (async () => { await runLoop() + InternalApi.op_log_info("plugin_loop", "Event loop was stopped") })(); From e084328368a66517fc0b5da425e24082c73081d0 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 5 Aug 2024 22:17:59 +0200 Subject: [PATCH 008/540] Always use dev react bundle for local plugins for better error messages --- rust/server/src/plugins/js/mod.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 9ae08c5..7a4abeb 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -292,9 +292,9 @@ async fn start_js_runtime( prompt: false, })?); - let stdout_to_file = plugin_id.to_string().starts_with("file://"); + let dev_plugin = plugin_id.to_string().starts_with("file://"); - let (stdout, stderr) = if stdout_to_file { + let (stdout, stderr) = if dev_plugin { let (out_log_file, err_log_file) = dirs.plugin_log_files(&plugin_uuid); std::fs::create_dir_all(out_log_file.parent().unwrap())?; @@ -322,7 +322,7 @@ async fn start_js_runtime( unused_url, permissions_container, WorkerOptions { - module_loader: Rc::new(CustomModuleLoader::new(code)), + module_loader: Rc::new(CustomModuleLoader::new(code, dev_plugin)), extensions: vec![plugin_ext::init_ops_and_esm( EventReceiver::new(event_stream), PluginData::new(plugin_id, plugin_uuid, inline_view_entrypoint_id), @@ -356,16 +356,18 @@ async fn start_js_runtime( pub struct CustomModuleLoader { code: PluginCode, static_loader: StaticModuleLoader, + dev_plugin: bool, } impl CustomModuleLoader { - fn new(code: PluginCode) -> Self { + fn new(code: PluginCode, dev_plugin: bool) -> Self { let module_map: HashMap<_, _> = MODULES.iter() .map(|(key, value)| (key.parse().expect("provided key is not valid url"), FastString::from_static(value))) .collect(); Self { code, static_loader: StaticModuleLoader::new(module_map), + dev_plugin } } } @@ -404,11 +406,13 @@ impl ModuleLoader for CustomModuleLoader { } } + let prod_react = cfg!(feature = "release") && !self.dev_plugin; + let specifier = match (specifier, referrer) { ("gauntlet:core", _) => "gauntlet:core", - ("gauntlet:renderer", _) => if cfg!(feature = "release") { "gauntlet:renderer:prod" } else { "gauntlet:renderer:dev" }, - ("react", _) => if cfg!(feature = "release") { "gauntlet:react:prod" } else { "gauntlet:react:dev" }, - ("react/jsx-runtime", _) => if cfg!(feature = "release") { "gauntlet:react-jsx-runtime:prod" } else { "gauntlet:react-jsx-runtime:dev" }, + ("gauntlet:renderer", _) => if prod_react { "gauntlet:renderer:prod" } else { "gauntlet:renderer:dev" }, + ("react", _) => if prod_react { "gauntlet:react:prod" } else { "gauntlet:react:dev" }, + ("react/jsx-runtime", _) => if prod_react { "gauntlet:react-jsx-runtime:prod" } else { "gauntlet:react-jsx-runtime:dev" }, ("@project-gauntlet/api/components", _) => "gauntlet:api-components", ("@project-gauntlet/api/hooks", _) => "gauntlet:api-hooks", ("@project-gauntlet/api/helpers", _) => "gauntlet:api-helpers", From 2f2f2fd4c5440d859629ed061222c600bf707c4a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 5 Aug 2024 22:35:14 +0200 Subject: [PATCH 009/540] Fix Deno runtime being unable to terminate after usage of React to render view --- js/react_renderer/src/renderer.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index 269bbf8..6e35e20 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -6,6 +6,13 @@ import { DefaultEventPriority } from 'react-reconciler/constants'; const denoCore: DenoCore = Deno[Deno.internal].core; const InternalApi = denoCore.ops; +// Usage of MessageChannel seems to block Deno runtime from exiting +// causing plugin to be in stuck state where it is disabled but still have running runtime +// +// For some reason React prefers MessageChannel to setTimeout but +// will fall back on setTimeout if MessageChannel is not present +globalThis.MessageChannel = undefined as any; + class HostContext { constructor(public nextId: number, public componentModel: Record) { } From 7d46fd2b1663c21037c73295ff6b000a5c604099 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:43:15 +0200 Subject: [PATCH 010/540] Slightly change implementation of hiding window on unfocus --- rust/client/src/ui/mod.rs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 2725502..61df894 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -56,7 +56,7 @@ pub struct AppModel { frontend_receiver: Arc>>, search_field_id: text_input::Id, scrollable_id: scrollable::Id, - waiting_for_next_unfocus: bool, + focused: bool, theme: GauntletTheme, wayland: bool, @@ -367,7 +367,7 @@ impl Application for AppModel { frontend_receiver: Arc::new(TokioRwLock::new(frontend_receiver)), search_field_id: text_input::Id::unique(), scrollable_id: scrollable::Id::unique(), - waiting_for_next_unfocus: false, + focused: false, theme: GauntletTheme::new(), wayland, @@ -543,6 +543,9 @@ impl Application for AppModel { _ => Command::none() } } + AppMsg::IcedEvent(Event::Window(_, window::Event::Focused)) => { + self.on_focused() + } AppMsg::IcedEvent(Event::Window(_, window::Event::Unfocused)) => { self.on_unfocused() } @@ -558,7 +561,8 @@ impl Application for AppModel { ) ) ) => { - self.on_unfocused() + // wayland layer shell doesn't have the same unfocused problem as the other platforms + self.hide_window() } AppMsg::IcedEvent(_) => Command::none(), AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => self.previous_view(), @@ -1049,21 +1053,20 @@ impl Application for AppModel { const ESTIMATED_ITEM_SIZE: f32 = 38.8; impl AppModel { + fn on_focused(&mut self) -> Command { + tracing::info!("on_focused"); + self.focused = true; + Command::none() + } + fn on_unfocused(&mut self) -> Command { - if self.wayland { + tracing::info!("on_unfocused: {}", self.focused); + // for some reason (on both macos and linux x11) duplicate Unfocused fires right before Focus event + if self.focused { + self.focused = false; self.hide_window() } else { - // for some reason (on both macos and linux x11) Unfocused fires right at the application start - // and second time on actual window unfocus - if self.waiting_for_next_unfocus { - if cfg!(target_os = "linux") { // gnome requires double global shortcut press (probably gnome bug, because works on kde). - self.waiting_for_next_unfocus = false; - } - self.hide_window() - } else { - self.waiting_for_next_unfocus = true; - Command::none() - } + Command::none() } } From 06db1ffa30ff5569ae57796a30747a87ed7b45c0 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:16:52 +0200 Subject: [PATCH 011/540] Fix being unable to stop plugin if there is unresolved pending promise --- Cargo.lock | 6 +- dev_plugin/src/detail-view.tsx | 11 +++ js/core/src/init.tsx | 6 +- js/typings/index.d.ts | 6 +- rust/client/src/ui/mod.rs | 2 - rust/server/Cargo.toml | 1 + rust/server/src/model.rs | 2 - rust/server/src/plugins/js/mod.rs | 105 ++++++++++++++------------ rust/server/src/plugins/mod.rs | 7 +- rust/server/src/plugins/run_status.rs | 35 +++++++-- 10 files changed, 105 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4846692..a3c75c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7305,6 +7305,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tokio-util", "toml 0.8.12", "tonic", "tracing", @@ -8904,16 +8905,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] diff --git a/dev_plugin/src/detail-view.tsx b/dev_plugin/src/detail-view.tsx index 1ece6a9..3e4ed73 100644 --- a/dev_plugin/src/detail-view.tsx +++ b/dev_plugin/src/detail-view.tsx @@ -68,6 +68,17 @@ export default function DetailView(): ReactElement { }; }, []); + // promise that takes a long time to resolve + // to test that plugin can be stopped even though there is a pending promise somewhere + new Promise(resolve => { + setTimeout( + () => { + resolve("Promise resolved after 10 minutes!"); + }, + 10 * 60 * 1000 + ); + }) + return ( Command { - tracing::info!("on_focused"); self.focused = true; Command::none() } fn on_unfocused(&mut self) -> Command { - tracing::info!("on_unfocused: {}", self.focused); // for some reason (on both macos and linux x11) duplicate Unfocused fires right before Focus event if self.focused { self.focused = false; diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 6c4bbd5..830a15e 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -7,6 +7,7 @@ serde = { version = "1.0", features = ["derive"] } deno_core = { version = "0.204.0" } deno_runtime = { version = "0.126.0" } tokio = "1.28.1" +tokio-util = "0.7.11" toml = "0.8.10" tantivy = "0.20.2" zstd-sys = "=2.0.9" # TODO REMOVE https://github.com/gyscos/zstd-rs/issues/270 diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index cff77d6..a14ad64 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -73,7 +73,6 @@ pub enum JsUiEvent { #[serde(rename = "modifierMeta")] modifier_meta: bool }, - StopPlugin, OpenInlineView { #[serde(rename = "text")] text: String, @@ -134,7 +133,6 @@ pub enum IntermediateUiEvent { modifier_alt: bool, modifier_meta: bool }, - StopPlugin, OpenInlineView { text: String, }, diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 7a4abeb..b92fdf9 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -1,12 +1,3 @@ -mod ui; -mod plugins; -mod logs; -mod assets; -mod preferences; -mod search; -mod command_generators; -mod clipboard; - use std::cell::RefCell; use std::collections::HashMap; use std::fs::File; @@ -27,25 +18,26 @@ use deno_runtime::permissions::{Permissions, PermissionsContainer, PermissionsOp use deno_runtime::worker::MainWorker; use deno_runtime::worker::WorkerOptions; use indexmap::IndexMap; - use once_cell::sync::Lazy; use regex::Regex; use serde::{Deserialize, Serialize}; use tokio::net::TcpStream; +use tokio_util::sync::CancellationToken; -use common::model::{EntrypointId, PluginId, UiRenderLocation, UiPropertyValue, UiWidget, UiWidgetId, SearchResultEntrypointType, PhysicalKey}; +use common::dirs::Dirs; +use common::model::{EntrypointId, PhysicalKey, PluginId, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidget, UiWidgetId}; use common::rpc::frontend_api::FrontendApi; use component_model::{Children, Component, create_component_model, Property, PropertyType, SharedType}; -use common::dirs::Dirs; -use crate::model::{IntermediateUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiEvent, JsUiRequestData, JsUiResponseData, JsUiWidget, PreferenceUserData}; + +use crate::model::{IntermediateUiEvent, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData, JsUiWidget, PreferenceUserData}; use crate::plugins::applications::{DesktopEntry, get_apps}; use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; -use crate::plugins::js::plugins::applications::{list_applications, open_application}; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; use crate::plugins::js::command_generators::get_command_generator_entrypoint_ids; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; +use crate::plugins::js::plugins::applications::{list_applications, open_application}; use crate::plugins::js::plugins::numbat::run_numbat; use crate::plugins::js::plugins::settings::open_settings; use crate::plugins::js::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; @@ -54,6 +46,15 @@ use crate::plugins::js::ui::{clear_inline_view, fetch_action_id_for_shortcut, op use crate::plugins::run_status::RunStatusGuard; use crate::search::{SearchIndex, SearchIndexItem}; +mod ui; +mod plugins; +mod logs; +mod assets; +mod preferences; +mod search; +mod command_generators; +mod clipboard; + pub struct PluginRuntimeData { pub id: PluginId, pub uuid: String, @@ -96,7 +97,6 @@ pub enum PluginCommand { #[derive(Clone, Debug)] pub enum OnePluginCommandData { - Stop, RenderView { entrypoint_id: EntrypointId, }, @@ -151,9 +151,6 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run None } else { match data { - OnePluginCommandData::Stop => { - Some(IntermediateUiEvent::StopPlugin) - } OnePluginCommandData::RenderView { entrypoint_id } => { Some(IntermediateUiEvent::OpenView { entrypoint_id, @@ -211,42 +208,55 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run let event_stream = Box::pin(event_stream); - let thread_fn = move || { - let _run_status_guard = run_status_guard; + let cache = data.icon_cache.clone(); + let plugin_uuid = data.uuid.clone(); + let plugin_id = data.id.clone(); - let result_plugin_id = data.id.clone(); - let result = tokio::runtime::Builder::new_current_thread() + let thread_fn = move || { + let plugin_id = data.id.clone(); + + tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("unable to start tokio runtime for plugin") - .block_on(tokio::task::unconstrained(async move { - let result_plugin_id = data.id.clone(); - let result = start_js_runtime( - data.id, - data.uuid.clone(), - data.code, - data.permissions, - data.inline_view_entrypoint_id, - event_stream, - data.frontend_api, - component_model, - data.db_repository, - data.search_index, - data.icon_cache.clone(), - data.dirs.clone() - ).await; + .block_on({ + let plugin_id = data.id.clone(); - if let Err(err) = data.icon_cache.clear_plugin_icon_cache_dir(&data.uuid) { - tracing::error!(target = "plugin", "plugin {:?} unable to cleanup icon cache {:?}", result_plugin_id, err) + async move { + tokio::select! { + _ = run_status_guard.stopped() => { + tracing::info!(target = "plugin", "Plugin runtime has been stopped {:?}", plugin_id) + } + result @ _ = { + tokio::task::unconstrained(async move { + start_js_runtime( + data.id.clone(), + data.uuid, + data.code, + data.permissions, + data.inline_view_entrypoint_id, + event_stream, + data.frontend_api, + component_model, + data.db_repository, + data.search_index, + data.icon_cache, + data.dirs + ).await + }) + } => { + if let Err(err) = result { + tracing::error!(target = "plugin", "Plugin runtime has failed {:?} - {:?}", plugin_id, err) + } else { + tracing::error!(target = "plugin", "Plugin runtime has stopped unexpectedly {:?}", plugin_id) + } + } + } } + }); - result - })); - - if let Err(err) = result { - tracing::error!(target = "plugin", "Plugin runtime failed {:?} - {:?}", result_plugin_id, err) - } else { - tracing::info!(target = "plugin", "Plugin runtime stopped {:?}", result_plugin_id) + if let Err(err) = cache.clear_plugin_icon_cache_dir(&plugin_uuid) { + tracing::error!(target = "plugin", "plugin {:?} unable to cleanup icon cache {:?}", plugin_id, err) } }; @@ -647,7 +657,6 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { modifier_meta } } - IntermediateUiEvent::StopPlugin => JsUiEvent::StopPlugin, IntermediateUiEvent::OpenInlineView { text } => JsUiEvent::OpenInlineView { text }, IntermediateUiEvent::ReloadSearchIndex => JsUiEvent::ReloadSearchIndex, } diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 5ba9c26..7420617 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -578,12 +578,7 @@ impl ApplicationManager { async fn stop_plugin(&self, plugin_id: PluginId) { tracing::info!(target = "plugin", "Stopping plugin with id: {:?}", plugin_id); - let data = PluginCommand::One { - id: plugin_id, - data: OnePluginCommandData::Stop, - }; - - self.send_command(data) + self.run_status_holder.stop_plugin(&plugin_id) } fn start_plugin_runtime(&self, data: PluginRuntimeData) { diff --git a/rust/server/src/plugins/run_status.rs b/rust/server/src/plugins/run_status.rs index a0444b2..dc83128 100644 --- a/rust/server/src/plugins/run_status.rs +++ b/rust/server/src/plugins/run_status.rs @@ -1,22 +1,24 @@ -use std::collections::HashSet; +use std::collections::HashMap; use std::sync::{Arc, Mutex}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; + use common::model::PluginId; pub struct RunStatusHolder { - running_plugins: Arc>> + running_plugins: Arc>> } impl RunStatusHolder { pub fn new() -> Self { Self { - running_plugins: Arc::new(Mutex::new(HashSet::new())) + running_plugins: Arc::new(Mutex::new(HashMap::new())) } } pub fn start_block(&self, plugin_id: PluginId) -> RunStatusGuard { let mut running_plugins = self.running_plugins.lock().expect("lock is poisoned"); - running_plugins.insert(plugin_id.clone()); + running_plugins.insert(plugin_id.clone(), CancellationToken::new()); RunStatusGuard { running_plugins: self.running_plugins.clone(), id: plugin_id, @@ -25,13 +27,34 @@ impl RunStatusHolder { pub fn is_plugin_running(&self, plugin_id: &PluginId) -> bool { let running_plugins = self.running_plugins.lock().expect("lock is poisoned"); - running_plugins.contains(plugin_id) + running_plugins.contains_key(plugin_id) + } + + pub fn stop_plugin(&self, plugin_id: &PluginId) { + let mut running_plugins = self.running_plugins.lock().expect("lock is poisoned"); + + running_plugins + .get(plugin_id) + .expect("value should always exist for specified id") + .cancel() } } pub struct RunStatusGuard { id: PluginId, - running_plugins: Arc>>, + running_plugins: Arc>>, +} + +impl RunStatusGuard { + pub fn stopped(&self) -> WaitForCancellationFutureOwned { + let mut running_plugins = self.running_plugins.lock().expect("lock is poisoned"); + + running_plugins + .get(&self.id) + .expect("value should always exist for specified id") + .clone() + .cancelled_owned() + } } impl Drop for RunStatusGuard { From 764ca8391cd46311af510f8d9788c70cd15640ce Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:32:44 +0200 Subject: [PATCH 012/540] Fix generator commands blocking plugin from being stopped/stopped --- README.md | 1 + bundled_plugins/applications/src/default.ts | 8 +-- js/core/src/command-generator.ts | 22 ++++--- js/core/src/init.tsx | 17 ++---- rust/server/src/plugins/js/logs.rs | 60 +++++++++++++++---- .../src/plugins/js/plugins/applications.rs | 7 ++- 6 files changed, 78 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index e27fd43..ed5e321 100644 --- a/README.md +++ b/README.md @@ -375,6 +375,7 @@ To build release run: git submodule update --init npm ci npm run build +npm run build-dev-plugin cargo build --release --features release ``` But the new version release needs to be done via GitHub Actions diff --git a/bundled_plugins/applications/src/default.ts b/bundled_plugins/applications/src/default.ts index 923428f..44b64fd 100644 --- a/bundled_plugins/applications/src/default.ts +++ b/bundled_plugins/applications/src/default.ts @@ -11,12 +11,12 @@ const denoCore: DenoCore = Deno[Deno.internal].core; const InternalApi: InternalApi = denoCore.ops; interface InternalApi { - list_applications(): DesktopEntry[] + list_applications(): Promise open_application(command: string[]): void } -export default function Default(): GeneratedCommand[] { - return InternalApi.list_applications() +export default async function Default(): Promise { + return (await InternalApi.list_applications()) .map(value => ({ id: `${value.name}-${value.command.join("-")}`, name: value.name, @@ -24,5 +24,5 @@ export default function Default(): GeneratedCommand[] { fn: () => { InternalApi.open_application(value.command) } - })) + })); } diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index b20757e..07fa2ec 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -2,24 +2,28 @@ const denoCore: DenoCore = Deno[Deno.internal].core; const InternalApi = denoCore.ops; -interface GeneratedCommand { // TODO Add this type to api +interface GeneratedCommand { // TODO is it possible to import api here id: string name: string - icon: ArrayBuffer | undefined + icon?: ArrayBuffer fn: () => void } -let storedGeneratedCommands: (GeneratedCommand & { lookupId: string, uuid: string })[] = [] +type ProcessedGeneratedCommand = GeneratedCommand & { lookupId: string, uuid: string }; + +let storedGeneratedCommands: ProcessedGeneratedCommand[] = [] export async function runCommandGenerators(): Promise { - storedGeneratedCommands = [] + let localGeneratedCommands: ProcessedGeneratedCommand[] = [] const entrypointIds = await InternalApi.get_command_generator_entrypoint_ids(); for (const generatorEntrypointId of entrypointIds) { try { - const generator: () => GeneratedCommand[] = (await import(`gauntlet:entrypoint?${generatorEntrypointId}`)).default; + const generator: () => Promise = (await import(`gauntlet:entrypoint?${generatorEntrypointId}`)).default; - const generatedCommands = generator() + InternalApi.op_log_info("command_generator", `Running command generator for entrypoint ${generatorEntrypointId}`) + + const generatedCommands = (await generator()) .map(value => { return { lookupId: generatorEntrypointId + ":" + value.id, @@ -28,11 +32,15 @@ export async function runCommandGenerators(): Promise { } }); - storedGeneratedCommands.push(...generatedCommands) + InternalApi.op_log_info("command_generator", `Finished running command generator for entrypoint ${generatorEntrypointId}, amount: ${generatedCommands.length}`) + + localGeneratedCommands.push(...generatedCommands) } catch (e) { console.error("Error occurred when calling command generator for entrypoint: " + generatorEntrypointId, e) } } + + storedGeneratedCommands = localGeneratedCommands } export function generatedCommandSearchIndex(): AdditionalSearchItem[] { diff --git a/js/core/src/init.tsx b/js/core/src/init.tsx index 5a08f42..5a28b7e 100644 --- a/js/core/src/init.tsx +++ b/js/core/src/init.tsx @@ -212,26 +212,17 @@ async function runLoop() { break; } case "ReloadSearchIndex": { - try { - await runCommandGenerators(); - await loadSearchIndex(false); - } catch (e) { - console.error("Error occurred when reloading search index", e) - } + runCommandGenerators() + .then(() => loadSearchIndex(false)); break; } } } } -try { - await runCommandGenerators(); - await loadSearchIndex(true); -} catch (e) { - console.error("Error occurred when reloading search index", e) -} +runCommandGenerators() + .then(() => loadSearchIndex(true)); (async () => { await runLoop() - InternalApi.op_log_info("plugin_loop", "Event loop was stopped") })(); diff --git a/rust/server/src/plugins/js/logs.rs b/rust/server/src/plugins/js/logs.rs index 1181139..460c209 100644 --- a/rust/server/src/plugins/js/logs.rs +++ b/rust/server/src/plugins/js/logs.rs @@ -1,26 +1,64 @@ -use deno_core::op; +use std::cell::RefCell; +use std::rc::Rc; +use deno_core::{op, OpState}; +use crate::plugins::js::PluginData; #[op] -fn op_log_trace(target: String, message: String) { - tracing::trace!(target = target, message) +fn op_log_trace(state: Rc>, target: String, message: String) -> anyhow::Result<()> { + let plugin_id = state.borrow() + .borrow::() + .plugin_id() + .to_string(); + + tracing::trace!(target = target, plugin_id = plugin_id, message); + + Ok(()) } #[op] -fn op_log_debug(target: String, message: String) { - tracing::debug!(target = target, message) +fn op_log_debug(state: Rc>, target: String, message: String) -> anyhow::Result<()> { + let plugin_id = state.borrow() + .borrow::() + .plugin_id() + .to_string(); + + tracing::debug!(target = target, plugin_id = plugin_id, message); + + Ok(()) } #[op] -fn op_log_info(target: String, message: String) { - tracing::info!(target = target, message) +fn op_log_info(state: Rc>, target: String, message: String) -> anyhow::Result<()> { + let plugin_id = state.borrow() + .borrow::() + .plugin_id() + .to_string(); + + tracing::info!(target = target, plugin_id = plugin_id, message); + + Ok(()) } #[op] -fn op_log_warn(target: String, message: String) { - tracing::warn!(target = target, message) +fn op_log_warn(state: Rc>, target: String, message: String) -> anyhow::Result<()> { + let plugin_id = state.borrow() + .borrow::() + .plugin_id() + .to_string(); + + tracing::warn!(target = target, plugin_id = plugin_id, message); + + Ok(()) } #[op] -fn op_log_error(target: String, message: String) { - tracing::error!(target = target, message) +fn op_log_error(state: Rc>, target: String, message: String) -> anyhow::Result<()> { + let plugin_id = state.borrow() + .borrow::() + .plugin_id() + .to_string(); + + tracing::error!(target = target, plugin_id = plugin_id, message); + + Ok(()) } \ No newline at end of file diff --git a/rust/server/src/plugins/js/plugins/applications.rs b/rust/server/src/plugins/js/plugins/applications.rs index d8f736e..a229332 100644 --- a/rust/server/src/plugins/js/plugins/applications.rs +++ b/rust/server/src/plugins/js/plugins/applications.rs @@ -1,10 +1,13 @@ use std::path::Path; + use deno_core::op; +use tokio::task::spawn_blocking; + use crate::plugins::applications::{DesktopEntry, get_apps}; #[op] -fn list_applications() -> Vec { - get_apps() +async fn list_applications() -> anyhow::Result> { + Ok(spawn_blocking(|| get_apps()).await?) } #[op] From 1dbd06844163481b96ecf1970d731b9b3efe6ea5 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 10 Aug 2024 15:10:53 +0200 Subject: [PATCH 013/540] Add proper types to make it explicit what value could be returned from command and generated command --- js/core/src/command-generator.ts | 2 +- js/core/src/init.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index 07fa2ec..fdf4009 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -19,7 +19,7 @@ export async function runCommandGenerators(): Promise { const entrypointIds = await InternalApi.get_command_generator_entrypoint_ids(); for (const generatorEntrypointId of entrypointIds) { try { - const generator: () => Promise = (await import(`gauntlet:entrypoint?${generatorEntrypointId}`)).default; + const generator: () => Promise | GeneratedCommand[] = (await import(`gauntlet:entrypoint?${generatorEntrypointId}`)).default; InternalApi.op_log_info("command_generator", `Running command generator for entrypoint ${generatorEntrypointId}`) diff --git a/js/core/src/init.tsx b/js/core/src/init.tsx index 5a28b7e..17b102e 100644 --- a/js/core/src/init.tsx +++ b/js/core/src/init.tsx @@ -172,7 +172,7 @@ async function runLoop() { break; } - const command: () => void = (await import(`gauntlet:entrypoint?${pluginEvent.entrypointId}`)).default; + const command: () => Promise | void = (await import(`gauntlet:entrypoint?${pluginEvent.entrypointId}`)).default; command() } catch (e) { console.error("Error occurred when running a command", pluginEvent.entrypointId, e) From c02af18c18b973430aa73c2f6238f23666fa0f50 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 10 Aug 2024 20:23:47 +0200 Subject: [PATCH 014/540] Move macos assets into subdirectory --- assets/{ => macos}/AppIcon.icns | Bin assets/{ => macos}/Info.plist | 0 assets/{ => macos}/dmg-background.png | Bin js/build/src/main.ts | 7 +++---- 4 files changed, 3 insertions(+), 4 deletions(-) rename assets/{ => macos}/AppIcon.icns (100%) rename assets/{ => macos}/Info.plist (100%) rename assets/{ => macos}/dmg-background.png (100%) diff --git a/assets/AppIcon.icns b/assets/macos/AppIcon.icns similarity index 100% rename from assets/AppIcon.icns rename to assets/macos/AppIcon.icns diff --git a/assets/Info.plist b/assets/macos/Info.plist similarity index 100% rename from assets/Info.plist rename to assets/macos/Info.plist diff --git a/assets/dmg-background.png b/assets/macos/dmg-background.png similarity index 100% rename from assets/dmg-background.png rename to assets/macos/dmg-background.png diff --git a/js/build/src/main.ts b/js/build/src/main.ts index a3bbfc2..875e532 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -284,8 +284,9 @@ async function packageForMacos(projectRoot: string, arch: string): Promise<{ fil const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet'); const outFileName = "gauntlet-aarch64-macos.dmg" const outFilePath = path.join(releaseDirPath, outFileName); - const sourceInfoFilePath = path.join(projectRoot, 'assets', 'Info.plist'); - const sourceIconFilePath = path.join(projectRoot, 'assets', 'AppIcon.icns'); + const sourceInfoFilePath = path.join(projectRoot, 'assets', 'macos', 'Info.plist'); + const sourceIconFilePath = path.join(projectRoot, 'assets', 'macos', 'AppIcon.icns'); + const dmgBackground = path.join(projectRoot, 'assets', 'macos', 'dmg-background.png'); const bundleDir = path.join(releaseDirPath, 'Gauntlet.app'); const contentsDir = path.join(bundleDir, 'Contents'); @@ -295,8 +296,6 @@ async function packageForMacos(projectRoot: string, arch: string): Promise<{ fil const targetInfoFilePath = path.join(contentsDir, 'Info.plist'); const targetIconFilePath = path.join(resourcesContentsDir, 'AppIcon.icns'); - const dmgBackground = path.join(projectRoot, 'assets', 'dmg-background.png'); - const version = await readVersion(projectRoot) mkdirSync(bundleDir) From 4e3b023fef7c1eb4a279ab6364fc6515b159acb1 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 11 Aug 2024 11:48:08 +0200 Subject: [PATCH 015/540] Add service, desktop and icon files to linux tar.gz archive --- assets/linux/gauntlet.desktop | 14 ++++++++ assets/linux/gauntlet.service | 12 +++++++ assets/linux/icon_256.png | Bin 0 -> 14803 bytes js/build/src/main.ts | 60 +++++++++++++++++++++++++++------- 4 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 assets/linux/gauntlet.desktop create mode 100644 assets/linux/gauntlet.service create mode 100644 assets/linux/icon_256.png diff --git a/assets/linux/gauntlet.desktop b/assets/linux/gauntlet.desktop new file mode 100644 index 0000000..af84cef --- /dev/null +++ b/assets/linux/gauntlet.desktop @@ -0,0 +1,14 @@ +[Desktop Entry] +Version=1.0 +Name=Gauntlet +Comment=Application launcher +Exec=gauntlet +NoDisplay=true +Icon=gauntlet +Terminal=false +Type=Application +Actions=settings; + +[Desktop Action settings] +Name=Gauntlet Settings +Exec=gauntlet settings \ No newline at end of file diff --git a/assets/linux/gauntlet.service b/assets/linux/gauntlet.service new file mode 100644 index 0000000..02c9809 --- /dev/null +++ b/assets/linux/gauntlet.service @@ -0,0 +1,12 @@ +[Unit] +Description=Gauntlet +PartOf=graphical-session.target +After=graphical-session.target + +[Service] +Type=exec +Restart=on-failure +ExecStart=/usr/bin/gauntlet --minimized + +[Install] +WantedBy=graphical-session.target \ No newline at end of file diff --git a/assets/linux/icon_256.png b/assets/linux/icon_256.png new file mode 100644 index 0000000000000000000000000000000000000000..fb55ec5a206ed361191ee7c7b6a242770d03f13c GIT binary patch literal 14803 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 literal 0 HcmV?d00001 diff --git a/js/build/src/main.ts b/js/build/src/main.ts index 875e532..b365ee2 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -47,10 +47,9 @@ program.command('build-macos') await program.parseAsync(process.argv); -async function doBuild(arch: string) { +async function doBuild(projectRoot: string, arch: string) { console.log("Building Gauntlet...") - const projectRoot = getProjectRoot(); build(projectRoot, true, arch) } @@ -79,7 +78,11 @@ async function doPublishLinux() { } async function doBuildLinux() { - await doBuild('x86_64-unknown-linux-gnu') + const arch = 'x86_64-unknown-linux-gnu'; + const projectRoot = getProjectRoot(); + + await doBuild(projectRoot, arch) + packageForLinux(projectRoot, arch) } async function doPublishMacOS() { @@ -95,7 +98,11 @@ async function doPublishMacOS() { } async function doBuildMacOS() { - await doBuild('aarch64-apple-darwin') + const projectRoot = getProjectRoot(); + const arch = 'aarch64-apple-darwin'; + + await doBuild(projectRoot, arch) + await packageForMacos(projectRoot, arch) } async function undraftRelease() { @@ -260,13 +267,40 @@ async function makeRepoChanges(projectRoot: string): Promise<{ releaseNotes: str function packageForLinux(projectRoot: string, arch: string): { filePath: string; fileName: string } { const releaseDirPath = path.join(projectRoot, 'target', arch, 'release'); - const executableFileName = 'gauntlet'; - const archiveFileName = "gauntlet-x86_64-linux.tar.gz" - const archiveFilePath = path.join(releaseDirPath, archiveFileName); + const assetsDirPath = path.join(projectRoot, 'assets', 'linux'); - const tarResult = spawnSync(`tar`, ['-czvf', archiveFileName, executableFileName], { + const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet'); + const sourceDesktopFilePath = path.join(assetsDirPath, 'gauntlet.desktop'); + const sourceServiceFilePath = path.join(assetsDirPath, 'gauntlet.service'); + const sourceLogoFilePath = path.join(assetsDirPath, 'icon_256.png'); + + const bundleDir = path.join(releaseDirPath, 'archive'); + + const targetExecutableFileName = 'gauntlet'; + const targetExecutableFilePath = path.join(bundleDir, targetExecutableFileName); + + const targetDesktopFileName = 'gauntlet.desktop'; + const targetDesktopFilePath = path.join(bundleDir, targetDesktopFileName); + + const targetServiceFileName = 'gauntlet.service'; + const targetServiceFilePath = path.join(bundleDir, targetServiceFileName); + + const targetLogoFileName = 'gauntlet.png'; + const targetLogoFilePath = path.join(bundleDir, targetLogoFileName); + + const archiveFileName = 'gauntlet-x86_64-linux.tar.gz'; + const archiveFilePath = path.join(bundleDir, archiveFileName); + + mkdirSync(bundleDir) + + copyFileSync(sourceExecutableFilePath, targetExecutableFilePath) + copyFileSync(sourceDesktopFilePath, targetDesktopFilePath) + copyFileSync(sourceServiceFilePath, targetServiceFilePath) + copyFileSync(sourceLogoFilePath, targetLogoFilePath) + + const tarResult = spawnSync(`tar`, ['-czvf', archiveFileName, targetExecutableFileName, targetDesktopFileName, targetServiceFileName, targetLogoFileName], { stdio: "inherit", - cwd: releaseDirPath + cwd: bundleDir }) if (tarResult.status !== 0) { @@ -284,9 +318,11 @@ async function packageForMacos(projectRoot: string, arch: string): Promise<{ fil const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet'); const outFileName = "gauntlet-aarch64-macos.dmg" const outFilePath = path.join(releaseDirPath, outFileName); - const sourceInfoFilePath = path.join(projectRoot, 'assets', 'macos', 'Info.plist'); - const sourceIconFilePath = path.join(projectRoot, 'assets', 'macos', 'AppIcon.icns'); - const dmgBackground = path.join(projectRoot, 'assets', 'macos', 'dmg-background.png'); + + const assetsDirPath = path.join(projectRoot, 'assets', 'macos'); + const sourceInfoFilePath = path.join(assetsDirPath, 'Info.plist'); + const sourceIconFilePath = path.join(assetsDirPath, 'AppIcon.icns'); + const dmgBackground = path.join(assetsDirPath, 'dmg-background.png'); const bundleDir = path.join(releaseDirPath, 'Gauntlet.app'); const contentsDir = path.join(bundleDir, 'Contents'); From eae407fab1da9258cdca1e8ca9cad8b2b218f5f8 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 11 Aug 2024 14:28:13 +0200 Subject: [PATCH 016/540] Run build github actions workflow when pushing to any branch instead of just main --- .github/workflows/build.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index dcea4ea..efc2fc6 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,8 +2,6 @@ name: build on: push: - branches: - - main pull_request: branches: - main From e93119eef7117cd5fa5bd930fa78a97c0610b61f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:15:17 +0200 Subject: [PATCH 017/540] Update tools submodule --- tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools b/tools index 0b25471..d85ac74 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit 0b254717e338cb3a93458b6737280df72d4473f2 +Subproject commit d85ac747727320956a20ee5d50505ad7fff61db0 From b5be1769c4ca7b2ec7a057fbea2ae2be361daa6d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:16:17 +0200 Subject: [PATCH 018/540] Build and publish pipeline for windows including installer --- .github/workflows/build.yaml | 5 + .github/workflows/release.yaml | 9 ++ .github/workflows/setup-windows.yaml | 39 +++++++ assets/windows/main.wxs | 32 ++++++ js/build/package.json | 6 +- js/build/src/main.ts | 156 +++++++++++++++++---------- package-lock.json | 11 ++ 7 files changed, 201 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/setup-windows.yaml create mode 100644 assets/windows/main.wxs diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index efc2fc6..71590d1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -16,3 +16,8 @@ jobs: uses: ./.github/workflows/setup-macos.yaml with: command: npm run build-macos-project --workspace @project-gauntlet/build + + build-windows: + uses: ./.github/workflows/setup-windows.yaml + with: + command: npm run build-windows-project --workspace @project-gauntlet/build diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 06eb04e..3873192 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -33,6 +33,14 @@ jobs: command: npm run publish-linux-project --workspace @project-gauntlet/build github-release-id: ${{ needs.publish-init.outputs.github-release-id }} + publish-windows: + needs: + - publish-init + uses: ./.github/workflows/setup-windows.yaml + with: + command: npm run publish-windows-project --workspace @project-gauntlet/build + github-release-id: ${{ needs.publish-init.outputs.github-release-id }} + publish-macos: needs: - publish-init @@ -45,6 +53,7 @@ jobs: needs: - publish-linux - publish-macos + - publish-windows - publish-init uses: ./.github/workflows/setup-linux.yaml with: diff --git a/.github/workflows/setup-windows.yaml b/.github/workflows/setup-windows.yaml new file mode 100644 index 0000000..1f33c1b --- /dev/null +++ b/.github/workflows/setup-windows.yaml @@ -0,0 +1,39 @@ +name: setup windows + +on: + workflow_call: + inputs: + command: + required: true + type: string + github-release-id: + type: string + +jobs: + run-on-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - run: git pull + - uses: actions/setup-node@v4 + with: + node-version: 18 + registry-url: "https://registry.npmjs.org" + scope: '@project-gauntlet' + - uses: dtolnay/rust-toolchain@stable + + - run: cargo install cargo-wix + + - uses: Swatinem/rust-cache@v2 + with: + save-if: ${{ github.ref == 'refs/heads/main' }} + + - run: npm ci + + - run: ${{ inputs.command }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} + GITHUB_RELEASE_ID: ${{ inputs.github-release-id }} diff --git a/assets/windows/main.wxs b/assets/windows/main.wxs new file mode 100644 index 0000000..5df6734 --- /dev/null +++ b/assets/windows/main.wxs @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/build/package.json b/js/build/package.json index 1b1f96c..0828044 100644 --- a/js/build/package.json +++ b/js/build/package.json @@ -4,9 +4,11 @@ "build-this": "tsc --noEmit && rollup --config rollup.config.ts --configPlugin typescript", "build-linux-project": "npm run build-this && node dist/main.js build-linux", "build-macos-project": "npm run build-this && node dist/main.js build-macos", + "build-windows-project": "npm run build-this && node dist/main.js build-windows", "publish-init-project": "npm run build-this && node dist/main.js publish-init", "publish-linux-project": "npm run build-this && node dist/main.js publish-linux", "publish-macos-project": "npm run build-this && node dist/main.js publish-macos", + "publish-windows-project": "npm run build-this && node dist/main.js publish-windows", "publish-final-project": "npm run build-this && node dist/main.js publish-final" }, "type": "module", @@ -14,13 +16,15 @@ "@actions/core": "^1.10.1", "commander": "^11.1.0", "octokit": "^3.1.2", - "simple-git": "^3.22.0" + "simple-git": "^3.22.0", + "cross-spawn": "^7.0.3" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.5", "@types/node": "^18.17.1", + "@types/cross-spawn": "^6.0.6", "tslib": "^2.6.2", "typescript": "^5.3.3" } diff --git a/js/build/src/main.ts b/js/build/src/main.ts index b365ee2..9907d5a 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -3,11 +3,12 @@ import { readFile, writeFile } from "node:fs/promises"; import { simpleGit } from 'simple-git'; import { EOL } from "node:os"; import { Octokit } from 'octokit'; -import { spawnSync } from "node:child_process"; +import { sync as spawnSync } from "cross-spawn"; import path from "node:path"; import { mkdirSync, readFileSync } from "fs"; import { copyFileSync, writeFileSync } from "node:fs"; import * as core from '@actions/core'; +import { SpawnSyncOptions } from "child_process"; const program = new Command(); @@ -30,6 +31,11 @@ program.command('publish-macos') await doPublishMacOS() }); +program.command('publish-windows') + .action(async () => { + await doPublishWindows() + }); + program.command('publish-final') .action(async () => { await doPublishFinal() @@ -45,12 +51,17 @@ program.command('build-macos') await doBuildMacOS() }); +program.command('build-windows') + .action(async () => { + await doBuildWindows() + }); + await program.parseAsync(process.argv); async function doBuild(projectRoot: string, arch: string) { console.log("Building Gauntlet...") - build(projectRoot, true, arch) + build(projectRoot, arch) } async function doPublishInit() { @@ -70,7 +81,7 @@ async function doPublishLinux() { const arch = 'x86_64-unknown-linux-gnu'; - build(projectRoot, false, arch) + build(projectRoot, arch) const { fileName, filePath } = packageForLinux(projectRoot, arch) @@ -90,7 +101,7 @@ async function doPublishMacOS() { const arch = 'aarch64-apple-darwin'; - build(projectRoot, false, arch) + build(projectRoot, arch) const { fileName, filePath } = await packageForMacos(projectRoot, arch) @@ -105,6 +116,26 @@ async function doBuildMacOS() { await packageForMacos(projectRoot, arch) } +async function doPublishWindows() { + const projectRoot = getProjectRoot(); + + const arch = 'x86_64-pc-windows-msvc'; + + build(projectRoot, arch) + + const { fileName, filePath } = await packageForWindows(projectRoot, arch) + + await addFileToRelease(filePath, fileName) +} + +async function doBuildWindows() { + const projectRoot = getProjectRoot(); + const arch = 'x86_64-pc-windows-msvc'; + + // await doBuild(projectRoot, arch) + await packageForWindows(projectRoot, arch) +} + async function undraftRelease() { const octokit = getOctokit(); @@ -132,39 +163,18 @@ async function doPublishFinal() { await undraftRelease() } -function build(projectRoot: string, check: boolean, arch: string) { +function build(projectRoot: string, arch: string) { buildJs(projectRoot) - if (check) { - console.log("Checking rust...") - const cargoCheckResult = spawnSync('cargo', ['check', '--features', 'release', '--target', arch], { - stdio: "inherit", - cwd: projectRoot - }); - - if (cargoCheckResult.status !== 0) { - throw new Error(`Unable to check, status: ${JSON.stringify(cargoCheckResult)}`); - } - } - console.log("Building rust...") - const cargoBuildResult = spawnSync('cargo', ['build', '--release', '--features', 'release', '--target', arch], { - stdio: "inherit", + spawnWithErrors('cargo', ['build', '--release', '--features', 'release', '--target', arch], { cwd: projectRoot }); - - if (cargoBuildResult.status !== 0) { - throw new Error(`Unable to build rust, status: ${JSON.stringify(cargoBuildResult)}`); - } } function buildJs(projectRoot: string) { console.log("Building js...") - const npmRunResult = spawnSync('npm', ['run', 'build'], { stdio: "inherit", cwd: projectRoot }); - - if (npmRunResult.status !== 0) { - throw new Error(`Unable to build js, status: ${JSON.stringify(npmRunResult)}`); - } + spawnWithErrors('npm', ['run', 'build'], { cwd: projectRoot }); } function getProjectRoot(): string { @@ -233,11 +243,7 @@ async function makeRepoChanges(projectRoot: string): Promise<{ releaseNotes: str await writeFile(changelogFilePath, newChangelog.join(EOL)) const bumpNpmPackage = (packageDir: string) => { - const npmVersionResult = spawnSync('npm', ['version', `0.${newVersion}.0`], { stdio: "inherit", cwd: packageDir }) - - if (npmVersionResult.status !== 0) { - throw new Error(`Unable to run npm version, status: ${JSON.stringify(npmVersionResult)}`); - } + spawnWithErrors('npm', ['version', `0.${newVersion}.0`], { cwd: packageDir }) } console.log("Bump version for deno subproject...") @@ -298,15 +304,10 @@ function packageForLinux(projectRoot: string, arch: string): { filePath: string; copyFileSync(sourceServiceFilePath, targetServiceFilePath) copyFileSync(sourceLogoFilePath, targetLogoFilePath) - const tarResult = spawnSync(`tar`, ['-czvf', archiveFileName, targetExecutableFileName, targetDesktopFileName, targetServiceFileName, targetLogoFileName], { - stdio: "inherit", + spawnWithErrors(`tar`, ['-czvf', archiveFileName, targetExecutableFileName, targetDesktopFileName, targetServiceFileName, targetLogoFileName], { cwd: bundleDir }) - if (tarResult.status !== 0) { - throw new Error(`Unable to package for linux, status: ${JSON.stringify(tarResult)}`); - } - return { filePath: archiveFilePath, fileName: archiveFileName @@ -347,7 +348,7 @@ async function packageForMacos(projectRoot: string, arch: string): Promise<{ fil const infoResult = infoSource.replace('__VERSION__', `${version}.0.0`); writeFileSync(targetInfoFilePath, infoResult,'utf8'); - const createDmgResult = spawnSync(`create-dmg`, [ + spawnWithErrors(`create-dmg`, [ '--volname', 'Gauntlet Installer', '--window-size', '660', '400', '--background', dmgBackground, @@ -358,36 +359,64 @@ async function packageForMacos(projectRoot: string, arch: string): Promise<{ fil outFileName, bundleDir ], { - stdio: "inherit", cwd: releaseDirPath }) - if (createDmgResult.status !== 0) { - throw new Error(`Unable to package for macos, status: ${JSON.stringify(createDmgResult)}`); - } - return { filePath: outFilePath, fileName: outFileName } } +async function packageForWindows(projectRoot: string, arch: string): Promise<{ filePath: string; fileName: string }> { + const releaseDirPath = path.join(projectRoot, 'target', arch, 'release'); + const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet.exe'); + const outFileName = "gauntlet-x86_64-windows.msi" + const outFilePath = path.join(releaseDirPath, outFileName); + + const assetsDirPath = path.join(projectRoot, 'assets', 'windows'); + const sourceWxsFilePath = path.join(assetsDirPath, 'main.wxs'); + const iconFilePath = path.join(projectRoot, 'assets', 'linux', 'icon_256.png'); + + const targetWxsFilePath = path.join(releaseDirPath, 'main.wxs'); + const targetIconFilePath = path.join(releaseDirPath, 'icon.ico'); + + const version = await readVersion(projectRoot) + + copyFileSync(sourceWxsFilePath, targetWxsFilePath) + + spawnWithErrors("magick.exe", [iconFilePath, '-define', 'icon:auto-resize=256,128,48,32,16', targetIconFilePath], { + cwd: releaseDirPath + }) + + spawnWithErrors("wix", [ + 'build', + targetWxsFilePath, + '-out', outFilePath, + '-define', `TargetBinaryPath=${sourceExecutableFilePath}`, + '-define', `TargetIconPath=${targetIconFilePath}`, + '-define', `TargetVersion=${version}.0`, + '-ext', "WixToolset.Util.wixext", + '-arch', "x64", + ], { + cwd: releaseDirPath + }) + + return { + filePath: outFilePath, + fileName: outFileName + } +} + + function publishNpmPackage(projectRoot: string) { console.log("Publishing npm deno package...") const denoProjectPath = path.join(projectRoot, "js", "deno"); - const denoNpmPublish = spawnSync('npm', ['publish'], { stdio: "inherit", cwd: denoProjectPath }) - - if (denoNpmPublish.status !== 0) { - throw new Error(`Unable to publish deno package, status: ${JSON.stringify(denoNpmPublish)}`); - } + spawnWithErrors('npm', ['publish'], { cwd: denoProjectPath }) console.log("Publishing npm api package...") const apiProjectPath = path.join(projectRoot, "js", "api"); - const apiNpmPublish = spawnSync('npm', ['publish'], { stdio: "inherit", cwd: apiProjectPath }) - - if (apiNpmPublish.status !== 0) { - throw new Error(`Unable to publish api package, status: ${JSON.stringify(apiNpmPublish)}`); - } + spawnWithErrors('npm', ['publish'], { cwd: apiProjectPath }) } async function createRelease(newVersion: number, releaseNotes: string) { @@ -455,4 +484,19 @@ async function readVersion(projectRoot: string): Promise { async function writeVersion(projectRoot: string, version: number) { const versionFilePath = path.join(projectRoot, "VERSION"); await writeFile(versionFilePath, `${version}`) -} \ No newline at end of file +} + +function spawnWithErrors(command: string, args: string[], options: SpawnSyncOptions) { + console.log(`running ${command} ${args}`) + + const npmRunResult = spawnSync(command, args, { ...options, encoding: "utf-8" }); + + if (npmRunResult.status !== 0) { + throw new Error(`Unable to run ${command} ${args}, status: ${JSON.stringify(npmRunResult, null, 2)}`); + } else { + console.log("stdout: ") + console.log(npmRunResult.stdout) + console.log("stderr: ") + console.log(npmRunResult.stderr) + } +} diff --git a/package-lock.json b/package-lock.json index a1caf89..bc7b4a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,6 +92,7 @@ "dependencies": { "@actions/core": "^1.10.1", "commander": "^11.1.0", + "cross-spawn": "^7.0.3", "octokit": "^3.1.2", "simple-git": "^3.22.0" }, @@ -99,6 +100,7 @@ "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.5", + "@types/cross-spawn": "^6.0.6", "@types/node": "^18.17.1", "tslib": "^2.6.2", "typescript": "^5.3.3" @@ -1481,6 +1483,15 @@ "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==" }, + "node_modules/@types/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", From 101ffe7f1111c421aa8bc9b6013d18870c674d4a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:18:32 +0200 Subject: [PATCH 019/540] Fix console popping up when starting application on windows --- src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.rs b/src/main.rs index 453eb97..525952d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![windows_subsystem = "windows"] + fn main() { cli::init(); } From 69935c8d7b62c1baadbf8888acffb5bfdad65bbb Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:25:06 +0200 Subject: [PATCH 020/540] Fix build on windows --- js/build/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/build/src/main.ts b/js/build/src/main.ts index 9907d5a..c8d24f9 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -132,7 +132,7 @@ async function doBuildWindows() { const projectRoot = getProjectRoot(); const arch = 'x86_64-pc-windows-msvc'; - // await doBuild(projectRoot, arch) + await doBuild(projectRoot, arch) await packageForWindows(projectRoot, arch) } From 8ed6ae2172eabc19739912dc976cc1e00fe45d46 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:56:41 +0200 Subject: [PATCH 021/540] Fix missing protobuf in windows build pipelines --- .github/workflows/setup-windows.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/setup-windows.yaml b/.github/workflows/setup-windows.yaml index 1f33c1b..7987b86 100644 --- a/.github/workflows/setup-windows.yaml +++ b/.github/workflows/setup-windows.yaml @@ -24,7 +24,7 @@ jobs: scope: '@project-gauntlet' - uses: dtolnay/rust-toolchain@stable - - run: cargo install cargo-wix + - run: choco install protoc - uses: Swatinem/rust-cache@v2 with: From 697e2dd0cc5f777cb224fd3fea1c2e7b15fc77f3 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 15 Aug 2024 19:29:23 +0200 Subject: [PATCH 022/540] Fix missing wix and util extension in windows build pipelines --- .github/workflows/setup-windows.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/setup-windows.yaml b/.github/workflows/setup-windows.yaml index 7987b86..acca72c 100644 --- a/.github/workflows/setup-windows.yaml +++ b/.github/workflows/setup-windows.yaml @@ -25,6 +25,8 @@ jobs: - uses: dtolnay/rust-toolchain@stable - run: choco install protoc + - run: dotnet tool install --global wix + - run: wix extension add -g WixToolset.Util.wixext - uses: Swatinem/rust-cache@v2 with: From 4d645a7760400f8fee735d603aab5e5f58063110 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 18 Aug 2024 19:16:13 +0200 Subject: [PATCH 023/540] Add log when running application with server already running --- rust/client/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rust/client/src/lib.rs b/rust/client/src/lib.rs index 7a11339..b8c441a 100644 --- a/rust/client/src/lib.rs +++ b/rust/client/src/lib.rs @@ -25,6 +25,8 @@ pub fn open_window() { match result { Ok(mut backend_api) => { + tracing::info!("Server is already running, opening window..."); + backend_api.show_window() .await .expect("Unknown error") From 9fe4f68010c07e502e03b7e9be1902aaa6018734 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 18 Aug 2024 19:16:27 +0200 Subject: [PATCH 024/540] Apply windows_subsystem only for release build --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 525952d..a6fe15b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -#![windows_subsystem = "windows"] +#![cfg_attr(feature = "release", windows_subsystem = "windows")] fn main() { cli::init(); From cafb29f4b6ad6ec69bda4a072e562b52f1ce5aca Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 18 Aug 2024 19:16:50 +0200 Subject: [PATCH 025/540] Fix `npm run build-dev-plugin` in wrong location in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed5e321..4449a48 100644 --- a/README.md +++ b/README.md @@ -366,6 +366,7 @@ To build dev run: git submodule update --init npm ci npm run build +npm run build-dev-plugin cargo build ``` In dev (without "release" feature) application will use only directories inside project directory to store state or cache. @@ -375,7 +376,6 @@ To build release run: git submodule update --init npm ci npm run build -npm run build-dev-plugin cargo build --release --features release ``` But the new version release needs to be done via GitHub Actions From e8c45b8defd82f7cadecf9fe168758be4ba779e1 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 18 Aug 2024 19:18:38 +0200 Subject: [PATCH 026/540] Run application and open command detached from server process Fixes server being unkillable because of invoked application that is still running --- Cargo.lock | 1 + rust/server/Cargo.toml | 3 ++ rust/server/src/plugins/applications/mod.rs | 5 +- .../src/plugins/js/plugins/applications.rs | 50 ++++++++++++++++--- rust/server/src/plugins/mod.rs | 2 +- 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3c75c3..1583bfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7292,6 +7292,7 @@ dependencies = [ "include_dir", "indexmap 2.2.6", "itertools 0.10.5", + "libc", "numbat", "once_cell", "open", diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 830a15e..92cd34d 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -42,6 +42,9 @@ bytes = "1.6.0" scenario_runner = { path = "../scenario_runner", optional = true } itertools = "0.10.5" +[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] +libc = "0.2.153" + [target.'cfg(target_os = "linux")'.dependencies] freedesktop_entry_parser = "1.3" freedesktop-icons = "0.2" diff --git a/rust/server/src/plugins/applications/mod.rs b/rust/server/src/plugins/applications/mod.rs index 44f3640..bba4251 100644 --- a/rust/server/src/plugins/applications/mod.rs +++ b/rust/server/src/plugins/applications/mod.rs @@ -1,9 +1,8 @@ +use image::ImageFormat; +use image::imageops::FilterType; #[cfg(target_os = "linux")] mod linux; - -use image::ImageFormat; -use image::imageops::FilterType; #[cfg(target_os = "linux")] pub use linux::get_apps; diff --git a/rust/server/src/plugins/js/plugins/applications.rs b/rust/server/src/plugins/js/plugins/applications.rs index a229332..a47de89 100644 --- a/rust/server/src/plugins/js/plugins/applications.rs +++ b/rust/server/src/plugins/js/plugins/applications.rs @@ -1,8 +1,5 @@ -use std::path::Path; - use deno_core::op; use tokio::task::spawn_blocking; - use crate::plugins::applications::{DesktopEntry, get_apps}; #[op] @@ -15,9 +12,50 @@ fn open_application(command: Vec) -> anyhow::Result<()> { let path = &command[0]; let args = &command[1..]; - std::process::Command::new(Path::new(path)) - .args(args) - .spawn()?; + #[cfg(not(windows))] + spawn_detached(path, args)?; Ok(()) } + +#[cfg(not(windows))] +pub fn spawn_detached( + path: &str, + args: I, +) -> std::io::Result<()> +where + I: IntoIterator + Copy, + S: AsRef, +{ + // from https://github.com/alacritty/alacritty/blob/5abb4b73937b17fe501b9ca20b602950f1218b96/alacritty/src/daemon.rs#L65 + use std::os::unix::prelude::CommandExt; + use std::process::{Command, Stdio}; + + let mut command = Command::new(path); + + command + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + unsafe { + command + .pre_exec(|| { + match libc::fork() { + -1 => return Err(std::io::Error::last_os_error()), + 0 => (), + _ => libc::_exit(0), + } + + if libc::setsid() == -1 { + return Err(std::io::Error::last_os_error()); + } + + Ok(()) + }) + .spawn()? + .wait() + .map(|_| ()) + } +} \ No newline at end of file diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 7420617..cc81832 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -416,7 +416,7 @@ impl ApplicationManager { } pub fn handle_open(&self, href: String) { - match open::that(&href) { + match open::that_detached(&href) { Ok(()) => tracing::info!("Opened '{}' successfully.", href), Err(err) => tracing::error!("An error occurred when opening '{}': {}", href, err), } From ec285a4cb6b61f26b70356e9e6f41c0115ca042a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 24 Aug 2024 17:40:00 +0200 Subject: [PATCH 027/540] Add ability to skip parts of publish workflow. Release undraft now needs to be done manually --- .github/workflows/release.yaml | 28 ++++++++++++++++++++++- js/build/src/main.ts | 42 ++++++++++++++-------------------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3873192..d5f0119 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,6 +1,27 @@ name: release -on: workflow_dispatch +on: + workflow_dispatch: + inputs: + provided-github-release-id: + type: string + description: Providing this value will reuse previous version and github release, use only in case of failed previous release + do-npm-publish: + type: boolean + default: true + description: Providing false will skip `npm publish`, use only in case of failed previous release + do-linux-build: + type: boolean + default: true + description: Providing false will skip linux build, use only in case of failed previous release + do-macos-build: + type: boolean + default: true + description: Providing false will skip macos build, use only in case of failed previous release + do-windows-build: + type: boolean + default: true + description: Providing false will skip windows build, use only in case of failed previous release jobs: publish-init: @@ -24,10 +45,12 @@ jobs: id: init-step env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PROVIDED_GITHUB_RELEASE_ID: ${{ inputs.provided-github-release-id }} publish-linux: needs: - publish-init + if: ${{ inputs.do-linux-build }} uses: ./.github/workflows/setup-linux.yaml with: command: npm run publish-linux-project --workspace @project-gauntlet/build @@ -36,6 +59,7 @@ jobs: publish-windows: needs: - publish-init + if: ${{ inputs.do-windows-build }} uses: ./.github/workflows/setup-windows.yaml with: command: npm run publish-windows-project --workspace @project-gauntlet/build @@ -44,6 +68,7 @@ jobs: publish-macos: needs: - publish-init + if: ${{ inputs.do-macos-build }} uses: ./.github/workflows/setup-macos.yaml with: command: npm run publish-macos-project --workspace @project-gauntlet/build @@ -55,6 +80,7 @@ jobs: - publish-macos - publish-windows - publish-init + if: ${{ inputs.do-npm-publish }} uses: ./.github/workflows/setup-linux.yaml with: command: npm run publish-final-project --workspace @project-gauntlet/build diff --git a/js/build/src/main.ts b/js/build/src/main.ts index c8d24f9..12beaae 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -69,9 +69,19 @@ async function doPublishInit() { const projectRoot = getProjectRoot() - const { newVersion, releaseNotes } = await makeRepoChanges(projectRoot); + const githubReleaseId = process.env.PROVIDED_GITHUB_RELEASE_ID; - await createRelease(newVersion, releaseNotes) + if (githubReleaseId) { + core.setOutput("github-release-id", `${githubReleaseId}`) + } else { + const { newVersion, releaseNotes } = await makeRepoChanges(projectRoot); + + const releaseId = await createRelease(newVersion, releaseNotes); + + console.log(`GitHub release id: ${releaseId}`) + + core.setOutput("github-release-id", `${releaseId}`) + } } async function doPublishLinux() { @@ -136,31 +146,13 @@ async function doBuildWindows() { await packageForWindows(projectRoot, arch) } -async function undraftRelease() { - const octokit = getOctokit(); - - const response = await octokit.rest.repos.getRelease({ - ...getGithubRepo(), - release_id: getGithubReleaseId(), - }); - - await octokit.rest.repos.updateRelease({ - ...getGithubRepo(), - release_id: response.data.id, - origin: response.data.upload_url, - draft: false - }); -} - async function doPublishFinal() { - console.log("Publishing Gauntlet... Finishing up...") + console.log("Publishing Gauntlet npm packages...") const projectRoot = getProjectRoot() buildJs(projectRoot) publishNpmPackage(projectRoot) - - await undraftRelease() } function build(projectRoot: string, arch: string) { @@ -257,7 +249,7 @@ async function makeRepoChanges(projectRoot: string): Promise<{ releaseNotes: str console.log("git add all files...") await git.raw('add', '-A') console.log("git commit...") - await git.commit(`Release v${newVersion}`); + await git.commit(`Prepare for v${newVersion} release`); console.log("git add version tag...") await git.addTag(`v${newVersion}`) console.log("git push...") @@ -419,7 +411,7 @@ function publishNpmPackage(projectRoot: string) { spawnWithErrors('npm', ['publish'], { cwd: apiProjectPath }) } -async function createRelease(newVersion: number, releaseNotes: string) { +async function createRelease(newVersion: number, releaseNotes: string): Promise { const octokit = getOctokit(); console.log("Creating github release...") @@ -430,10 +422,10 @@ async function createRelease(newVersion: number, releaseNotes: string) { target_commitish: 'main', name: `v${newVersion}`, body: releaseNotes, - draft: true + draft: true // release needs to be undrafted manually after each release }); - core.setOutput("github-release-id", `${response.data.id}`) + return response.data.id; } async function addFileToRelease(filePath: string, fileName: string) { From c86d11d8850ee9f0ac037e8bb029320a33b34fc0 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 25 Aug 2024 10:25:25 +0200 Subject: [PATCH 028/540] Fix generate-sample-theme and generate-sample-color-theme failing if config folder doesn't exist --- rust/client/src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rust/client/src/lib.rs b/rust/client/src/lib.rs index b8c441a..c5f812c 100644 --- a/rust/client/src/lib.rs +++ b/rust/client/src/lib.rs @@ -48,6 +48,12 @@ pub fn generate_theme_sample() -> anyhow::Result<()> { let string = serde_json::to_string_pretty(&theme)?; + let sample_theme_parent = sample_theme_file + .parent() + .expect("no parent?"); + + std::fs::create_dir_all(sample_theme_parent)?; + std::fs::write(&sample_theme_file, string)?; println!("Created sample using default theme at {:?}", sample_theme_file); @@ -66,6 +72,12 @@ pub fn generate_color_theme_sample() -> anyhow::Result<()> { let string = serde_json::to_string_pretty(&theme)?; + let sample_theme_parent = sample_theme_color_file + .parent() + .expect("no parent?"); + + std::fs::create_dir_all(sample_theme_parent)?; + std::fs::write(&sample_theme_color_file, string)?; println!("Created sample using default color theme at {:?}", sample_theme_color_file); From ffbebd437cffebbafa07ae5d1f252b64a2209d06 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 25 Aug 2024 11:17:38 +0200 Subject: [PATCH 029/540] Sign, notarize and staple macos dmg --- .github/workflows/release.yaml | 1 + .github/workflows/setup-macos.yaml | 4 ++ assets/macos/entitlements.plist | 6 +++ js/build/src/main.ts | 62 ++++++++++++++++++++++++++++-- 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 assets/macos/entitlements.plist diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d5f0119..20de2e8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -73,6 +73,7 @@ jobs: with: command: npm run publish-macos-project --workspace @project-gauntlet/build github-release-id: ${{ needs.publish-init.outputs.github-release-id }} + secrets: inherit publish-final: needs: diff --git a/.github/workflows/setup-macos.yaml b/.github/workflows/setup-macos.yaml index cb8f299..86896af 100644 --- a/.github/workflows/setup-macos.yaml +++ b/.github/workflows/setup-macos.yaml @@ -36,6 +36,7 @@ jobs: - run: brew install protobuf - run: brew install create-dmg + - run: cargo install apple-codesign - uses: Swatinem/rust-cache@v2 with: @@ -48,3 +49,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} GITHUB_RELEASE_ID: ${{ inputs.github-release-id }} + APPLE_SIGNING_KEY_PEM: ${{ secrets.APPLE_SIGNING_KEY_PEM && fromJson(secrets.APPLE_SIGNING_KEY_PEM).content }} + APPLE_SIGNING_CERT_PEM: ${{ secrets.APPLE_SIGNING_CERT_PEM && fromJson(secrets.APPLE_SIGNING_CERT_PEM).content }} + APP_STORE_CONNECT_KEY: ${{ secrets.APP_STORE_CONNECT_KEY }} diff --git a/assets/macos/entitlements.plist b/assets/macos/entitlements.plist new file mode 100644 index 0000000..8c00664 --- /dev/null +++ b/assets/macos/entitlements.plist @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/js/build/src/main.ts b/js/build/src/main.ts index 12beaae..3126603 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -113,7 +113,7 @@ async function doPublishMacOS() { build(projectRoot, arch) - const { fileName, filePath } = await packageForMacos(projectRoot, arch) + const { fileName, filePath } = await packageForMacos(projectRoot, arch, true, true) await addFileToRelease(filePath, fileName) } @@ -123,7 +123,7 @@ async function doBuildMacOS() { const arch = 'aarch64-apple-darwin'; await doBuild(projectRoot, arch) - await packageForMacos(projectRoot, arch) + await packageForMacos(projectRoot, arch, true, false) } async function doPublishWindows() { @@ -306,7 +306,7 @@ function packageForLinux(projectRoot: string, arch: string): { filePath: string; } } -async function packageForMacos(projectRoot: string, arch: string): Promise<{ filePath: string; fileName: string }> { +async function packageForMacos(projectRoot: string, arch: string, sign: boolean, notarize: boolean): Promise<{ filePath: string; fileName: string }> { const releaseDirPath = path.join(projectRoot, 'target', arch, 'release'); const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet'); const outFileName = "gauntlet-aarch64-macos.dmg" @@ -316,6 +316,7 @@ async function packageForMacos(projectRoot: string, arch: string): Promise<{ fil const sourceInfoFilePath = path.join(assetsDirPath, 'Info.plist'); const sourceIconFilePath = path.join(assetsDirPath, 'AppIcon.icns'); const dmgBackground = path.join(assetsDirPath, 'dmg-background.png'); + const entitlementsPath = path.join(assetsDirPath, 'entitlements.plist'); const bundleDir = path.join(releaseDirPath, 'Gauntlet.app'); const contentsDir = path.join(bundleDir, 'Contents'); @@ -340,6 +341,33 @@ async function packageForMacos(projectRoot: string, arch: string): Promise<{ fil const infoResult = infoSource.replace('__VERSION__', `${version}.0.0`); writeFileSync(targetInfoFilePath, infoResult,'utf8'); + const signKeyPath = path.join(releaseDirPath, 'signKey.pem'); + const signCertPath = path.join(releaseDirPath, 'signCert.pem'); + const connectApiKeyPath = path.join(releaseDirPath, 'connectApiKey.json'); + + const signKeyContent = process.env.APPLE_SIGNING_KEY_PEM; + const signCertContent = process.env.APPLE_SIGNING_CERT_PEM; + const connectApiKeyContent = process.env.APP_STORE_CONNECT_KEY; + + if (sign) { + writeFileSync(signKeyPath, signKeyContent!!); + writeFileSync(signCertPath, signCertContent!!); + + spawnWithErrors(`rcodesign`, [ + 'sign', + '--pem-file', + signKeyPath, + '--pem-file', + signCertPath, + '--for-notarization', + '--entitlements-xml-file', + entitlementsPath, + bundleDir + ], { + cwd: releaseDirPath + }) + } + spawnWithErrors(`create-dmg`, [ '--volname', 'Gauntlet Installer', '--window-size', '660', '400', @@ -354,6 +382,34 @@ async function packageForMacos(projectRoot: string, arch: string): Promise<{ fil cwd: releaseDirPath }) + if (sign) { + spawnWithErrors(`rcodesign`, [ + 'sign', + '--pem-file', + signKeyPath, + '--pem-file', + signCertPath, + '--for-notarization', + outFilePath + ], { + cwd: releaseDirPath + }) + } + + if (notarize) { + writeFileSync(connectApiKeyPath, connectApiKeyContent!!); + + spawnWithErrors(`rcodesign`, [ + 'notary-submit', + '--api-key-file', + connectApiKeyPath, + '--staple', + outFilePath + ], { + cwd: releaseDirPath + }) + } + return { filePath: outFilePath, fileName: outFileName From feb61ec691adec58df697ae75145b58ce58e5e31 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 25 Aug 2024 11:38:59 +0200 Subject: [PATCH 030/540] Pass secrets to build macos workflow for signing --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 71590d1..e6e4dc1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -16,6 +16,7 @@ jobs: uses: ./.github/workflows/setup-macos.yaml with: command: npm run build-macos-project --workspace @project-gauntlet/build + secrets: inherit build-windows: uses: ./.github/workflows/setup-windows.yaml From 4ca19c95cd0c6c5a6c240057b9be670762afe52d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 25 Aug 2024 14:16:43 +0200 Subject: [PATCH 031/540] Add proper entitlements for macos app --- assets/macos/entitlements.plist | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/assets/macos/entitlements.plist b/assets/macos/entitlements.plist index 8c00664..1fd5a09 100644 --- a/assets/macos/entitlements.plist +++ b/assets/macos/entitlements.plist @@ -2,5 +2,12 @@ + + com.apple.security.cs.disable-library-validation + + + + com.apple.security.cs.allow-jit + \ No newline at end of file From fc476dfe50fa62ad6db9bbfc492b52f46c7abc70 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 25 Aug 2024 18:25:13 +0200 Subject: [PATCH 032/540] Add system tray on macos and windows --- Cargo.lock | 550 +++++++++++++++++++++++--- rust/client/Cargo.toml | 3 + rust/client/src/lib.rs | 21 + rust/client/src/ui/mod.rs | 6 + rust/client/src/ui/sys_tray.rs | 64 +++ rust/common/src/rpc/backend_api.rs | 10 +- rust/common/src/rpc/backend_server.rs | 12 +- rust/server/src/plugins/mod.rs | 3 +- rust/server/src/rpc.rs | 6 + schema/backend.proto | 6 + 10 files changed, 629 insertions(+), 52 deletions(-) create mode 100644 rust/client/src/ui/sys_tray.rs diff --git a/Cargo.lock b/Cargo.lock index 1583bfc..ecbcb53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -423,6 +423,29 @@ dependencies = [ "syn 2.0.59", ] +[[package]] +name = "atk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atoi" version = "2.0.0" @@ -818,7 +841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5952f0958672e4aa8fc706d01905c56af57759e078c53a6fddf4a13361943e7a" dependencies = [ "block", - "core-foundation", + "core-foundation 0.9.4", "core-graphics 0.22.3", "dispatch", "lazy_static", @@ -835,6 +858,31 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bf2a5fb3207c12b5d208ebc145f967fea5cac41a021c37417ccc31ba40f39ee" +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.5.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "calloop" version = "0.12.4" @@ -1023,6 +1071,7 @@ dependencies = [ "tonic", "tracing", "tracing-subscriber", + "tray-icon", "utils", ] @@ -1082,14 +1131,30 @@ checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" dependencies = [ "bitflags 1.3.2", "block", - "cocoa-foundation", - "core-foundation", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", "core-graphics 0.23.2", "foreign-types 0.5.0", "libc", "objc", ] +[[package]] +name = "cocoa" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" +dependencies = [ + "bitflags 2.5.0", + "block", + "cocoa-foundation 0.2.0", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types 0.5.0", + "libc", + "objc", +] + [[package]] name = "cocoa-foundation" version = "0.1.2" @@ -1098,8 +1163,22 @@ checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ "bitflags 1.3.2", "block", - "core-foundation", - "core-graphics-types", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" +dependencies = [ + "bitflags 2.5.0", + "block", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", "libc", "objc", ] @@ -1276,10 +1355,20 @@ dependencies = [ ] [[package]] -name = "core-foundation-sys" -version = "0.8.6" +name = "core-foundation" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" @@ -1288,8 +1377,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", "foreign-types 0.3.2", "libc", ] @@ -1301,8 +1390,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.5.0", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", "foreign-types 0.5.0", "libc", ] @@ -1314,7 +1416,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.5.0", + "core-foundation 0.10.0", "libc", ] @@ -2380,6 +2493,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" + [[package]] name = "dprint-swc-ext" version = "0.11.1" @@ -2799,6 +2918,16 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f" +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset 0.9.1", + "rustc_version 0.4.0", +] + [[package]] name = "filetime" version = "0.2.23" @@ -3147,6 +3276,64 @@ dependencies = [ "cli", ] +[[package]] +name = "gdk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + [[package]] name = "generator" version = "0.7.5" @@ -3238,6 +3425,38 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + [[package]] name = "git2" version = "0.18.3" @@ -3324,6 +3543,53 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.5.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2 1.0.80", + "quote 1.0.36", + "syn 2.0.59", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "global-hotkey" version = "0.4.2" @@ -3371,6 +3637,17 @@ dependencies = [ "wgpu", ] +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "gpu-alloc" version = "0.6.0" @@ -3445,6 +3722,58 @@ dependencies = [ "subtle", ] +[[package]] +name = "gtk" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2 1.0.80", + "quote 1.0.36", + "syn 2.0.59", +] + [[package]] name = "guillotiere" version = "0.6.2" @@ -4453,6 +4782,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + [[package]] name = "libc" version = "0.2.153" @@ -4520,7 +4873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.48.5", ] [[package]] @@ -4853,6 +5206,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.27.0" @@ -4861,7 +5223,7 @@ checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" dependencies = [ "bitflags 2.5.0", "block", - "core-graphics-types", + "core-graphics-types 0.1.3", "foreign-types 0.5.0", "log", "objc", @@ -4917,6 +5279,24 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4519a88847ba2d5ead3dc53f1060ec6a571de93f325d9c5c4968147382b1cbc3" +[[package]] +name = "muda" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" +dependencies = [ + "cocoa 0.26.0", + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc", + "once_cell", + "png 0.17.13", + "thiserror", + "windows-sys 0.59.0", +] + [[package]] name = "multimap" version = "0.8.3" @@ -5654,6 +6034,31 @@ dependencies = [ "syn 2.0.59", ] +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -6103,6 +6508,15 @@ dependencies = [ "toml_edit 0.19.15", ] +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", +] + [[package]] name = "proc-macro-crate" version = "3.1.0" @@ -7138,7 +7552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", @@ -7603,7 +8017,7 @@ dependencies = [ "as-raw-xcb-connection", "bytemuck", "cfg_aliases 0.2.0", - "cocoa", + "cocoa 0.25.0", "core-graphics 0.23.2", "drm", "fastrand", @@ -8468,7 +8882,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -8958,6 +9372,17 @@ dependencies = [ "winnow 0.5.40", ] +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.2.6", + "toml_datetime", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.21.1" @@ -9116,6 +9541,26 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tray-icon" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b92252d649d771105448969f2b2dda4342ba48b77731b60d37c93665e26615b" +dependencies = [ + "core-graphics 0.24.0", + "crossbeam-channel", + "dirs 5.0.1", + "libappindicator", + "muda", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "png 0.17.13", + "thiserror", + "windows-sys 0.59.0", +] + [[package]] name = "triomphe" version = "0.1.11" @@ -9950,7 +10395,7 @@ dependencies = [ "bitflags 2.5.0", "block", "cfg_aliases 0.1.1", - "core-graphics-types", + "core-graphics-types 0.1.3", "d3d12", "glow", "glutin_wgl_sys", @@ -10083,7 +10528,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10092,7 +10537,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10119,7 +10564,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -10154,18 +10608,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -10182,9 +10636,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -10200,9 +10654,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -10218,15 +10672,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -10242,9 +10696,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -10260,9 +10714,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -10278,9 +10732,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -10296,9 +10750,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" @@ -10313,7 +10767,7 @@ dependencies = [ "bytemuck", "calloop", "cfg_aliases 0.1.1", - "core-foundation", + "core-foundation 0.9.4", "core-graphics 0.23.2", "cursor-icon", "icrate", diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index 210467c..c41ccc9 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -22,6 +22,9 @@ serde_json = "1.0" once_cell = "1.19" bytes = "1.6.0" +[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] +tray-icon = { version = "0.15.1", default-features = false } + [target.'cfg(target_os = "linux")'.dependencies] iced.workspace = true iced.features = ["wayland"] diff --git a/rust/client/src/lib.rs b/rust/client/src/lib.rs index c5f812c..209b5ff 100644 --- a/rust/client/src/lib.rs +++ b/rust/client/src/lib.rs @@ -38,6 +38,27 @@ pub fn open_window() { }) } +pub fn open_settings_window() { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("unable to start server tokio runtime") + .block_on(async { + let result = BackendApi::new().await; + + match result { + Ok(mut backend_api) => { + backend_api.show_settings_window() + .await + .expect("Unknown error") + } + Err(_) => { + tracing::error!("Unable to connect to server. Please check if you have Gauntlet running on your PC") + } + } + }) +} + pub fn generate_theme_sample() -> anyhow::Result<()> { let dirs = Dirs::new(); diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index aed1b54..1129836 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -47,6 +47,8 @@ mod theme; mod client_context; mod widget_container; mod inline_view_container; +#[cfg(any(target_os = "macos", target_os = "windows"))] +mod sys_tray; pub use theme::GauntletTheme; @@ -59,6 +61,8 @@ pub struct AppModel { focused: bool, theme: GauntletTheme, wayland: bool, + #[cfg(any(target_os = "macos", target_os = "windows"))] + tray_icon: tray_icon::TrayIcon, // ephemeral state prompt: String, @@ -370,6 +374,8 @@ impl Application for AppModel { focused: false, theme: GauntletTheme::new(), wayland, + #[cfg(any(target_os = "macos", target_os = "windows"))] + tray_icon: sys_tray::create_tray(), // ephemeral state prompt: "".to_string(), diff --git a/rust/client/src/ui/sys_tray.rs b/rust/client/src/ui/sys_tray.rs new file mode 100644 index 0000000..c4bcd54 --- /dev/null +++ b/rust/client/src/ui/sys_tray.rs @@ -0,0 +1,64 @@ +use image::ImageFormat; + +pub fn create_tray() -> tray_icon::TrayIcon { + use tray_icon::TrayIconBuilder; + use tray_icon::menu::{MenuEvent, Menu, MenuItem, PredefinedMenuItem, AboutMetadataBuilder}; + + MenuEvent::set_event_handler(Some(|event: MenuEvent| { + match event.id().as_ref() { + "GAUNTLET_OPEN_MAIN_WINDOW" => { + crate::open_window() + } + "GAUNTLET_OPEN_SETTING_WINDOW" => { + crate::open_settings_window() + } + _ => {} + } + })); + + let (tray_icon, muda_icon) = { + let bytes = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/linux/icon_256.png")); + + let image = image::load_from_memory_with_format(bytes, ImageFormat::Png) + .expect("Failed to open icon path") + .into_rgba8(); + + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + + let tray_icon = tray_icon::Icon::from_rgba(rgba.clone(), width, height) + .expect("Failed to open icon"); + + let muda_icon = tray_icon::menu::Icon::from_rgba(rgba, width, height) + .expect("Failed to open icon"); + + (tray_icon, muda_icon) + }; + + let about_metadata = AboutMetadataBuilder::new() + .name(Some("Gauntlet")) + .version(Some(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../VERSION")))) + .authors(Some(vec!["Exidex".to_string()])) + .credits(Some("Exidex".to_string())) + .license(Some("MPL-2.0")) + .website(Some("https://github.com/project-gauntlet/gauntlet")) + .icon(Some(muda_icon)) + .build(); + + let menu = Menu::with_items( + &[ + &MenuItem::new("Gauntlet", false, None), + &MenuItem::with_id("GAUNTLET_OPEN_MAIN_WINDOW", "Open", true, None), + &MenuItem::with_id("GAUNTLET_OPEN_SETTING_WINDOW", "Open Settings", true, None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::about(Some("About..."), Some(about_metadata)), + &PredefinedMenuItem::quit(Some("Quit Gauntlet")), + ] + ).expect("unable to create tray menu"); + + TrayIconBuilder::new() + .with_menu(Box::new(menu)) + .with_icon(tray_icon) + .build() + .expect("unable to create tray") +} \ No newline at end of file diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index 952b941..c448d82 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -7,7 +7,7 @@ use tonic::transport::Channel; use utils::channel::{RequestError, RequestSender}; use crate::model::{BackendRequestData, BackendResponseData, DownloadStatus, EntrypointId, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, UiPropertyValue, UiWidgetId}; -use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadStatus, RpcDownloadStatusRequest, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcPingRequest, RpcPluginsRequest, RpcRemovePluginRequest, RpcSaveLocalPluginRequest, RpcSetEntrypointStateRequest, RpcSetGlobalShortcutRequest, RpcSetPluginStateRequest, RpcSetPreferenceValueRequest, RpcShowWindowRequest}; +use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadStatus, RpcDownloadStatusRequest, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcPingRequest, RpcPluginsRequest, RpcRemovePluginRequest, RpcSaveLocalPluginRequest, RpcSetEntrypointStateRequest, RpcSetGlobalShortcutRequest, RpcSetPluginStateRequest, RpcSetPreferenceValueRequest, RpcShowSettingsWindowRequest, RpcShowWindowRequest}; use crate::rpc::grpc::rpc_backend_client::RpcBackendClient; use crate::rpc::grpc_convert::{plugin_preference_from_rpc, plugin_preference_user_data_from_rpc, plugin_preference_user_data_to_rpc}; @@ -242,6 +242,14 @@ impl BackendApi { Ok(()) } + + pub async fn show_settings_window(&mut self) -> Result<(), BackendApiError> { + let _ = self.client.show_settings_window(Request::new(RpcShowSettingsWindowRequest::default())) + .await?; + + Ok(()) + } + pub async fn plugins(&mut self) -> Result, BackendApiError> { let plugins = self.client.plugins(Request::new(RpcPluginsRequest::default())) .await? diff --git a/rust/common/src/rpc/backend_server.rs b/rust/common/src/rpc/backend_server.rs index d1600db..747bae5 100644 --- a/rust/common/src/rpc/backend_server.rs +++ b/rust/common/src/rpc/backend_server.rs @@ -7,7 +7,7 @@ use tonic::{Request, Response, Status}; use tonic::transport::Server; use crate::model::{DownloadStatus, EntrypointId, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SettingsEntrypointType, SettingsPlugin}; -use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadPluginResponse, RpcDownloadStatus, RpcDownloadStatusRequest, RpcDownloadStatusResponse, RpcDownloadStatusValue, RpcEntrypoint, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetGlobalShortcutResponse, RpcPingRequest, RpcPingResponse, RpcPlugin, RpcPluginsRequest, RpcPluginsResponse, RpcRemovePluginRequest, RpcRemovePluginResponse, RpcSaveLocalPluginRequest, RpcSaveLocalPluginResponse, RpcSetEntrypointStateRequest, RpcSetEntrypointStateResponse, RpcSetGlobalShortcutRequest, RpcSetGlobalShortcutResponse, RpcSetPluginStateRequest, RpcSetPluginStateResponse, RpcSetPreferenceValueRequest, RpcSetPreferenceValueResponse, RpcShowWindowRequest, RpcShowWindowResponse}; +use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadPluginResponse, RpcDownloadStatus, RpcDownloadStatusRequest, RpcDownloadStatusResponse, RpcDownloadStatusValue, RpcEntrypoint, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetGlobalShortcutResponse, RpcPingRequest, RpcPingResponse, RpcPlugin, RpcPluginsRequest, RpcPluginsResponse, RpcRemovePluginRequest, RpcRemovePluginResponse, RpcSaveLocalPluginRequest, RpcSaveLocalPluginResponse, RpcSetEntrypointStateRequest, RpcSetEntrypointStateResponse, RpcSetGlobalShortcutRequest, RpcSetGlobalShortcutResponse, RpcSetPluginStateRequest, RpcSetPluginStateResponse, RpcSetPreferenceValueRequest, RpcSetPreferenceValueResponse, RpcShowSettingsWindowRequest, RpcShowSettingsWindowResponse, RpcShowWindowRequest, RpcShowWindowResponse}; use crate::rpc::grpc::rpc_backend_server::{RpcBackend, RpcBackendServer}; use crate::rpc::grpc_convert::{plugin_preference_to_rpc, plugin_preference_user_data_from_rpc, plugin_preference_user_data_to_rpc}; @@ -49,6 +49,8 @@ impl RpcBackendServerImpl { pub trait BackendServer { async fn show_window(&self) -> anyhow::Result<()>; + async fn show_settings_window(&self) -> anyhow::Result<()>; + async fn plugins(&self) -> anyhow::Result>; async fn set_plugin_state( @@ -105,6 +107,14 @@ impl RpcBackend for RpcBackendServerImpl { Ok(Response::new(RpcShowWindowResponse::default())) } + async fn show_settings_window(&self, _request: Request) -> Result, Status> { + self.server.show_settings_window() + .await + .map_err(|err| Status::internal(format!("{:#}", err)))?; + + Ok(Response::new(RpcShowSettingsWindowResponse::default())) + } + async fn plugins(&self, _: Request) -> Result, Status> { let plugins = self.server.plugins() .await diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index cc81832..be09932 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -422,7 +422,7 @@ impl ApplicationManager { } } - pub fn handle_open_settings_window(&self){ + pub fn handle_open_settings_window(&self) { let current_exe = std::env::current_exe() .expect("unable to get current_exe"); @@ -430,7 +430,6 @@ impl ApplicationManager { .args(["settings"]) .spawn() .expect("failed to execute settings process"); - } pub fn handle_open_settings_window_preferences(&self, plugin_id: PluginId, entrypoint_id: Option) { diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index 15f4dcf..060b51c 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -28,6 +28,12 @@ impl BackendServer for BackendServerImpl { self.application_manager.show_window().await } + async fn show_settings_window(&self) -> anyhow::Result<()> { + self.application_manager.handle_open_settings_window(); + + Ok(()) + } + async fn plugins(&self) -> anyhow::Result> { let result = self.application_manager.plugins() .await; diff --git a/schema/backend.proto b/schema/backend.proto index 60de86d..7418b77 100644 --- a/schema/backend.proto +++ b/schema/backend.proto @@ -8,6 +8,7 @@ service RpcBackend { // cli rpc ShowWindow (RpcShowWindowRequest) returns (RpcShowWindowResponse); + rpc ShowSettingsWindow (RpcShowSettingsWindowRequest) returns (RpcShowSettingsWindowResponse); // settings rpc Plugins (RpcPluginsRequest) returns (RpcPluginsResponse); @@ -36,6 +37,11 @@ message RpcShowWindowRequest { message RpcShowWindowResponse { } +message RpcShowSettingsWindowRequest { +} +message RpcShowSettingsWindowResponse { +} + message RpcPingRequest { } message RpcPingResponse { From 74922b01ca79a85f8aa6f249cc663b2d32cc8c95 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 27 Aug 2024 21:30:25 +0200 Subject: [PATCH 033/540] Hide action panel and settings download info popups when clicking on backdrop --- rust/client/src/ui/widget.rs | 10 +++++++++- rust/management_client/src/ui.rs | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index e7c2016..47f0679 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -7,7 +7,7 @@ use anyhow::anyhow; use iced::{Alignment, Font, Length}; use iced::alignment::Horizontal; use iced::font::Weight; -use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, pick_list, row, scrollable, Space, text, text_input, tooltip, vertical_rule}; +use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, pick_list, row, scrollable, Space, text, text_input, tooltip, vertical_rule, mouse_area}; use iced::widget::image::Handle; use iced::widget::tooltip::Position; use iced_aw::{floating_element, GridRow}; @@ -1333,6 +1333,14 @@ fn render_root<'a>( let content: Element<_> = column(vec![top_panel, top_separator, content, bottom_panel]) .into(); + let content = if hide_action_panel { + content + } else { + mouse_area(content) + .on_press(ComponentWidgetEvent::ToggleActionPanel { widget_id }) + .into() + }; + floating_element(content, action_panel_element) .offset(Offset::from([8.0, 40.0])) // TODO calculate based on theme .hide(hide_action_panel) diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index 5e9070f..a2641a8 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -3,7 +3,7 @@ use std::time::Duration; use iced::{Alignment, alignment, Application, color, Command, executor, font, futures, Length, Padding, Settings, Size, Subscription, time, window}; use iced::advanced::Widget; -use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text}; +use iced::widget::{button, column, container, horizontal_rule, horizontal_space, mouse_area, row, scrollable, text}; use iced_aw::{floating_element, Spinner}; use iced_aw::core::icons; use iced_aw::floating_element::{Anchor, Offset}; @@ -736,6 +736,14 @@ impl Application for ManagementAppModel { .into() }; + let content = if !self.download_info_shown { + content + } else { + mouse_area(content) + .on_press(ManagementAppMsg::ToggleDownloadInfo) + .into() + }; + floating_element(content, download_info_panel) .offset(Offset::from([8.0, 60.0])) .anchor(Anchor::NorthEast) From 46fd4bd15a52fc3c1d9e302c80bdd9ccf0d8cb84 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 29 Aug 2024 19:23:12 +0200 Subject: [PATCH 034/540] Update CHANGELOG.md --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3645c80..3598a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,29 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +### Big things +- Bundling improvements + - `.dmg` file is now signed, notarized and stapled, removing the need for manual manipulations on `.dmg` file to be able to install Gauntlet + - `.msi` installer for Windows is now available + - `.tar.gz` archive for Linux now contains default `systemd` service, `.desktop` and `.png` icon file +- Added system tray icon on Windows and macOS with ability to open main or settings window, see version or quit Gauntlet +- Local plugins now always use development React bundle for better error messages at the expense of performance. + +### General fixes +- Fix being unable to stop plugin in various situations including when there is unresolved pending promise +- Fix `generate-sample-theme` and `generate-sample-color-theme` CLI commands failing if config folder doesn't exist +- Fix console window popping up when starting Gauntlet on Windows + +### `Applications` plugin +- On macOS and Linux, applications are now started detached from main process, fixing situations when other applications blocked Gauntlet from exiting + +### UI Improvements +- Action panel and download information panel in settings are now closed when clicking outside of panel on background + +### API changes +- Type of `GeneratedCommand`'s `icon` field changed from `icon: ArrayBuffer | undefined` to `icon?: ArrayBuffer` +- Default function returned from `command` and `command-generator` entrypoints can now be `async` + ## [6] - 2024-08-04 ### Big things From 0bce0a528fe236ced7bcddd913cc9c9f9e4c4204 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:50:17 +0000 Subject: [PATCH 035/540] Prepare for v7 release --- CHANGELOG.md | 2 ++ VERSION | 2 +- js/api/package.json | 2 +- js/deno/package.json | 2 +- package-lock.json | 4 ++-- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3598a80..22c4a6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +## [7] - 2024-08-30 + ### Big things - Bundling improvements - `.dmg` file is now signed, notarized and stapled, removing the need for manual manipulations on `.dmg` file to be able to install Gauntlet diff --git a/VERSION b/VERSION index 62f9457..c793025 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6 \ No newline at end of file +7 \ No newline at end of file diff --git a/js/api/package.json b/js/api/package.json index 0eb379e..effd701 100644 --- a/js/api/package.json +++ b/js/api/package.json @@ -1,6 +1,6 @@ { "name": "@project-gauntlet/api", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "exports": { "./components": { diff --git a/js/deno/package.json b/js/deno/package.json index 947f143..84f0535 100644 --- a/js/deno/package.json +++ b/js/deno/package.json @@ -1,6 +1,6 @@ { "name": "@project-gauntlet/deno", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "exports": { ".": { diff --git a/package-lock.json b/package-lock.json index bc7b4a8..06ffb80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ }, "js/api": { "name": "@project-gauntlet/api", - "version": "0.6.0", + "version": "0.7.0", "devDependencies": { "@project-gauntlet/typings": "*", "typescript": "^5.3.3" @@ -123,7 +123,7 @@ }, "js/deno": { "name": "@project-gauntlet/deno", - "version": "0.6.0", + "version": "0.7.0", "devDependencies": { "@types/node": "^18.17.1", "typescript": "^5.3.3" From a63e56dea056384348cc6e1a8af9411c00de6e34 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:38:53 +0200 Subject: [PATCH 036/540] Update README.md --- README.md | 88 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 4449a48..ed05f50 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,13 @@ [![Discord](https://discord.com/api/guilds/1205606511603359785/widget.png?style=shield)](https://discord.gg/gFTqYUkBrW) - + Web-first cross-platform application launcher with React-based plugins. > [!NOTE] > Launcher is in active development, expect bugs, missing features, incomplete ux, etc. > -> At the moment, it may not yet be ready for daily usage. -> > There will probably be breaking changes which will be documented in [changelog](CHANGELOG.md). https://github.com/user-attachments/assets/1aa790dc-fce9-4ac5-97d8-3b81b83acf2e @@ -69,13 +67,8 @@ https://github.com/user-attachments/assets/1aa790dc-fce9-4ac5-97d8-3b81b83acf2e - Linux - Both X11 and Wayland (via LayerShell protocol) are supported - macOS - -###### Planned - - Windows - - already works - - publish process is not yet implemented, so application needs to be build manually - - built-in "Applications" plugin is not yet implemented + - built-in "Applications" plugin is not yet implemented. See [#9](https://github.com/project-gauntlet/gauntlet/issues/9) ##### UI @@ -93,7 +86,8 @@ https://github.com/user-attachments/assets/1aa790dc-fce9-4ac5-97d8-3b81b83acf2e ###### Planned -- Toast popups +See [#13](https://github.com/project-gauntlet/gauntlet/issues/13) + - Keyboard only navigation in plugin-views - Vim motions @@ -107,8 +101,7 @@ https://github.com/user-attachments/assets/1aa790dc-fce9-4ac5-97d8-3b81b83acf2e ###### Planned -- Local Storage -- OAuth PKCE flow support +See [#13](https://github.com/project-gauntlet/gauntlet/issues/13) ## Getting Started @@ -121,14 +114,14 @@ https://github.com/user-attachments/assets/1aa790dc-fce9-4ac5-97d8-3b81b83acf2e - You can configure plugin using [Plugin manifest](#plugin-manifest) - Documentation is, at the moment, basically non-existent but TypeScript declarations in `@project-gauntlet/api` and `@project-gauntlet/deno` should help - - See Dev Plugin for examples ![here](dev_plugin) + - For examples see [Dev Plugin](dev_plugin). It is very busy because it is used for Gauntlet development, but it has examples of pretty much every available API - Push changes to GitHub - Run `publish` GitHub Actions workflow to publish plugin to `gauntlet/release` branch - Profit! ### Install plugin -Plugins are installed in Settings UI. Use Git repository name of the plugin to install it. +Plugins are installed in Settings UI. Use Git repository url of the plugin to install it. ![](docs/settings_ui.png) @@ -136,21 +129,38 @@ Plugins are installed in Settings UI. Use Git repository name of the plugin to i #### macOS -Download `.dmg` file from [Releases](https://github.com/project-gauntlet/gauntlet/releases) section and install application from it. -At the moment, the application is not notarized by Apple and if plain installation is attempted -the "Is Damaged and Can’t Be Opened. You Should Move It To The Trash" error will appear. -To workaround this problem (given that you trust the application) you can remove `com.apple.quarantine` attribute from the `.dmg` file in terminal: -```bash -xattr -d com.apple.quarantine ./gauntlet-aarch64-macos.dmg +Download `.dmg` file from [Releases](https://github.com/project-gauntlet/gauntlet/releases) section and install application from it. Brew package is WIP + +#### Windows + +Although it is possible to install Gauntlet by using `.msi` directly, application doesn't have auto-update functionality so it is recommended to install using `chocolatey` package manager. + +Chocolatey package: [link](https://community.chocolatey.org/packages/gauntlet) + +To install run: +``` +choco install gauntlet ``` -#### Linux +To start, manually open application. -> [!NOTE] -> At the moment application is not published anywhere, -> so you have to download it from the [GitHub Releases](https://github.com/project-gauntlet/gauntlet/releases) +#### Arch Linux -Be the first one to create a package. See [Application packaging for Linux](#application-packaging-for-Linux) +AUR package: [link](https://aur.archlinux.org/packages/gauntlet-bin) + +To install run: +``` +yay -S gauntlet-bin +``` + +To start `systemd` service run: +``` +systemctl --user enable --now gauntlet.service +``` + +#### Other Linux Distributions + +At the moment application is only available for Arch Linux. If you want to create a package for other distributions see [Application packaging for Linux](#application-packaging-for-Linux) ### Global Shortcut Main window can be opened using global shortcut or CLI command: @@ -320,18 +330,20 @@ This section contains a list of things that could be useful for someone who wants to package application for Linux distribution. If something is missing, please [create an issue](https://github.com/project-gauntlet/gauntlet/issues). -Gauntlet executable consists of three applications: +Application is already packaged for Arch Linux so you can use it as example, see [Arch Linux](#arch-linux) -- `$ path/to/gauntlet --minimized` - - Needs to be started when user logs in -- `$ path/to/gauntlet open` - - Expected to be run on demand using launcher or system provided global shortcut -- `$ path/to/gauntlet settings` - - Started on demand from the list of available applications (will vary depending on desktop environment or window - manager chosen) or from Gauntlet itself +Relevant CLI commands: -Settings application expects Server to always be running. -Recommended way of ensuring that is running Server as SystemD service. +- `$ gauntlet --minimized` + - Server needs to be started when user logs in, e.g. using `systemd` service +- `$ gauntlet open` + - Main windows is usually opened using [global shortcut](#global-shortcut), this CLI command can be used in cases where global shortcut functionality is not available +- `$ gauntlet settings` + - Settings are usually started on demand from Gauntlet itself + +`.desktop` sample file can be found [here](assets/linux/gauntlet.desktop) + +`systemd` service sample file can be found [here](assets/linux/gauntlet.service) ###### Directories used @@ -342,7 +354,9 @@ Recommended way of ensuring that is running Server as SystemD service. - config dir - `$XDG_CONFIG_HOME/gauntlet` or `$HOME/.config/gauntlet` - contains application config `config.toml` - application will never do changes to config file -- `.desktop` file at locations defined by [Desktop Entry Specification](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html) +- state dir - `$XDG_STATE_HOME/gauntlet` or `$HOME/.local/state/gauntlet` + - contains log files created by plugin development +- `.desktop` files at locations defined by [Desktop Entry Specification](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html) Application and Dev Tools use temporary directories: @@ -352,7 +366,7 @@ Application and Dev Tools use temporary directories: X11 API is used to add global shortcut Client and Setting applications have GUI and therefore use all the usual graphics-related stuff from X11. -Wayland is not supported at the moment. +Wayland support requires LayerShell protocol `zwlr_layer_shell_v1`. ## Building Gauntlet You will need: From af16099a91d8348cb8e617ead957d8a3bfb8d47a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 30 Aug 2024 23:41:34 +0200 Subject: [PATCH 037/540] Add brew package to README.md --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ed05f50..dcef151 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,16 @@ Plugins are installed in Settings UI. Use Git repository url of the plugin to in #### macOS -Download `.dmg` file from [Releases](https://github.com/project-gauntlet/gauntlet/releases) section and install application from it. Brew package is WIP +Although it is possible to install Gauntlet by using `.dmg` directly, application doesn't have auto-update functionality so it is recommended to install using `brew` package manager. + +Brew package: [link](https://formulae.brew.sh/cask/gauntlet) + +To install run: +``` +brew install --cask gauntlet +``` + +To start, manually open application. #### Windows From 852404a5bd65cb58f55557f1830cc5e3ac0d06d3 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 3 Sep 2024 20:47:43 +0200 Subject: [PATCH 038/540] Fix crash on latest archlinux by updating softbuffer crate to latest --- Cargo.lock | 78 +++++++++++++++++++++--------------------------------- 1 file changed, 30 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ecbcb53..60a9004 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1123,22 +1123,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation 0.1.2", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "foreign-types 0.5.0", - "libc", - "objc", -] - [[package]] name = "cocoa" version = "0.26.0" @@ -1147,7 +1131,7 @@ checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ "bitflags 2.5.0", "block", - "cocoa-foundation 0.2.0", + "cocoa-foundation", "core-foundation 0.10.0", "core-graphics 0.24.0", "foreign-types 0.5.0", @@ -1155,20 +1139,6 @@ dependencies = [ "objc", ] -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "libc", - "objc", -] - [[package]] name = "cocoa-foundation" version = "0.2.0" @@ -1641,7 +1611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" dependencies = [ "bitflags 2.5.0", - "libloading 0.8.3", + "libloading 0.7.4", "winapi", ] @@ -2446,7 +2416,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.8.3", + "libloading 0.7.4", ] [[package]] @@ -2517,9 +2487,9 @@ dependencies = [ [[package]] name = "drm" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" +checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" dependencies = [ "bitflags 2.5.0", "bytemuck", @@ -2530,9 +2500,9 @@ dependencies = [ [[package]] name = "drm-ffi" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41334f8405792483e32ad05fbb9c5680ff4e84491883d2947a4757dc54cb2ac6" +checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" dependencies = [ "drm-sys", "rustix", @@ -2546,9 +2516,9 @@ checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" [[package]] name = "drm-sys" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d09ff881f92f118b11105ba5e34ff8f4adf27b30dae8f12e28c193af1c83176" +checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" dependencies = [ "libc", "linux-raw-sys 0.6.4", @@ -3856,7 +3826,7 @@ dependencies = [ "bitflags 2.5.0", "com", "libc", - "libloading 0.8.3", + "libloading 0.7.4", "thiserror", "widestring", "winapi", @@ -4873,7 +4843,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -5285,7 +5255,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" dependencies = [ - "cocoa 0.26.0", + "cocoa", "crossbeam-channel", "dpi", "gtk", @@ -5738,6 +5708,7 @@ checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.5.0", "block2 0.5.1", + "dispatch", "libc", "objc2 0.5.2", ] @@ -6934,6 +6905,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "redox_users" version = "0.4.5" @@ -8010,14 +7990,13 @@ dependencies = [ [[package]] name = "softbuffer" -version = "0.4.1" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071916a85d1db274b4ed57af3a14afb66bd836ae7f82ebb6f1fd3455107830d9" +checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd" dependencies = [ "as-raw-xcb-connection", "bytemuck", "cfg_aliases 0.2.0", - "cocoa 0.25.0", "core-graphics 0.23.2", "drm", "fastrand", @@ -8025,9 +8004,12 @@ dependencies = [ "js-sys", "log", "memmap2 0.9.4", - "objc", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", + "objc2-quartz-core", "raw-window-handle", - "redox_syscall 0.4.1", + "redox_syscall 0.5.3", "rustix", "tiny-xlib", "wasm-bindgen", @@ -10406,7 +10388,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.3", + "libloading 0.7.4", "log", "metal", "naga", From 960d773b9198c6eba2f0937213141f991dd27b90 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:23:50 +0200 Subject: [PATCH 039/540] Reduce minimal required version for macOS down to 11 --- .github/workflows/setup-macos.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/setup-macos.yaml b/.github/workflows/setup-macos.yaml index 86896af..f34f208 100644 --- a/.github/workflows/setup-macos.yaml +++ b/.github/workflows/setup-macos.yaml @@ -46,6 +46,7 @@ jobs: - run: ${{ inputs.command }} env: + MACOSX_DEPLOYMENT_TARGET: 11 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} GITHUB_RELEASE_ID: ${{ inputs.github-release-id }} From bfe3589cd78939cbe2a43570df05ed80073fbfe2 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 5 Sep 2024 20:52:46 +0200 Subject: [PATCH 040/540] Do not call command generators before reloading search index after frecency mark --- js/core/src/init.tsx | 11 ++++++++--- js/core/src/search-index.ts | 4 ++-- js/typings/index.d.ts | 8 ++++++-- rust/server/src/model.rs | 2 ++ rust/server/src/plugins/js/mod.rs | 9 +++++++-- rust/server/src/plugins/js/search.rs | 2 +- rust/server/src/plugins/mod.rs | 9 ++++++++- 7 files changed, 34 insertions(+), 11 deletions(-) diff --git a/js/core/src/init.tsx b/js/core/src/init.tsx index 17b102e..62a88f9 100644 --- a/js/core/src/init.tsx +++ b/js/core/src/init.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; import { runCommandGenerators, runGeneratedCommand } from "./command-generator"; -import { loadSearchIndex } from "./search-index"; +import { reloadSearchIndex } from "./search-index"; import { clearRenderer } from "gauntlet:renderer"; // @ts-expect-error does typescript support such symbol declarations? @@ -213,7 +213,12 @@ async function runLoop() { } case "ReloadSearchIndex": { runCommandGenerators() - .then(() => loadSearchIndex(false)); + .then(() => reloadSearchIndex(false)); + break; + } + case "RefreshSearchIndex": { + // noinspection ES6MissingAwait + reloadSearchIndex(false) break; } } @@ -221,7 +226,7 @@ async function runLoop() { } runCommandGenerators() - .then(() => loadSearchIndex(true)); + .then(() => reloadSearchIndex(true)); (async () => { await runLoop() diff --git a/js/core/src/search-index.ts b/js/core/src/search-index.ts index 7b2ebd0..31caaec 100644 --- a/js/core/src/search-index.ts +++ b/js/core/src/search-index.ts @@ -4,6 +4,6 @@ import { generatedCommandSearchIndex } from "./command-generator"; const denoCore: DenoCore = Deno[Deno.internal].core; const InternalApi = denoCore.ops; -export async function loadSearchIndex(refreshSearchList: boolean) { - await InternalApi.load_search_index(generatedCommandSearchIndex(), refreshSearchList); +export async function reloadSearchIndex(refreshSearchList: boolean) { + await InternalApi.reload_search_index(generatedCommandSearchIndex(), refreshSearchList); } \ No newline at end of file diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index d1ace34..61d2791 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -6,7 +6,7 @@ interface DenoCore { ops: InternalApi } -type PluginEvent = ViewEvent | NotReactsKeyboardEvent | RunCommand | RunGeneratedCommand | OpenView | CloseView | OpenInlineView | ReloadSearchIndex +type PluginEvent = ViewEvent | NotReactsKeyboardEvent | RunCommand | RunGeneratedCommand | OpenView | CloseView | OpenInlineView | ReloadSearchIndex | RefreshSearchIndex type RenderLocation = "InlineView" | "View" type ViewEvent = { @@ -55,6 +55,10 @@ type ReloadSearchIndex = { type: "ReloadSearchIndex" } +type RefreshSearchIndex = { + type: "RefreshSearchIndex" +} + type PropertyValue = PropertyValueString | PropertyValueNumber | PropertyValueBool | PropertyValueUndefined type PropertyValueString = { type: "String", value: string } type PropertyValueNumber = { type: "Number", value: number } @@ -99,7 +103,7 @@ interface InternalApi { entrypoint_preferences_required(entrypointId: string): Promise; show_preferences_required_view(entrypointId: string, pluginPreferencesRequired: boolean, entrypointPreferencesRequired: boolean): void; - load_search_index(searchItems: AdditionalSearchItem[], refreshSearchList: boolean): Promise; + reload_search_index(searchItems: AdditionalSearchItem[], refreshSearchList: boolean): Promise; op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, entrypoint_id: string, container: UiWidget): void; show_plugin_error_view(entrypoint_id: string, render_location: RenderLocation): void; diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index a14ad64..8bf7af6 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -78,6 +78,7 @@ pub enum JsUiEvent { text: String, }, ReloadSearchIndex, + RefreshSearchIndex, } // FIXME this could have been serde_v8::AnyValue but it doesn't support undefined, make a pr? @@ -137,6 +138,7 @@ pub enum IntermediateUiEvent { text: String, }, ReloadSearchIndex, + RefreshSearchIndex, } #[derive(Debug, Deserialize, Serialize)] diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index b92fdf9..a6280a5 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -41,7 +41,7 @@ use crate::plugins::js::plugins::applications::{list_applications, open_applicat use crate::plugins::js::plugins::numbat::run_numbat; use crate::plugins::js::plugins::settings::open_settings; use crate::plugins::js::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; -use crate::plugins::js::search::load_search_index; +use crate::plugins::js::search::reload_search_index; use crate::plugins::js::ui::{clear_inline_view, fetch_action_id_for_shortcut, op_component_model, op_inline_view_endpoint_id, op_react_replace_view, show_plugin_error_view, show_preferences_required_view}; use crate::plugins::run_status::RunStatusGuard; use crate::search::{SearchIndex, SearchIndexItem}; @@ -121,6 +121,7 @@ pub enum OnePluginCommandData { modifier_meta: bool, }, ReloadSearchIndex, + RefreshSearchIndex, } #[derive(Clone, Debug)] @@ -189,6 +190,9 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run OnePluginCommandData::ReloadSearchIndex => { Some(IntermediateUiEvent::ReloadSearchIndex) } + OnePluginCommandData::RefreshSearchIndex => { + Some(IntermediateUiEvent::RefreshSearchIndex) + } } } } @@ -500,7 +504,7 @@ deno_core::extension!( entrypoint_preferences_required, // search - load_search_index, + reload_search_index, // clipboard clipboard_read_text, @@ -659,6 +663,7 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { } IntermediateUiEvent::OpenInlineView { text } => JsUiEvent::OpenInlineView { text }, IntermediateUiEvent::ReloadSearchIndex => JsUiEvent::ReloadSearchIndex, + IntermediateUiEvent::RefreshSearchIndex => JsUiEvent::RefreshSearchIndex, } } diff --git a/rust/server/src/plugins/js/search.rs b/rust/server/src/plugins/js/search.rs index d17b9fb..8a156e6 100644 --- a/rust/server/src/plugins/js/search.rs +++ b/rust/server/src/plugins/js/search.rs @@ -11,7 +11,7 @@ use crate::plugins::js::PluginData; use crate::search::{SearchIndex, SearchIndexItem}; #[op] -async fn load_search_index(state: Rc>, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { +async fn reload_search_index(state: Rc>, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { let (plugin_id, plugin_uuid, repository, mut search_index, icon_cache) = { let state = state.borrow(); diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index be09932..706ccf3 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -415,6 +415,13 @@ impl ApplicationManager { }) } + pub fn request_search_index_refresh(&self, plugin_id: PluginId) { + self.send_command(PluginCommand::One { + id: plugin_id, + data: OnePluginCommandData::RefreshSearchIndex + }) + } + pub fn handle_open(&self, href: String) { match open::that_detached(&href) { Ok(()) => tracing::info!("Opened '{}' successfully.", href), @@ -603,7 +610,7 @@ impl ApplicationManager { tracing::warn!(target = "rpc", "error occurred when marking entrypoint frecency {:?}", err) } - self.request_search_index_reload(plugin_id); + self.request_search_index_refresh(plugin_id); } } From 908358b338d39cffe98eb6c82c95497406ff41c2 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 5 Sep 2024 20:54:48 +0200 Subject: [PATCH 041/540] Fix panic when spamming enable/disable plugin or entrypoint in settings --- rust/server/src/search.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index b93a911..822fcdd 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -71,6 +71,9 @@ impl SearchIndex { } pub fn remove_for_plugin(&self, plugin_id: PluginId) -> tantivy::Result<()> { + // writer panics if another writer exists + let _guard = self.index_writer_mutex.lock().expect("lock is poisoned"); + let mut index_writer = self.index.writer(5_000_000)?; index_writer.delete_query(Box::new( @@ -294,12 +297,12 @@ impl QueryParser { let entrypoint_name_terms = terms_fn(self.entrypoint_name); let plugin_name_terms = terms_fn(self.plugin_name); - return Box::new( + Box::new( BooleanQuery::union(vec![ Box::new(entrypoint_name_terms), Box::new(plugin_name_terms), ]), - ); + ) } fn tokenize(&self, query: &str) -> Vec { From 395fc5b33a3aa53d6f64593deff5982ce75b3a9b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 6 Sep 2024 18:37:05 +0200 Subject: [PATCH 042/540] Move back to using on the fly logs for npm build --- js/build/src/main.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/js/build/src/main.ts b/js/build/src/main.ts index 3126603..cf6fe0a 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -537,14 +537,9 @@ async function writeVersion(projectRoot: string, version: number) { function spawnWithErrors(command: string, args: string[], options: SpawnSyncOptions) { console.log(`running ${command} ${args}`) - const npmRunResult = spawnSync(command, args, { ...options, encoding: "utf-8" }); + const npmRunResult = spawnSync(command, args, { ...options, stdio: "inherit", }); if (npmRunResult.status !== 0) { throw new Error(`Unable to run ${command} ${args}, status: ${JSON.stringify(npmRunResult, null, 2)}`); - } else { - console.log("stdout: ") - console.log(npmRunResult.stdout) - console.log("stderr: ") - console.log(npmRunResult.stderr) } -} +} \ No newline at end of file From bfa9a2e8766b3eec5c5cd7ddb9a063551b5b0a88 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:02:56 +0200 Subject: [PATCH 043/540] Whoopsies! --- .github/workflows/setup-macos.yaml | 4 ++-- js/build/src/main.ts | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/setup-macos.yaml b/.github/workflows/setup-macos.yaml index f34f208..00daab7 100644 --- a/.github/workflows/setup-macos.yaml +++ b/.github/workflows/setup-macos.yaml @@ -50,6 +50,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} GITHUB_RELEASE_ID: ${{ inputs.github-release-id }} - APPLE_SIGNING_KEY_PEM: ${{ secrets.APPLE_SIGNING_KEY_PEM && fromJson(secrets.APPLE_SIGNING_KEY_PEM).content }} - APPLE_SIGNING_CERT_PEM: ${{ secrets.APPLE_SIGNING_CERT_PEM && fromJson(secrets.APPLE_SIGNING_CERT_PEM).content }} + APPLE_SIGNING_KEY_PEM: ${{ secrets.APPLE_SIGNING_KEY_PEM }} + APPLE_SIGNING_CERT_PEM: ${{ secrets.APPLE_SIGNING_CERT_PEM }} APP_STORE_CONNECT_KEY: ${{ secrets.APP_STORE_CONNECT_KEY }} diff --git a/js/build/src/main.ts b/js/build/src/main.ts index cf6fe0a..9569bba 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -350,8 +350,11 @@ async function packageForMacos(projectRoot: string, arch: string, sign: boolean, const connectApiKeyContent = process.env.APP_STORE_CONNECT_KEY; if (sign) { - writeFileSync(signKeyPath, signKeyContent!!); - writeFileSync(signCertPath, signCertContent!!); + const key = JSON.parse(signKeyContent!!).content; + const cert = JSON.parse(signCertContent!!).content; + + writeFileSync(signKeyPath, key); + writeFileSync(signCertPath, cert); spawnWithErrors(`rcodesign`, [ 'sign', From 5145d52e5191cdc3b2ceca29dff5b1927cfe71de Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 7 Sep 2024 12:11:39 +0200 Subject: [PATCH 044/540] Update CHANGELOG.md --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c4a6a..68cd866 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +### Plugin API +- Command Generator functions are no longer needlessly called after every entrypoint click in main window + +### Fixes +- Fixed crash on Arch Linux if using AMD GPU with Vulkan not setup properly + - Still if Vulkan is not setup properly it can result in low FPS when scrolling + - In some cases installing [vulkan-radeon](https://archlinux.org/packages/extra/x86_64/vulkan-radeon/) package resolves the FPS problem + - Alternatively setting `WGPU_BACKEND=gl` environment variable may also resolve the FPS problem +- Reduced minimal required version of macOS to 11 (Big Sur) +- Fix panic when spamming enable/disable plugin or entrypoint checkbox in settings + ## [7] - 2024-08-30 ### Big things From e59bdd5711c2abb814f237e9628972e7543d2df6 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 7 Sep 2024 12:25:15 +0200 Subject: [PATCH 045/540] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dcef151..3aa636b 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ https://github.com/user-attachments/assets/1aa790dc-fce9-4ac5-97d8-3b81b83acf2e - [Deno JavaScript Runtime](https://github.com/denoland/deno) - Deno allows us to sandbox JavaScript code for better security - Plugins are required to explicitly specify what permissions they need to work - - NodeJS is used to run plugin tooling + - NodeJS is used to run plugin tooling, but as a plugin developer you will always write code that runs on Deno - Frecency-based search result ordering - Frecency is a combination of frequency and recency - More often the item is used the higher in the result list it will be, but items used a lot in the past will be ranked lower than items used the same amount of times recently From 56096f84fe357fc4848d204d218a8857fa517d46 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 7 Sep 2024 10:41:21 +0000 Subject: [PATCH 046/540] Prepare for v8 release --- CHANGELOG.md | 2 ++ VERSION | 2 +- js/api/package.json | 2 +- js/deno/package.json | 2 +- package-lock.json | 4 ++-- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68cd866..e38448b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +## [8] - 2024-09-07 + ### Plugin API - Command Generator functions are no longer needlessly called after every entrypoint click in main window diff --git a/VERSION b/VERSION index c793025..301160a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7 \ No newline at end of file +8 \ No newline at end of file diff --git a/js/api/package.json b/js/api/package.json index effd701..1085889 100644 --- a/js/api/package.json +++ b/js/api/package.json @@ -1,6 +1,6 @@ { "name": "@project-gauntlet/api", - "version": "0.7.0", + "version": "0.8.0", "type": "module", "exports": { "./components": { diff --git a/js/deno/package.json b/js/deno/package.json index 84f0535..5516d4d 100644 --- a/js/deno/package.json +++ b/js/deno/package.json @@ -1,6 +1,6 @@ { "name": "@project-gauntlet/deno", - "version": "0.7.0", + "version": "0.8.0", "type": "module", "exports": { ".": { diff --git a/package-lock.json b/package-lock.json index 06ffb80..57affe1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ }, "js/api": { "name": "@project-gauntlet/api", - "version": "0.7.0", + "version": "0.8.0", "devDependencies": { "@project-gauntlet/typings": "*", "typescript": "^5.3.3" @@ -123,7 +123,7 @@ }, "js/deno": { "name": "@project-gauntlet/deno", - "version": "0.7.0", + "version": "0.8.0", "devDependencies": { "@types/node": "^18.17.1", "typescript": "^5.3.3" From cdcda9fb75d919765e2df54db369c65e032019b8 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 8 Sep 2024 16:07:04 +0200 Subject: [PATCH 047/540] Fix race condition when restarting plugins that leads to crash --- rust/server/src/plugins/run_status.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/rust/server/src/plugins/run_status.rs b/rust/server/src/plugins/run_status.rs index dc83128..5c13702 100644 --- a/rust/server/src/plugins/run_status.rs +++ b/rust/server/src/plugins/run_status.rs @@ -34,7 +34,7 @@ impl RunStatusHolder { let mut running_plugins = self.running_plugins.lock().expect("lock is poisoned"); running_plugins - .get(plugin_id) + .remove(plugin_id) .expect("value should always exist for specified id") .cancel() } @@ -56,10 +56,3 @@ impl RunStatusGuard { .cancelled_owned() } } - -impl Drop for RunStatusGuard { - fn drop(&mut self) { - let mut running_plugins = self.running_plugins.lock().expect("lock is poisoned"); - running_plugins.remove(&self.id); - } -} \ No newline at end of file From 3c72104ea76878d36f8c2f21dbd1e4f3eb8e8822 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 8 Sep 2024 19:51:50 +0200 Subject: [PATCH 048/540] Add isLoading property --- docs/js/components/detail/props/isLoading.md | 1 + docs/js/components/form/props/isLoading.md | 1 + docs/js/components/grid/props/isLoading.md | 1 + docs/js/components/list/props/isLoading.md | 1 + js/api/src/gen/components.tsx | 16 +- .../src/ui/custom_widgets/loading_bar.rs | 236 ++++++++++++++++++ rust/client/src/ui/custom_widgets/mod.rs | 1 + rust/client/src/ui/mod.rs | 1 + rust/client/src/ui/theme/loading_bar.rs | 29 +++ rust/client/src/ui/theme/mod.rs | 16 +- rust/client/src/ui/widget.rs | 29 ++- rust/component_model/src/lib.rs | 4 + 12 files changed, 319 insertions(+), 17 deletions(-) create mode 100644 docs/js/components/detail/props/isLoading.md create mode 100644 docs/js/components/form/props/isLoading.md create mode 100644 docs/js/components/grid/props/isLoading.md create mode 100644 docs/js/components/list/props/isLoading.md create mode 100644 rust/client/src/ui/custom_widgets/loading_bar.rs create mode 100644 rust/client/src/ui/custom_widgets/mod.rs create mode 100644 rust/client/src/ui/theme/loading_bar.rs diff --git a/docs/js/components/detail/props/isLoading.md b/docs/js/components/detail/props/isLoading.md new file mode 100644 index 0000000..d1ebe77 --- /dev/null +++ b/docs/js/components/detail/props/isLoading.md @@ -0,0 +1 @@ +If `true` loading bar is shown above content \ No newline at end of file diff --git a/docs/js/components/form/props/isLoading.md b/docs/js/components/form/props/isLoading.md new file mode 100644 index 0000000..d1ebe77 --- /dev/null +++ b/docs/js/components/form/props/isLoading.md @@ -0,0 +1 @@ +If `true` loading bar is shown above content \ No newline at end of file diff --git a/docs/js/components/grid/props/isLoading.md b/docs/js/components/grid/props/isLoading.md new file mode 100644 index 0000000..d1ebe77 --- /dev/null +++ b/docs/js/components/grid/props/isLoading.md @@ -0,0 +1 @@ +If `true` loading bar is shown above content \ No newline at end of file diff --git a/docs/js/components/list/props/isLoading.md b/docs/js/components/list/props/isLoading.md new file mode 100644 index 0000000..d1ebe77 --- /dev/null +++ b/docs/js/components/list/props/isLoading.md @@ -0,0 +1 @@ +If `true` loading bar is shown above content \ No newline at end of file diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index bb21efa..410f2d2 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -74,6 +74,7 @@ declare global { }; ["gauntlet:detail"]: { children?: ElementComponent; + isLoading?: boolean; }; ["gauntlet:text_field"]: { label?: string; @@ -109,6 +110,7 @@ declare global { ["gauntlet:separator"]: {}; ["gauntlet:form"]: { children?: ElementComponent; + isLoading?: boolean; }; ["gauntlet:inline_separator"]: { icon?: Icons; @@ -134,6 +136,7 @@ declare global { }; ["gauntlet:list"]: { children?: ElementComponent; + isLoading?: boolean; onSelectionChange?: (id: string) => void; }; ["gauntlet:grid_item"]: { @@ -150,6 +153,7 @@ declare global { }; ["gauntlet:grid"]: { children?: ElementComponent; + isLoading?: boolean; columns?: number; onSelectionChange?: (id: string) => void; }; @@ -516,13 +520,14 @@ Content.HorizontalBreak = HorizontalBreak; Content.CodeBlock = CodeBlock; export interface DetailProps { children?: ElementComponent; + isLoading?: boolean; actions?: ElementComponent; } export const Detail: FC & { Metadata: typeof Metadata; Content: typeof Content; } = (props: DetailProps): ReactNode => { - return {props.actions as any}{props.children}; + return {props.actions as any}{props.children}; }; Detail.Metadata = Metadata; Detail.Content = Content; @@ -583,6 +588,7 @@ export const Separator: FC = (): ReactNode => { }; export interface FormProps { children?: ElementComponent; + isLoading?: boolean; actions?: ElementComponent; } export const Form: FC & { @@ -593,7 +599,7 @@ export const Form: FC & { Select: typeof Select; Separator: typeof Separator; } = (props: FormProps): ReactNode => { - return {props.actions as any}{props.children}; + return {props.actions as any}{props.children}; }; Form.TextField = TextField; Form.PasswordField = PasswordField; @@ -653,6 +659,7 @@ ListSection.Item = ListItem; export interface ListProps { children?: ElementComponent; actions?: ElementComponent; + isLoading?: boolean; onSelectionChange?: (id: string) => void; } export const List: FC & { @@ -661,7 +668,7 @@ export const List: FC & { Item: typeof ListItem; Section: typeof ListSection; } = (props: ListProps): ReactNode => { - return {props.actions as any}{props.children}; + return {props.actions as any}{props.children}; }; List.EmptyView = EmptyView; List.Detail = Detail; @@ -693,6 +700,7 @@ export const GridSection: FC & { GridSection.Item = GridItem; export interface GridProps { children?: ElementComponent; + isLoading?: boolean; actions?: ElementComponent; columns?: number; onSelectionChange?: (id: string) => void; @@ -702,7 +710,7 @@ export const Grid: FC & { Item: typeof GridItem; Section: typeof GridSection; } = (props: GridProps): ReactNode => { - return {props.actions as any}{props.children}; + return {props.actions as any}{props.children}; }; Grid.EmptyView = EmptyView; Grid.Item = GridItem; diff --git a/rust/client/src/ui/custom_widgets/loading_bar.rs b/rust/client/src/ui/custom_widgets/loading_bar.rs new file mode 100644 index 0000000..16af639 --- /dev/null +++ b/rust/client/src/ui/custom_widgets/loading_bar.rs @@ -0,0 +1,236 @@ +use iced::advanced::layout::Limits; +use iced::advanced::layout::Node; +use iced::advanced::renderer; +use iced::advanced::widget::tree::State; +use iced::advanced::widget::tree::Tag; +use iced::advanced::widget::Tree; +use iced::advanced::Clipboard; +use iced::advanced::Layout; +use iced::advanced::Shell; +use iced::advanced::Widget; +use iced::event::Status; +use iced::mouse::Cursor; +use iced::{window, Color}; +use iced::Border; +use iced::Element; +use iced::Event; +use iced::Length; +use iced::Rectangle; +use iced::Shadow; +use iced::Size; +use std::time::{Duration, Instant}; + +pub struct LoadingBar +where + Theme: StyleSheet, +{ + width: Length, + segment_width: f32, + height: Length, + rate: Duration, + style: ::Style, +} + +impl Default for LoadingBar +where + Theme: StyleSheet, +{ + fn default() -> Self { + Self { + width: Length::Fill, + segment_width: 200.0, + height: Length::Fixed(1.0), + rate: Duration::from_secs_f32(1.0), + style: ::Style::default(), + } + } +} + +impl LoadingBar +where + Theme: StyleSheet, +{ + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + #[must_use] + pub fn segment_width(mut self, segment_width: f32) -> Self { + self.segment_width = segment_width; + self + } + + #[must_use] + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + #[must_use] + pub fn style(mut self, style: ::Style) -> Self { + self.style = style; + self + } +} + +struct LoadingBarState { + last_update: Instant, + t: f32, +} + +fn is_visible(bounds: &Rectangle) -> bool { + bounds.width > 0.0 && bounds.height > 0.0 +} + +impl Widget for LoadingBar +where + Renderer: renderer::Renderer, + Theme: StyleSheet, +{ + fn size(&self) -> Size { + Size::new(self.width, self.height) + } + + fn layout(&self, _tree: &mut Tree, _renderer: &Renderer, limits: &Limits) -> Node { + Node::new(limits.width(self.width).height(self.height).resolve( + self.width, + self.height, + Size::new(f32::INFINITY, f32::INFINITY), + )) + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + if !is_visible(&bounds) { + return; + } + + let position = bounds.position(); + let size = bounds.size(); + let styling = theme.appearance(&self.style); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: position.x, + y: position.y, + width: size.width, + height: size.height, + }, + border: Border::default(), + shadow: Shadow::default(), + }, + styling.background_color, + ); + + let state = state.state.downcast_ref::(); + + // works but quick and hacky + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: position.x + (size.width * state.t * 1.3) - self.segment_width, + y: position.y, + width: self.segment_width, + height: size.height, + }, + border: Border::default(), + shadow: Shadow::default(), + }, + styling.loading_bar_color, + ); + } + + fn tag(&self) -> Tag { + Tag::of::() + } + + fn state(&self) -> State { + State::new(LoadingBarState { + last_update: Instant::now(), + t: 0.0, + }) + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + _cursor: Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> Status { + const FRAMES_PER_SECOND: u64 = 60; + + let bounds = layout.bounds(); + + if let Event::Window(_id, window::Event::RedrawRequested(now)) = event { + if is_visible(&bounds) { + let state = state.state.downcast_mut::(); + let duration = (now - state.last_update).as_secs_f32(); + let increment = if self.rate == Duration::ZERO { + 0.0 + } else { + duration * 1.0 / self.rate.as_secs_f32() + }; + + state.t += increment; + + if state.t > 1.0 { + state.t -= 1.0; + } + + shell.request_redraw(window::RedrawRequest::At( + now + Duration::from_millis(1000 / FRAMES_PER_SECOND), + )); + state.last_update = now; + + return Status::Captured; + } + } + + Status::Ignored + } +} + +#[derive(Clone, Copy, Debug)] +pub struct Appearance { + pub background_color: Color, + pub loading_bar_color: Color, +} + +pub trait StyleSheet { + type Style: Default; + fn appearance(&self, style: &Self::Style) -> Appearance; +} + + +impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> +where + Renderer: renderer::Renderer + 'a, + Theme: 'a + StyleSheet, +{ + fn from(spinner: LoadingBar) -> Self { + Self::new(spinner) + } +} diff --git a/rust/client/src/ui/custom_widgets/mod.rs b/rust/client/src/ui/custom_widgets/mod.rs new file mode 100644 index 0000000..cf7b726 --- /dev/null +++ b/rust/client/src/ui/custom_widgets/mod.rs @@ -0,0 +1 @@ +pub mod loading_bar; diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 1129836..8b66a24 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -49,6 +49,7 @@ mod widget_container; mod inline_view_container; #[cfg(any(target_os = "macos", target_os = "windows"))] mod sys_tray; +mod custom_widgets; pub use theme::GauntletTheme; diff --git a/rust/client/src/ui/theme/loading_bar.rs b/rust/client/src/ui/theme/loading_bar.rs new file mode 100644 index 0000000..5f94901 --- /dev/null +++ b/rust/client/src/ui/theme/loading_bar.rs @@ -0,0 +1,29 @@ +use crate::ui::custom_widgets::loading_bar; +use crate::ui::custom_widgets::loading_bar::{Appearance, LoadingBar}; +use crate::ui::theme::{Element, ThemableWidget}; +use crate::ui::GauntletTheme; + +#[derive(Default)] +pub enum LoadingBarStyle { + #[default] + Default, +} + +impl loading_bar::StyleSheet for GauntletTheme { + type Style = LoadingBarStyle; + + fn appearance(&self, _style: &Self::Style) -> Appearance { + Appearance { + background_color: self.loading_bar.background_color.to_iced(), + loading_bar_color: self.loading_bar.loading_bar_color.to_iced(), + } + } +} + +impl<'a, Message: 'a> ThemableWidget<'a, Message> for LoadingBar { + type Kind = LoadingBarStyle; + + fn themed(self, _kind: LoadingBarStyle) -> Element<'a, Message> { + self.into() + } +} diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 640387f..eb7f6a0 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -20,11 +20,12 @@ pub mod rule; pub mod space; pub mod grid; pub mod tooltip; +mod loading_bar; pub type Element<'a, Message> = iced::Element<'a, Message, GauntletTheme>; const CURRENT_COLOR_THEME_VERSION: u64 = 1; -const CURRENT_THEME_VERSION: u64 = 1; +const CURRENT_THEME_VERSION: u64 = 2; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GauntletColorTheme { @@ -110,6 +111,7 @@ pub struct GauntletTheme { separator: ThemeSeparator, scrollbar: ThemeScrollbar, tooltip: ThemeTooltip, + loading_bar: ThemeLoadingBar, } impl Default for GauntletTheme { @@ -534,7 +536,7 @@ impl GauntletTheme { border_color_hovered: background_light_color, }, separator: ThemeSeparator { - color: BACKGROUND_LIGHT + color: background_light_color }, scrollbar: ThemeScrollbar { color: primary_color, @@ -546,6 +548,10 @@ impl GauntletTheme { padding: 8.0, background_color: background_overlay_color, }, + loading_bar: ThemeLoadingBar { + loading_bar_color: primary_color, + background_color: background_light_color, + }, } } } @@ -700,6 +706,12 @@ pub struct ThemeActionShortcutModifier { border_color: ThemeColor, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThemeLoadingBar { + loading_bar_color: ThemeColor, + background_color: ThemeColor, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ThemeLink { text_color: ThemeColor, diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 47f0679..853a993 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -21,6 +21,7 @@ use common::model::{PhysicalShortcut, PluginId, UiPropertyValue, UiPropertyValue use common_ui::shortcut_to_text; use crate::model::UiViewEvent; +use crate::ui::custom_widgets::loading_bar::LoadingBar; use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; @@ -536,7 +537,7 @@ impl ComponentWidgetWrapper { content } } - ComponentWidget::Detail { children } => { + ComponentWidget::Detail { children, isLoading: is_loading } => { let ComponentWidgetState::Detail { show_action_panel } = *state else { panic!("unexpected state kind {:?}", state) }; @@ -607,7 +608,7 @@ impl ComponentWidgetWrapper { if is_in_list { content } else { - render_root(show_action_panel, widget_id, children, content, context) + render_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } } ComponentWidget::Root { children } => { @@ -717,7 +718,7 @@ impl ComponentWidgetWrapper { horizontal_rule(1) .into() } - ComponentWidget::Form { children } => { + ComponentWidget::Form { children, isLoading: is_loading } => { let ComponentWidgetState::Form { show_action_panel } = *state else { panic!("unexpected state kind {:?}", state) }; @@ -794,7 +795,7 @@ impl ComponentWidgetWrapper { .width(Length::Fill) .themed(ContainerStyle::Form); - render_root(show_action_panel, widget_id, children, content, context) + render_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } ComponentWidget::InlineSeparator { icon } => { match icon { @@ -971,7 +972,7 @@ impl ComponentWidgetWrapper { render_section(content, Some(title), subtitle, RowStyle::ListSectionTitle, TextStyle::ListSectionTitle) } - ComponentWidget::List { children } => { + ComponentWidget::List { children, isLoading: is_loading } => { let ComponentWidgetState::List { show_action_panel } = *state else { panic!("unexpected state kind {:?}", state) }; @@ -1057,7 +1058,7 @@ impl ComponentWidgetWrapper { .height(Length::Fill) .into(); - render_root(show_action_panel, widget_id, children, content, context) + render_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } ComponentWidget::GridItem { children, id, title, subtitle } => { let ComponentRenderContext::Grid { widget_id: grid_widget_id } = context else { @@ -1093,7 +1094,7 @@ impl ComponentWidgetWrapper { render_section(content, Some(title), subtitle, RowStyle::GridSectionTitle, TextStyle::GridSectionTitle) } - ComponentWidget::Grid { children, columns } => { + ComponentWidget::Grid { children, columns, isLoading: is_loading } => { let ComponentWidgetState::Grid { show_action_panel } = *state else { panic!("unexpected state kind {:?}", state) }; @@ -1145,7 +1146,7 @@ impl ComponentWidgetWrapper { .width(Length::Fill) .themed(ContainerStyle::Grid); - render_root(show_action_panel, widget_id, children, content, context) + render_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } } } @@ -1281,7 +1282,8 @@ fn render_root<'a>( widget_id: UiWidgetId, children: &[ComponentWidgetWrapper], content: Element<'a, ComponentWidgetEvent>, - context: ComponentRenderContext + context: ComponentRenderContext, + is_loading: bool ) -> Element<'a, ComponentWidgetEvent> { let ComponentRenderContext::Root { entrypoint_name, action_shortcuts } = context else { panic!("not supposed to be passed to root item: {:?}", context) @@ -1322,8 +1324,13 @@ fn render_root<'a>( .width(Length::Fill) .themed(ContainerStyle::RootBottomPanel); - let top_separator = horizontal_rule(1) - .into(); + let top_separator = if is_loading { + LoadingBar::new() + .into() + } else { + horizontal_rule(1) + .into() + }; let content: Element<_> = container(content) .width(Length::Fill) diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index 50ee102..de3e8b8 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -750,6 +750,7 @@ pub fn create_component_model() -> Vec { mark_doc!("/detail/description.md"), "Detail", [ + property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), property("actions", mark_doc!("/detail/props/actions.md"), true, component_ref(&action_panel_component)) ], children_members([ @@ -869,6 +870,7 @@ pub fn create_component_model() -> Vec { mark_doc!("/form/description.md"), "Form", [ + property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), property("actions", mark_doc!("/form/props/actions.md"), true, component_ref(&action_panel_component)), ], children_members([ @@ -951,6 +953,7 @@ pub fn create_component_model() -> Vec { "List", [ property("actions", mark_doc!("/list/props/actions.md"), true, component_ref(&action_panel_component)), + property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), event("onSelectionChange", mark_doc!("/list/props/onSelectionChange.md"), true, [ property("id", "".to_string(), false, PropertyType::String) ]), @@ -1000,6 +1003,7 @@ pub fn create_component_model() -> Vec { mark_doc!("/grid/description.md"), "Grid", [ + property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), property("actions", mark_doc!("/grid/props/actions.md"),true, component_ref(&action_panel_component)), // property("aspectRatio", true, PropertyType::String), property("columns", mark_doc!("/grid/props/columns.md"),true, PropertyType::Number), // TODO default From adb6a48347d5e06317253c06a697bee6102b9ebf Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 8 Sep 2024 21:01:41 +0200 Subject: [PATCH 049/540] Prevent js runtime from shutting down when uncaught exception is thrown inside a promise --- dev_plugin/src/command-generator.ts | 4 ++++ js/core/src/init.tsx | 4 ++++ js/typings/index.d.ts | 3 +++ 3 files changed, 11 insertions(+) diff --git a/dev_plugin/src/command-generator.ts b/dev_plugin/src/command-generator.ts index 50e8449..b063d79 100644 --- a/dev_plugin/src/command-generator.ts +++ b/dev_plugin/src/command-generator.ts @@ -6,6 +6,10 @@ export default function CommandGenerator(): GeneratedCommand[] { id: 'generated-test-1', name: 'Generated Item 1', fn: () => { + new Promise(() => { + throw new Error("gen") + }) + console.log('generated-test-1') } }, diff --git a/js/core/src/init.tsx b/js/core/src/init.tsx index 62a88f9..6718492 100644 --- a/js/core/src/init.tsx +++ b/js/core/src/init.tsx @@ -225,6 +225,10 @@ async function runLoop() { } } +denoCore.setPromiseRejectCallback((_type, _promise, reason) => { + console.error("Uncaught exception in promise", reason) +}) + runCommandGenerators() .then(() => reloadSearchIndex(true)); diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 61d2791..ad58fd7 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -3,9 +3,12 @@ interface DenoCore { opAsync: (op: "op_plugin_get_pending_event") => Promise + setPromiseRejectCallback: (cb: PromiseRejectCallback) => undefined | PromiseRejectCallback; ops: InternalApi } +type PromiseRejectCallback = (type: number, promise: Promise, reason: any) => void; + type PluginEvent = ViewEvent | NotReactsKeyboardEvent | RunCommand | RunGeneratedCommand | OpenView | CloseView | OpenInlineView | ReloadSearchIndex | RefreshSearchIndex type RenderLocation = "InlineView" | "View" From 2f232aae0278cbc8a94e910459433452a78e5b7b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:38:46 +0200 Subject: [PATCH 050/540] useAsync hook --- dev_plugin/src/list-view.tsx | 208 ++++++++++++++++++++++++++++++++++- js/api/src/hooks.ts | 183 +++++++++++++++++++++++++++++- js/core/src/init.tsx | 2 +- 3 files changed, 390 insertions(+), 3 deletions(-) diff --git a/dev_plugin/src/list-view.tsx b/dev_plugin/src/list-view.tsx index ac0f04a..f2902b5 100644 --- a/dev_plugin/src/list-view.tsx +++ b/dev_plugin/src/list-view.tsx @@ -1,7 +1,18 @@ import { Icons, List } from "@project-gauntlet/api/components"; -import { ReactElement, useState } from "react"; +import { ReactElement, useRef, useState } from "react"; +import { useAsync } from "@project-gauntlet/api/hooks"; export default function ListView(): ReactElement { + // return useAsyncTestBasic() + // return useAsyncTestExecuteFalse() + // return useAsyncTestRevalidate() + // return useAsyncTestAbortableRevalidate() + // return useAsyncTestMutate() + // return useAsyncTestMutateOptimistic() + // return useAsyncTestMutateOptimisticRollback() + // return useAsyncTestMutateNoRevalidate() + // return useAsyncTestThrow() + const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; const [id, setId] = useState("default"); @@ -27,4 +38,199 @@ export default function ListView(): ReactElement { ) +} + +function useAsyncTestBasic(): ReactElement { + const { data, error, isLoading } = useAsync( + async (one, two, three) => await inNSec(5), + [1, 2, 3] + ); + + printState(data, error, isLoading) + + return ( + {}}> + + + ) +} + +function useAsyncTestExecuteFalse(): ReactElement { + const { data, error, isLoading } = useAsync( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + { + execute: false + } + ); + + printState(data, error, isLoading) + + return ( + {}}> + + + ) +} + +function useAsyncTestRevalidate(): ReactElement { + const { data, error, isLoading, revalidate } = useAsync( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + ); + + printState(data, error, isLoading) + + return ( + revalidate()}> + + + ) +} + +function useAsyncTestAbortableRevalidate(): ReactElement { + const abortable = useRef(); + + const { data, error, isLoading, revalidate } = useAsync( + async (one, two, three) => { + await inNSec(5) + }, + [1, 2, 3], + { + abortable, + } + ); + + printState(data, error, isLoading) + + return ( + revalidate()}> + + + ) +} + +function useAsyncTestMutate(): ReactElement { + const { data, error, isLoading, mutate } = useAsync( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + ); + + printState(data, error, isLoading) + + return ( + await mutate(inNSec(5))}> + + + ) +} + +function useAsyncTestMutateOptimistic(): ReactElement { + const { data, error, isLoading, mutate } = useAsync( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + ); + + printState(data, error, isLoading) + + return ( + { + await mutate( + inNSec(5), + { + optimisticUpdate: data1 => data1 + " optimistic", + } + ) + }}> + + + ) +} + +function useAsyncTestMutateOptimisticRollback(): ReactElement { + const { data, error, isLoading, mutate } = useAsync( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + ); + + printState(data, error, isLoading) + + return ( + { + const newVar = await mutate( + new Promise((_resolve, reject) => { + setTimeout( + () => { + reject("fail") + }, + 5 * 1000 + ); + }), + { + optimisticUpdate: data1 => data1 + " optimistic", + rollbackOnError: data1 => data1 + " failed", + } + ); + }}> + + + ) +} + +function useAsyncTestMutateNoRevalidate(): ReactElement { + const { data, error, isLoading, mutate } = useAsync( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + ); + + printState(data, error, isLoading) + + return ( + { + await mutate( + inNSec(5), + { + shouldRevalidateAfter: false, + } + ) + }}> + + + ) +} + +function useAsyncTestThrow(): ReactElement { + const { data, error, isLoading } = useAsync( + async (one, two, three) => { + throw new Error("test") + }, + [1, 2, 3], + ); + + printState(data, error, isLoading) + + return ( + {}}> + + + ) +} + +async function inNSec(n: number): Promise { + return new Promise(resolve => { + setTimeout( + () => { + resolve(`Promise resolved after ${n} sec: ${Math.random()}`) + }, + n * 1000 + ); + }) +} + +function printState(data: any, error: unknown, isLoading: boolean) { + console.log("") + console.log("=====") + console.dir(data) + console.dir(error) + console.dir(isLoading) } \ No newline at end of file diff --git a/js/api/src/hooks.ts b/js/api/src/hooks.ts index 8e6c399..baf71e4 100644 --- a/js/api/src/hooks.ts +++ b/js/api/src/hooks.ts @@ -1,4 +1,4 @@ -import { ReactNode } from 'react'; +import { ReactNode, useRef, useState, useCallback, useEffect, MutableRefObject } from 'react'; // @ts-ignore TODO how to add declaration for this? import { useGauntletContext } from "gauntlet:renderer"; @@ -14,3 +14,184 @@ export function useNavigation(): { popView: () => void, pushView: (component: Re } } } + +export type AsyncStateInitial = { + isLoading: boolean; // false if options.execute is false, otherwise true + error?: undefined; + data?: undefined; +}; +export type AsyncStateLoading = { + isLoading: true; + error?: unknown | undefined; + data?: T; +}; +export type AsyncStateError = { + isLoading: false; + error: unknown; + data?: undefined; +}; +export type AsyncStateSuccess = { + isLoading: false; + error?: undefined; + data: T; +}; + +export type AsyncState = AsyncStateInitial | AsyncStateLoading | AsyncStateError | AsyncStateSuccess; + +export type MutatePromiseFn = ( + asyncUpdate: Promise, + options?: { + optimisticUpdate?: (data: T | undefined) => T; // undefined, if options.execute is false and function was never called, needs to be pure + rollbackOnError?: boolean | ((data: T | undefined) => T); // only used if optimisticUpdate is specified, needs to be pure + shouldRevalidateAfter?: boolean; // only matters for successful updates + }, +) => Promise; + +export function useAsync Promise, R>( + fn: T, + args?: Parameters, + options?: { + abortable?: MutableRefObject; + execute?: boolean; + onError?: (error: unknown) => void; + onData?: (data: Awaited>) => void; + onWillExecute?: (...args: Parameters) => void; + }, +): AsyncState>> & { + revalidate: () => void; // will execute even if options.execute is false + mutate: MutatePromiseFn>, R>; // will execute even if options.execute is false +} { + const execute = options?.execute !== false; // execute by default + + const promiseRef = useRef>(); + const [state, setState] = useState>>>({ isLoading: execute }); + + useEffect(() => { + return () => { + options?.abortable?.current?.abort(); + }; + }, [options?.abortable]); + + const callback = useCallback(async (...args: Parameters): Promise => { + if (options && options.abortable) { + options.abortable.current?.abort(); + options.abortable.current = new AbortController() + } + + options?.onWillExecute?.(...args); + + const promise = fn(...args); + + setState(prevState => ({ ...prevState, isLoading: true })); + + promiseRef.current = promise; + + let promiseResult: Awaited>; + try { + promiseResult = await promise; + } catch (error) { + // We dont want to handle result/error of non-latest function + // this approach helps to avoid race conditions + if (promise === promiseRef.current) { + setState({ error, isLoading: false }) + + options?.onError?.(error); + } + return + } + + // We dont want to handle result/error of non-latest function + // this approach helps to avoid race conditions + if (promise === promiseRef.current) { + setState({ data: promiseResult, isLoading: false }); + + options?.onData?.(promiseResult) + } + }, args || []); + + useEffect(() => { + if (execute) { + callback(...(args || ([] as any))); + } + }, [callback, execute]); + + return { + revalidate: () => { + callback(...(args || ([] as any))); + }, + mutate: async ( + asyncUpdate: Promise, + options?: { + optimisticUpdate?: (data: Awaited> | undefined) => Awaited>; + rollbackOnError?: boolean | ((data: Awaited> | undefined) => Awaited>); + shouldRevalidateAfter?: boolean; + }, + ): Promise => { + const prevData = state.data; + + const optimisticUpdate = options?.optimisticUpdate; + const rollbackOnError = options?.rollbackOnError; + const shouldRevalidateAfter = options?.shouldRevalidateAfter !== false; + + if (optimisticUpdate) { + const newData = optimisticUpdate(state.data); + setState({ data: newData, isLoading: true }) + + try { + const asyncUpdateResult = await asyncUpdate; + + if (shouldRevalidateAfter) { + callback(...(args || ([] as any))); + } else { + // set loading false, only when not revalidating, because revalidate will unset it itself + setState(prevState => ({ ...prevState, isLoading: false })); + } + + return asyncUpdateResult + } catch (e) { + switch (typeof rollbackOnError) { + case "undefined": { + if (prevData === undefined) { + setState({ data: prevData, isLoading: false }) + } else { + setState({ data: prevData, isLoading: false }) + } + break; + } + case "boolean": { + if (rollbackOnError) { + if (prevData === undefined) { + setState({ data: prevData, isLoading: false }) + } else { + setState({ data: prevData, isLoading: false }) + } + } + break; + } + case "function": { + const rolledBackData = rollbackOnError(state.data); + setState({ data: rolledBackData, isLoading: false }) + break; + } + } + + throw e + } + } else { + setState(prevState => ({ ...prevState, isLoading: true })); + + const asyncUpdateResult = await asyncUpdate; + + if (shouldRevalidateAfter) { + callback(...(args || ([] as any))); + } else { + // set loading false, only when not revalidating, because revalidate will unset it itself + setState(prevState => ({ ...prevState, isLoading: false })); + } + + return asyncUpdateResult + } + }, + ...state + }; +} diff --git a/js/core/src/init.tsx b/js/core/src/init.tsx index 6718492..697537f 100644 --- a/js/core/src/init.tsx +++ b/js/core/src/init.tsx @@ -226,7 +226,7 @@ async function runLoop() { } denoCore.setPromiseRejectCallback((_type, _promise, reason) => { - console.error("Uncaught exception in promise", reason) + console.error("Rejected promise", reason) }) runCommandGenerators() From b86ae6c0d9a0cf67c6035fb25476e96103bc9022 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 9 Sep 2024 21:27:48 +0200 Subject: [PATCH 051/540] Rename useAsync to usePromise --- dev_plugin/src/list-view.tsx | 56 ++++++++++++++++++------------------ js/api/src/hooks.ts | 2 +- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/dev_plugin/src/list-view.tsx b/dev_plugin/src/list-view.tsx index f2902b5..c5ac1bb 100644 --- a/dev_plugin/src/list-view.tsx +++ b/dev_plugin/src/list-view.tsx @@ -1,17 +1,17 @@ import { Icons, List } from "@project-gauntlet/api/components"; import { ReactElement, useRef, useState } from "react"; -import { useAsync } from "@project-gauntlet/api/hooks"; +import { usePromise } from "@project-gauntlet/api/hooks"; export default function ListView(): ReactElement { - // return useAsyncTestBasic() - // return useAsyncTestExecuteFalse() - // return useAsyncTestRevalidate() - // return useAsyncTestAbortableRevalidate() - // return useAsyncTestMutate() - // return useAsyncTestMutateOptimistic() - // return useAsyncTestMutateOptimisticRollback() - // return useAsyncTestMutateNoRevalidate() - // return useAsyncTestThrow() + // return usePromiseTestBasic() + // return usePromiseTestExecuteFalse() + // return usePromiseTestRevalidate() + // return usePromiseTestAbortableRevalidate() + // return usePromiseTestMutate() + // return usePromiseTestMutateOptimistic() + // return usePromiseTestMutateOptimisticRollback() + // return usePromiseTestMutateNoRevalidate() + // return usePromiseTestThrow() const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; const [id, setId] = useState("default"); @@ -40,8 +40,8 @@ export default function ListView(): ReactElement { ) } -function useAsyncTestBasic(): ReactElement { - const { data, error, isLoading } = useAsync( +function usePromiseTestBasic(): ReactElement { + const { data, error, isLoading } = usePromise( async (one, two, three) => await inNSec(5), [1, 2, 3] ); @@ -55,8 +55,8 @@ function useAsyncTestBasic(): ReactElement { ) } -function useAsyncTestExecuteFalse(): ReactElement { - const { data, error, isLoading } = useAsync( +function usePromiseTestExecuteFalse(): ReactElement { + const { data, error, isLoading } = usePromise( async (one, two, three) => await inNSec(5), [1, 2, 3], { @@ -73,8 +73,8 @@ function useAsyncTestExecuteFalse(): ReactElement { ) } -function useAsyncTestRevalidate(): ReactElement { - const { data, error, isLoading, revalidate } = useAsync( +function usePromiseTestRevalidate(): ReactElement { + const { data, error, isLoading, revalidate } = usePromise( async (one, two, three) => await inNSec(5), [1, 2, 3], ); @@ -88,10 +88,10 @@ function useAsyncTestRevalidate(): ReactElement { ) } -function useAsyncTestAbortableRevalidate(): ReactElement { +function usePromiseTestAbortableRevalidate(): ReactElement { const abortable = useRef(); - const { data, error, isLoading, revalidate } = useAsync( + const { data, error, isLoading, revalidate } = usePromise( async (one, two, three) => { await inNSec(5) }, @@ -110,8 +110,8 @@ function useAsyncTestAbortableRevalidate(): ReactElement { ) } -function useAsyncTestMutate(): ReactElement { - const { data, error, isLoading, mutate } = useAsync( +function usePromiseTestMutate(): ReactElement { + const { data, error, isLoading, mutate } = usePromise( async (one, two, three) => await inNSec(5), [1, 2, 3], ); @@ -125,8 +125,8 @@ function useAsyncTestMutate(): ReactElement { ) } -function useAsyncTestMutateOptimistic(): ReactElement { - const { data, error, isLoading, mutate } = useAsync( +function usePromiseTestMutateOptimistic(): ReactElement { + const { data, error, isLoading, mutate } = usePromise( async (one, two, three) => await inNSec(5), [1, 2, 3], ); @@ -147,8 +147,8 @@ function useAsyncTestMutateOptimistic(): ReactElement { ) } -function useAsyncTestMutateOptimisticRollback(): ReactElement { - const { data, error, isLoading, mutate } = useAsync( +function usePromiseTestMutateOptimisticRollback(): ReactElement { + const { data, error, isLoading, mutate } = usePromise( async (one, two, three) => await inNSec(5), [1, 2, 3], ); @@ -177,8 +177,8 @@ function useAsyncTestMutateOptimisticRollback(): ReactElement { ) } -function useAsyncTestMutateNoRevalidate(): ReactElement { - const { data, error, isLoading, mutate } = useAsync( +function usePromiseTestMutateNoRevalidate(): ReactElement { + const { data, error, isLoading, mutate } = usePromise( async (one, two, three) => await inNSec(5), [1, 2, 3], ); @@ -199,8 +199,8 @@ function useAsyncTestMutateNoRevalidate(): ReactElement { ) } -function useAsyncTestThrow(): ReactElement { - const { data, error, isLoading } = useAsync( +function usePromiseTestThrow(): ReactElement { + const { data, error, isLoading } = usePromise( async (one, two, three) => { throw new Error("test") }, diff --git a/js/api/src/hooks.ts b/js/api/src/hooks.ts index baf71e4..9e3f34f 100644 --- a/js/api/src/hooks.ts +++ b/js/api/src/hooks.ts @@ -47,7 +47,7 @@ export type MutatePromiseFn = ( }, ) => Promise; -export function useAsync Promise, R>( +export function usePromise Promise, R>( fn: T, args?: Parameters, options?: { From bff3bd87780ad0a4166eb52946972cba4ffeae0d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 9 Sep 2024 21:32:19 +0200 Subject: [PATCH 052/540] Reduce job timeout to 60 min --- .github/workflows/setup-linux.yaml | 1 + .github/workflows/setup-macos.yaml | 1 + .github/workflows/setup-windows.yaml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/setup-linux.yaml b/.github/workflows/setup-linux.yaml index b06d552..98a92e0 100644 --- a/.github/workflows/setup-linux.yaml +++ b/.github/workflows/setup-linux.yaml @@ -12,6 +12,7 @@ on: jobs: run-on-linux: runs-on: ubuntu-latest + timeout-minutes: 60 steps: - run: sudo apt-get update - run: sudo apt-get install -y protobuf-compiler diff --git a/.github/workflows/setup-macos.yaml b/.github/workflows/setup-macos.yaml index 00daab7..0843161 100644 --- a/.github/workflows/setup-macos.yaml +++ b/.github/workflows/setup-macos.yaml @@ -12,6 +12,7 @@ on: jobs: run-on-macos: runs-on: macos-latest + timeout-minutes: 60 steps: # https://github.com/actions/runner-images/issues/7522#issuecomment-1556766641 - name: Kill XProtectBehaviorService diff --git a/.github/workflows/setup-windows.yaml b/.github/workflows/setup-windows.yaml index acca72c..e409a6a 100644 --- a/.github/workflows/setup-windows.yaml +++ b/.github/workflows/setup-windows.yaml @@ -12,6 +12,7 @@ on: jobs: run-on-windows: runs-on: windows-latest + timeout-minutes: 60 steps: - uses: actions/checkout@v4 with: From 0eb1ba4c9b680705902f451c20372e7e5f31b424 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 10 Sep 2024 19:11:15 +0200 Subject: [PATCH 053/540] Do not send abort event in usePromise when promise has already finished --- js/api/src/hooks.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/js/api/src/hooks.ts b/js/api/src/hooks.ts index 9e3f34f..b644a0c 100644 --- a/js/api/src/hooks.ts +++ b/js/api/src/hooks.ts @@ -95,6 +95,10 @@ export function usePromise Promise, R>( if (promise === promiseRef.current) { setState({ error, isLoading: false }) + if (options && options.abortable) { + options.abortable.current = undefined; + } + options?.onError?.(error); } return @@ -105,6 +109,10 @@ export function usePromise Promise, R>( if (promise === promiseRef.current) { setState({ data: promiseResult, isLoading: false }); + if (options && options.abortable) { + options.abortable.current = undefined; + } + options?.onData?.(promiseResult) } }, args || []); From 2154131b5c382d374c322bc5dcf612f7f6048795 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:48:32 +0200 Subject: [PATCH 054/540] Add useStorage and useCache hooks --- dev_data/state/.gitignore | 3 +- dev_plugin/src/command-generator.ts | 6 ++++ dev_plugin/src/grid-view.tsx | 8 +++++- js/api/src/hooks.ts | 44 ++++++++++++++++++++++++++++- rust/common/src/dirs.rs | 4 +++ rust/server/src/plugins/js/mod.rs | 3 ++ 6 files changed, 65 insertions(+), 3 deletions(-) diff --git a/dev_data/state/.gitignore b/dev_data/state/.gitignore index cd3d225..f1a26a4 100644 --- a/dev_data/state/.gitignore +++ b/dev_data/state/.gitignore @@ -1 +1,2 @@ -logs \ No newline at end of file +logs +local_storage \ No newline at end of file diff --git a/dev_plugin/src/command-generator.ts b/dev_plugin/src/command-generator.ts index b063d79..39f91af 100644 --- a/dev_plugin/src/command-generator.ts +++ b/dev_plugin/src/command-generator.ts @@ -18,6 +18,12 @@ export default function CommandGenerator(): GeneratedCommand[] { name: 'Generated Item 2', fn: () => { console.log('generated-test-2') + + sessionStorage.setItem("test", "test") + console.dir(sessionStorage.getItem("test")) + + localStorage.setItem("test", "test") + console.dir(localStorage.getItem("test")) } }, { diff --git a/dev_plugin/src/grid-view.tsx b/dev_plugin/src/grid-view.tsx index c7af561..1180676 100644 --- a/dev_plugin/src/grid-view.tsx +++ b/dev_plugin/src/grid-view.tsx @@ -1,11 +1,17 @@ import { Grid } from "@project-gauntlet/api/components"; import { ReactElement } from "react"; +import { useStorage } from "@project-gauntlet/api/hooks"; export default function GridView(): ReactElement { const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; + const [val1, setValue1] = useStorage("test", undefined); + const [val2, setValue2] = useStorage("test", { " test": "test" }); + const [val3, setValue3] = useStorage("test", ""); + const [val4, setValue4] = useStorage("test", ""); + return ( - + {}}> { numbers.map(value => ( diff --git a/js/api/src/hooks.ts b/js/api/src/hooks.ts index b644a0c..94dde6f 100644 --- a/js/api/src/hooks.ts +++ b/js/api/src/hooks.ts @@ -1,4 +1,4 @@ -import { ReactNode, useRef, useState, useCallback, useEffect, MutableRefObject } from 'react'; +import { ReactNode, useRef, useState, useCallback, useEffect, MutableRefObject, Dispatch, SetStateAction } from 'react'; // @ts-ignore TODO how to add declaration for this? import { useGauntletContext } from "gauntlet:renderer"; @@ -203,3 +203,45 @@ export function usePromise Promise, R>( ...state }; } + +// persistent, uses localStorage under the hood +export function useStorage(key: string, initialState: T | (() => T)): [T, Dispatch>] { + return useWebStorage(key, initialState, localStorage) +} + +// ephemeral, uses sessionStorage under the hood +export function useCache(key: string, initialState: T | (() => T)): [T, Dispatch>] { + return useWebStorage(key, initialState, sessionStorage) +} + +// keys are shared per plugin, across all entrypoints +// uses JSON.serialize +function useWebStorage( + key: string, + initialState: T | (() => T), + storageObject: Storage +): [T, Dispatch>] { + + const [value, setValue] = useState(() => { + const jsonValue = storageObject.getItem(key) + + if (jsonValue != null) { + return JSON.parse(jsonValue) as T + } + + if (initialState instanceof Function) { + return initialState() + } else { + return initialState + } + }) + + useEffect(() => { + if (value === undefined) { + return storageObject.removeItem(key) + } + storageObject.setItem(key, JSON.stringify(value)) + }, [key, value, storageObject]) + + return [value, setValue] +} diff --git a/rust/common/src/dirs.rs b/rust/common/src/dirs.rs index 96f3485..3da1480 100644 --- a/rust/common/src/dirs.rs +++ b/rust/common/src/dirs.rs @@ -94,6 +94,10 @@ impl Dirs { (out_log_file, err_log_file) } + pub fn plugin_local_storage(&self, plugin_uuid: &str) -> PathBuf { + self.state_dir().join("local_storage").join(&plugin_uuid) + } + pub fn state_dir(&self) -> PathBuf { let state_dir = if cfg!(feature = "release") || cfg!(feature = "scenario_runner") { let dir = match self.inner.state_dir() { diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index a6280a5..f385962 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -322,6 +322,8 @@ async fn start_js_runtime( (StdioPipe::Inherit, StdioPipe::Inherit) }; + let local_storage_dir = dirs.plugin_local_storage(&plugin_uuid); + // let _inspector_server = Arc::new( // InspectorServer::new( // "127.0.0.1:9229".parse::().unwrap(), @@ -352,6 +354,7 @@ async fn start_js_runtime( maybe_inspector_server: None, should_wait_for_inspector_session: false, should_break_on_first_statement: false, + origin_storage_dir: Some(local_storage_dir), stdio: Stdio { stdin: StdioPipe::Inherit, stdout, From f9f373916f1879cd17f54011e04bba927fc0df00 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:25:28 +0200 Subject: [PATCH 055/540] Add useCachedPromise hook --- dev_plugin/gauntlet.toml | 7 + dev_plugin/src/hooks-view.tsx | 372 ++++++++++++++++++++++++++++++ dev_plugin/src/list-view.tsx | 208 +---------------- js/api/src/hooks.ts | 126 +++++++--- js/react_renderer/src/renderer.ts | 8 +- 5 files changed, 483 insertions(+), 238 deletions(-) create mode 100644 dev_plugin/src/hooks-view.tsx diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index b36fefd..6741b0c 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -107,6 +107,13 @@ path = 'src/list-view.tsx' type = 'view' description = '' +[[entrypoint]] +id = 'hooks-view' +name = 'Hooks view' +path = 'src/hooks-view.tsx' +type = 'view' +description = '' + [[entrypoint]] id = 'command-a' name = 'Command A' diff --git a/dev_plugin/src/hooks-view.tsx b/dev_plugin/src/hooks-view.tsx new file mode 100644 index 0000000..4099dbf --- /dev/null +++ b/dev_plugin/src/hooks-view.tsx @@ -0,0 +1,372 @@ +import { Icons, List } from "@project-gauntlet/api/components"; +import React, { ReactElement, useRef } from "react"; +import { useCachedPromise, useNavigation, usePromise } from "@project-gauntlet/api/hooks"; + +export default function ListView(): ReactElement { + const { pushView } = useNavigation(); + + return ( + { + switch (id) { + case "UsePromiseTestBasic": { + pushView() + break; + } + case "UsePromiseTestExecuteFalse": { + pushView() + break; + } + case "UsePromiseTestRevalidate": { + pushView() + break; + } + case "UsePromiseTestAbortableRevalidate": { + pushView() + break; + } + case "UsePromiseTestMutate": { + pushView() + break; + } + case "UsePromiseTestMutateOptimistic": { + pushView() + break; + } + case "UsePromiseTestMutateOptimisticRollback": { + pushView() + break; + } + case "UsePromiseTestMutateNoRevalidate": { + pushView() + break; + } + case "UsePromiseTestThrow": { + pushView() + break; + } + case "UseCachedPromiseBasic": { + pushView() + break; + } + case "UseCachedPromiseInitialState": { + pushView() + break; + } + } + }} + > + + + + + + + + + + + + + ) +} + +function UsePromiseTestBasic(): ReactElement { + const { popView } = useNavigation(); + const { data, error, isLoading } = usePromise( + async (one, two, three) => await inNSec(5), + [1, 2, 3] + ); + + printState(data, error, isLoading) + + return ( + + + + ) +} + +function UseCachedPromiseBasic(): ReactElement { + const { popView } = useNavigation(); + const { data, error, isLoading } = useCachedPromise( + async (one, two, three) => await inNSec(5), + [1, 2, 3] + ); + + printState(data, error, isLoading) + + return ( + + + + + + ) +} + +function UseCachedPromiseInitialState(): ReactElement { + const { popView } = useNavigation(); + const { data, error, isLoading } = useCachedPromise( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + { + initialState: () => "initial" + } + ); + + printState(data, error, isLoading) + + return ( + + + + + + ) +} + +function UsePromiseTestExecuteFalse(): ReactElement { + const { popView } = useNavigation(); + const { data, error, isLoading } = usePromise( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + { + execute: false + } + ); + + printState(data, error, isLoading) + + return ( + + + + ) +} + +function UsePromiseTestRevalidate(): ReactElement { + const { popView } = useNavigation(); + + const { data, error, isLoading, revalidate } = usePromise( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + ); + + printState(data, error, isLoading) + + return ( + revalidate())} + > + + + + ) +} + +function UsePromiseTestAbortableRevalidate(): ReactElement { + const { popView } = useNavigation(); + const abortable = useRef(); + + const { data, error, isLoading, revalidate } = usePromise( + async (one, two, three) => { + await inNSec(5) + }, + [1, 2, 3], + { + abortable, + } + ); + + printState(data, error, isLoading) + + return ( + revalidate())} + > + + + + ) +} + +function UsePromiseTestMutate(): ReactElement { + const { popView } = useNavigation(); + const { data, error, isLoading, mutate } = usePromise( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + ); + + printState(data, error, isLoading) + + return ( + { + await mutate(inNSec(5)) + })} + > + + + + ) +} + +function UsePromiseTestMutateOptimistic(): ReactElement { + const { popView } = useNavigation(); + const { data, error, isLoading, mutate } = usePromise( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + ); + + printState(data, error, isLoading) + + return ( + { + await mutate( + inNSec(5), + { + optimisticUpdate: data1 => data1 + " optimistic", + } + ) + })} + > + + + + ) +} + +function UsePromiseTestMutateOptimisticRollback(): ReactElement { + const { popView } = useNavigation(); + const { data, error, isLoading, mutate } = usePromise( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + ); + + printState(data, error, isLoading) + + return ( + { + await mutate( + new Promise((_resolve, reject) => { + setTimeout( + () => { + reject("fail") + }, + 5 * 1000 + ); + }), + { + optimisticUpdate: data1 => data1 + " optimistic", + rollbackOnError: data1 => data1 + " failed", + } + ); + })} + > + + + + ) +} + +function UsePromiseTestMutateNoRevalidate(): ReactElement { + const { popView } = useNavigation(); + const { data, error, isLoading, mutate } = usePromise( + async (one, two, three) => await inNSec(5), + [1, 2, 3], + ); + + printState(data, error, isLoading) + + return ( + { + await mutate( + inNSec(5), + { + shouldRevalidateAfter: false, + } + ) + })} + > + + + + ) +} + +function UsePromiseTestThrow(): ReactElement { + const { popView } = useNavigation(); + const { data, error, isLoading } = usePromise( + async (one, two, three) => { + throw new Error("test") + }, + [1, 2, 3], + ); + + printState(data, error, isLoading) + + return ( + + + + ) +} + +function onSelectionChangeHandler(popView: () => void, handler?: () => void): (id: string) => void { + return (id) => { + switch (id) { + case "back": { + popView() + break; + } + case "run": { + handler?.() + break; + } + } + } +} + +async function inNSec(n: number): Promise { + return new Promise(resolve => { + setTimeout( + () => { + resolve(`Value: ${Math.random()}`) + }, + n * 1000 + ); + }) +} + +function printState(data: any, error: unknown, isLoading: boolean) { + console.log("") + console.log("=====") + console.dir(data) + console.dir(error) + console.dir(isLoading) +} \ No newline at end of file diff --git a/dev_plugin/src/list-view.tsx b/dev_plugin/src/list-view.tsx index c5ac1bb..ac0f04a 100644 --- a/dev_plugin/src/list-view.tsx +++ b/dev_plugin/src/list-view.tsx @@ -1,18 +1,7 @@ import { Icons, List } from "@project-gauntlet/api/components"; -import { ReactElement, useRef, useState } from "react"; -import { usePromise } from "@project-gauntlet/api/hooks"; +import { ReactElement, useState } from "react"; export default function ListView(): ReactElement { - // return usePromiseTestBasic() - // return usePromiseTestExecuteFalse() - // return usePromiseTestRevalidate() - // return usePromiseTestAbortableRevalidate() - // return usePromiseTestMutate() - // return usePromiseTestMutateOptimistic() - // return usePromiseTestMutateOptimisticRollback() - // return usePromiseTestMutateNoRevalidate() - // return usePromiseTestThrow() - const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; const [id, setId] = useState("default"); @@ -38,199 +27,4 @@ export default function ListView(): ReactElement { ) -} - -function usePromiseTestBasic(): ReactElement { - const { data, error, isLoading } = usePromise( - async (one, two, three) => await inNSec(5), - [1, 2, 3] - ); - - printState(data, error, isLoading) - - return ( - {}}> - - - ) -} - -function usePromiseTestExecuteFalse(): ReactElement { - const { data, error, isLoading } = usePromise( - async (one, two, three) => await inNSec(5), - [1, 2, 3], - { - execute: false - } - ); - - printState(data, error, isLoading) - - return ( - {}}> - - - ) -} - -function usePromiseTestRevalidate(): ReactElement { - const { data, error, isLoading, revalidate } = usePromise( - async (one, two, three) => await inNSec(5), - [1, 2, 3], - ); - - printState(data, error, isLoading) - - return ( - revalidate()}> - - - ) -} - -function usePromiseTestAbortableRevalidate(): ReactElement { - const abortable = useRef(); - - const { data, error, isLoading, revalidate } = usePromise( - async (one, two, three) => { - await inNSec(5) - }, - [1, 2, 3], - { - abortable, - } - ); - - printState(data, error, isLoading) - - return ( - revalidate()}> - - - ) -} - -function usePromiseTestMutate(): ReactElement { - const { data, error, isLoading, mutate } = usePromise( - async (one, two, three) => await inNSec(5), - [1, 2, 3], - ); - - printState(data, error, isLoading) - - return ( - await mutate(inNSec(5))}> - - - ) -} - -function usePromiseTestMutateOptimistic(): ReactElement { - const { data, error, isLoading, mutate } = usePromise( - async (one, two, three) => await inNSec(5), - [1, 2, 3], - ); - - printState(data, error, isLoading) - - return ( - { - await mutate( - inNSec(5), - { - optimisticUpdate: data1 => data1 + " optimistic", - } - ) - }}> - - - ) -} - -function usePromiseTestMutateOptimisticRollback(): ReactElement { - const { data, error, isLoading, mutate } = usePromise( - async (one, two, three) => await inNSec(5), - [1, 2, 3], - ); - - printState(data, error, isLoading) - - return ( - { - const newVar = await mutate( - new Promise((_resolve, reject) => { - setTimeout( - () => { - reject("fail") - }, - 5 * 1000 - ); - }), - { - optimisticUpdate: data1 => data1 + " optimistic", - rollbackOnError: data1 => data1 + " failed", - } - ); - }}> - - - ) -} - -function usePromiseTestMutateNoRevalidate(): ReactElement { - const { data, error, isLoading, mutate } = usePromise( - async (one, two, three) => await inNSec(5), - [1, 2, 3], - ); - - printState(data, error, isLoading) - - return ( - { - await mutate( - inNSec(5), - { - shouldRevalidateAfter: false, - } - ) - }}> - - - ) -} - -function usePromiseTestThrow(): ReactElement { - const { data, error, isLoading } = usePromise( - async (one, two, three) => { - throw new Error("test") - }, - [1, 2, 3], - ); - - printState(data, error, isLoading) - - return ( - {}}> - - - ) -} - -async function inNSec(n: number): Promise { - return new Promise(resolve => { - setTimeout( - () => { - resolve(`Promise resolved after ${n} sec: ${Math.random()}`) - }, - n * 1000 - ); - }) -} - -function printState(data: any, error: unknown, isLoading: boolean) { - console.log("") - console.log("=====") - console.dir(data) - console.dir(error) - console.dir(isLoading) } \ No newline at end of file diff --git a/js/api/src/hooks.ts b/js/api/src/hooks.ts index 94dde6f..1073c9d 100644 --- a/js/api/src/hooks.ts +++ b/js/api/src/hooks.ts @@ -1,4 +1,4 @@ -import { ReactNode, useRef, useState, useCallback, useEffect, MutableRefObject, Dispatch, SetStateAction } from 'react'; +import { ReactNode, useRef, useId, useState, useCallback, useEffect, MutableRefObject, Dispatch, SetStateAction } from 'react'; // @ts-ignore TODO how to add declaration for this? import { useGauntletContext } from "gauntlet:renderer"; @@ -47,38 +47,110 @@ export type MutatePromiseFn = ( }, ) => Promise; +export type UsePromiseOptions Promise> = { + abortable?: MutableRefObject; + execute?: boolean; + onError?: (error: unknown) => void; + onData?: (data: Awaited>) => void; + onWillExecute?: (...args: Parameters) => void; +} + export function usePromise Promise, R>( fn: T, args?: Parameters, - options?: { - abortable?: MutableRefObject; - execute?: boolean; - onError?: (error: unknown) => void; - onData?: (data: Awaited>) => void; - onWillExecute?: (...args: Parameters) => void; - }, + options?: UsePromiseOptions, +): AsyncState>> & { + revalidate: () => void; + mutate: MutatePromiseFn>, R>; +} { + const execute = options?.execute !== false; // execute by default + + const [state, setState] = useState>>>({ isLoading: execute }); + + return usePromiseInternal( + fn, + state, + setState, + args || ([] as any), + execute, + options?.abortable, + options?.onError, + options?.onData, + options?.onWillExecute + ) +} + +export function useCachedPromise Promise, R>( + fn: T, + args?: Parameters, + options?: UsePromiseOptions & { initialState?: Awaited> | (() => Awaited>) }, +): AsyncState>> & { + revalidate: () => void; + mutate: MutatePromiseFn>, R>; +} { + const execute = options?.execute !== false; // execute by default + + const id = useId(); + + const { entrypointId }: { entrypointId: () => string } = useGauntletContext(); + + // same store is fetched and updated between command runs + const [state, setState] = useCache>>>("useCachedPromise" + entrypointId() + id, () => { + const initialState = options?.initialState; + if (initialState) { + if (initialState instanceof Function) { + return { isLoading: execute, data: initialState() } + } else { + return { isLoading: execute, data: initialState } + } + } else { + return { isLoading: execute } + } + }); + + return usePromiseInternal( + fn, + state, + setState, + args || ([] as any), + execute, + options?.abortable, + options?.onError, + options?.onData, + options?.onWillExecute + ) +} + +function usePromiseInternal Promise, R>( + fn: T, + state: AsyncState>>, + setState: Dispatch>>>>, + args: Parameters, + execute: boolean, + abortable?: MutableRefObject, + onError?: (error: unknown) => void, + onData?: (data: Awaited>) => void, + onWillExecute?: (...args: Parameters) => void, ): AsyncState>> & { revalidate: () => void; // will execute even if options.execute is false mutate: MutatePromiseFn>, R>; // will execute even if options.execute is false } { - const execute = options?.execute !== false; // execute by default const promiseRef = useRef>(); - const [state, setState] = useState>>>({ isLoading: execute }); useEffect(() => { return () => { - options?.abortable?.current?.abort(); + abortable?.current?.abort(); }; - }, [options?.abortable]); + }, [abortable]); const callback = useCallback(async (...args: Parameters): Promise => { - if (options && options.abortable) { - options.abortable.current?.abort(); - options.abortable.current = new AbortController() + if (abortable) { + abortable.current?.abort(); + abortable.current = new AbortController() } - options?.onWillExecute?.(...args); + onWillExecute?.(...args); const promise = fn(...args); @@ -95,11 +167,11 @@ export function usePromise Promise, R>( if (promise === promiseRef.current) { setState({ error, isLoading: false }) - if (options && options.abortable) { - options.abortable.current = undefined; + if (abortable) { + abortable.current = undefined; } - options?.onError?.(error); + onError?.(error); } return } @@ -109,23 +181,23 @@ export function usePromise Promise, R>( if (promise === promiseRef.current) { setState({ data: promiseResult, isLoading: false }); - if (options && options.abortable) { - options.abortable.current = undefined; + if (abortable) { + abortable.current = undefined; } - options?.onData?.(promiseResult) + onData?.(promiseResult) } - }, args || []); + }, args); useEffect(() => { if (execute) { - callback(...(args || ([] as any))); + callback(...args); } }, [callback, execute]); return { revalidate: () => { - callback(...(args || ([] as any))); + callback(...args); }, mutate: async ( asyncUpdate: Promise, @@ -149,7 +221,7 @@ export function usePromise Promise, R>( const asyncUpdateResult = await asyncUpdate; if (shouldRevalidateAfter) { - callback(...(args || ([] as any))); + callback(...args); } else { // set loading false, only when not revalidating, because revalidate will unset it itself setState(prevState => ({ ...prevState, isLoading: false })); @@ -191,7 +263,7 @@ export function usePromise Promise, R>( const asyncUpdateResult = await asyncUpdate; if (shouldRevalidateAfter) { - callback(...(args || ([] as any))); + callback(...args); } else { // set loading false, only when not revalidating, because revalidate will unset it itself setState(prevState => ({ ...prevState, isLoading: false })); diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index 6e35e20..a1331ee 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -52,19 +52,19 @@ class GauntletContextValue { this._navStack.push(view) } - renderLocation(): RenderLocation { + renderLocation = (): RenderLocation => { return this._renderLocation!! } - isBottommostView() { + isBottommostView = () => { return this._navStack.length === 1 } - topmostView() { + topmostView = () => { return this._navStack[this._navStack.length - 1] } - entrypointId() { + entrypointId = () => { return this._entrypointId!! } From 9f38e3aa2d2432392fc38c59fe9ec007b797a958 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:39:23 +0200 Subject: [PATCH 056/540] Display data in all hooks examples --- dev_plugin/src/hooks-view.tsx | 48 ++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/dev_plugin/src/hooks-view.tsx b/dev_plugin/src/hooks-view.tsx index 4099dbf..19dd1e4 100644 --- a/dev_plugin/src/hooks-view.tsx +++ b/dev_plugin/src/hooks-view.tsx @@ -85,7 +85,9 @@ function UsePromiseTestBasic(): ReactElement { isLoading={isLoading} onSelectionChange={onSelectionChangeHandler(popView)} > - + + + ) } @@ -152,7 +154,9 @@ function UsePromiseTestExecuteFalse(): ReactElement { isLoading={isLoading} onSelectionChange={onSelectionChangeHandler(popView)} > - + + + ) } @@ -172,8 +176,10 @@ function UsePromiseTestRevalidate(): ReactElement { isLoading={isLoading} onSelectionChange={onSelectionChangeHandler(popView, () => revalidate())} > - - + + + + ) } @@ -199,8 +205,10 @@ function UsePromiseTestAbortableRevalidate(): ReactElement { isLoading={isLoading} onSelectionChange={onSelectionChangeHandler(popView, () => revalidate())} > - - + + + + ) } @@ -221,8 +229,10 @@ function UsePromiseTestMutate(): ReactElement { await mutate(inNSec(5)) })} > - - + + + + ) } @@ -248,8 +258,10 @@ function UsePromiseTestMutateOptimistic(): ReactElement { ) })} > - - + + + + ) } @@ -283,8 +295,10 @@ function UsePromiseTestMutateOptimisticRollback(): ReactElement { ); })} > - - + + + + ) } @@ -310,8 +324,10 @@ function UsePromiseTestMutateNoRevalidate(): ReactElement { ) })} > - - + + + + ) } @@ -332,7 +348,9 @@ function UsePromiseTestThrow(): ReactElement { isLoading={isLoading} onSelectionChange={onSelectionChangeHandler(popView)} > - + + + ) } From 60578e63914348fd8b5c1f8dfb53a85c879534c8 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 13 Sep 2024 19:27:10 +0200 Subject: [PATCH 057/540] Add useFetch hook. Refine types in other hooks --- dev_plugin/gauntlet.toml | 2 +- dev_plugin/src/hooks-view.tsx | 64 ++++++++++- js/api/src/hooks.ts | 198 +++++++++++++++++++++++----------- 3 files changed, 197 insertions(+), 67 deletions(-) diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 6741b0c..9d36add 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -147,4 +147,4 @@ os = 'windows' [permissions] environment = ["RUST_LOG"] system = ["systemMemoryInfo"] -network = ["upload.wikimedia.org"] \ No newline at end of file +network = ["upload.wikimedia.org", "api.github.com"] \ No newline at end of file diff --git a/dev_plugin/src/hooks-view.tsx b/dev_plugin/src/hooks-view.tsx index 19dd1e4..babc285 100644 --- a/dev_plugin/src/hooks-view.tsx +++ b/dev_plugin/src/hooks-view.tsx @@ -1,6 +1,6 @@ import { Icons, List } from "@project-gauntlet/api/components"; import React, { ReactElement, useRef } from "react"; -import { useCachedPromise, useNavigation, usePromise } from "@project-gauntlet/api/hooks"; +import { useCachedPromise, useFetch, useNavigation, usePromise } from "@project-gauntlet/api/hooks"; export default function ListView(): ReactElement { const { pushView } = useNavigation(); @@ -53,6 +53,14 @@ export default function ListView(): ReactElement { pushView() break; } + case "UseFetchBasic": { + pushView() + break; + } + case "UseFetchMap": { + pushView() + break; + } } }} > @@ -67,6 +75,8 @@ export default function ListView(): ReactElement { + + ) } @@ -355,6 +365,58 @@ function UsePromiseTestThrow(): ReactElement { ) } +function UseFetchBasic(): ReactElement { + const { popView } = useNavigation(); + + interface GithubLatestRelease { + + } + + const { data, error, isLoading } = useFetch( + "https://api.github.com/repos/project-gauntlet/gauntlet/releases/latest" + ); + + printState(data, error, isLoading) + + return ( + + + + + + ) +} + +function UseFetchMap(): ReactElement { + interface GithubLatestRelease { + url: string + } + + const { popView } = useNavigation(); + const { data, error, isLoading } = useFetch( + "https://api.github.com/repos/project-gauntlet/gauntlet/releases/latest", + { + map: result => result.url + } + ); + + printState(data, error, isLoading) + + return ( + + + + + + ) +} + function onSelectionChangeHandler(popView: () => void, handler?: () => void): (id: string) => void { return (id) => { switch (id) { diff --git a/js/api/src/hooks.ts b/js/api/src/hooks.ts index 1073c9d..535ec31 100644 --- a/js/api/src/hooks.ts +++ b/js/api/src/hooks.ts @@ -15,28 +15,11 @@ export function useNavigation(): { popView: () => void, pushView: (component: Re } } -export type AsyncStateInitial = { - isLoading: boolean; // false if options.execute is false, otherwise true - error?: undefined; - data?: undefined; -}; -export type AsyncStateLoading = { - isLoading: true; - error?: unknown | undefined; +export type AsyncState = { + isLoading: boolean; + error?: unknown; data?: T; }; -export type AsyncStateError = { - isLoading: false; - error: unknown; - data?: undefined; -}; -export type AsyncStateSuccess = { - isLoading: false; - error?: undefined; - data: T; -}; - -export type AsyncState = AsyncStateInitial | AsyncStateLoading | AsyncStateError | AsyncStateSuccess; export type MutatePromiseFn = ( asyncUpdate: Promise, @@ -47,25 +30,23 @@ export type MutatePromiseFn = ( }, ) => Promise; -export type UsePromiseOptions Promise> = { - abortable?: MutableRefObject; - execute?: boolean; - onError?: (error: unknown) => void; - onData?: (data: Awaited>) => void; - onWillExecute?: (...args: Parameters) => void; -} - -export function usePromise Promise, R>( - fn: T, - args?: Parameters, - options?: UsePromiseOptions, -): AsyncState>> & { +export function usePromise( + fn: (...args: Args) => Promise, + args?: Args, + options?: { + abortable?: MutableRefObject; + execute?: boolean; + onError?: (error: unknown) => void; + onData?: (data: Return) => void; + onWillExecute?: (...args: Args) => void; + }, +): AsyncState & { revalidate: () => void; - mutate: MutatePromiseFn>, R>; + mutate: MutatePromiseFn; } { const execute = options?.execute !== false; // execute by default - const [state, setState] = useState>>>({ isLoading: execute }); + const [state, setState] = useState>({ isLoading: execute }); return usePromiseInternal( fn, @@ -80,13 +61,20 @@ export function usePromise Promise, R>( ) } -export function useCachedPromise Promise, R>( - fn: T, - args?: Parameters, - options?: UsePromiseOptions & { initialState?: Awaited> | (() => Awaited>) }, -): AsyncState>> & { +export function useCachedPromise( + fn: (...args: Args) => Promise, + args?: Args, + options?: { + initialState?: Return | (() => Return), + abortable?: MutableRefObject; + execute?: boolean; + onError?: (error: unknown) => void; + onData?: (data: Return) => void; + onWillExecute?: (...args: Args) => void; + }, +): AsyncState & { revalidate: () => void; - mutate: MutatePromiseFn>, R>; + mutate: MutatePromiseFn; } { const execute = options?.execute !== false; // execute by default @@ -95,7 +83,7 @@ export function useCachedPromise Promise, R>( const { entrypointId }: { entrypointId: () => string } = useGauntletContext(); // same store is fetched and updated between command runs - const [state, setState] = useCache>>>("useCachedPromise" + entrypointId() + id, () => { + const [state, setState] = useCache>("useCachedPromise" + entrypointId() + id, (): AsyncState => { const initialState = options?.initialState; if (initialState) { if (initialState instanceof Function) { @@ -121,19 +109,19 @@ export function useCachedPromise Promise, R>( ) } -function usePromiseInternal Promise, R>( - fn: T, - state: AsyncState>>, - setState: Dispatch>>>>, - args: Parameters, +function usePromiseInternal( + fn: (...args: Args) => Promise, + state: AsyncState, + setState: Dispatch>>, + args: Args, execute: boolean, abortable?: MutableRefObject, onError?: (error: unknown) => void, - onData?: (data: Awaited>) => void, - onWillExecute?: (...args: Parameters) => void, -): AsyncState>> & { + onData?: (data: Return) => void, + onWillExecute?: (...args: Args) => void, +): AsyncState & { revalidate: () => void; // will execute even if options.execute is false - mutate: MutatePromiseFn>, R>; // will execute even if options.execute is false + mutate: MutatePromiseFn; // will execute even if options.execute is false } { const promiseRef = useRef>(); @@ -144,7 +132,7 @@ function usePromiseInternal Promise, R>( }; }, [abortable]); - const callback = useCallback(async (...args: Parameters): Promise => { + const callback = useCallback(async (...args: Args): Promise => { if (abortable) { abortable.current?.abort(); abortable.current = new AbortController() @@ -158,7 +146,7 @@ function usePromiseInternal Promise, R>( promiseRef.current = promise; - let promiseResult: Awaited>; + let promiseResult: Return; try { promiseResult = await promise; } catch (error) { @@ -202,8 +190,8 @@ function usePromiseInternal Promise, R>( mutate: async ( asyncUpdate: Promise, options?: { - optimisticUpdate?: (data: Awaited> | undefined) => Awaited>; - rollbackOnError?: boolean | ((data: Awaited> | undefined) => Awaited>); + optimisticUpdate?: (data: Return | undefined) => Return; + rollbackOnError?: boolean | ((data: Return | undefined) => Return); shouldRevalidateAfter?: boolean; }, ): Promise => { @@ -231,20 +219,12 @@ function usePromiseInternal Promise, R>( } catch (e) { switch (typeof rollbackOnError) { case "undefined": { - if (prevData === undefined) { - setState({ data: prevData, isLoading: false }) - } else { - setState({ data: prevData, isLoading: false }) - } + setState({ data: prevData, isLoading: false }) break; } case "boolean": { if (rollbackOnError) { - if (prevData === undefined) { - setState({ data: prevData, isLoading: false }) - } else { - setState({ data: prevData, isLoading: false }) - } + setState({ data: prevData, isLoading: false }) } break; } @@ -317,3 +297,91 @@ function useWebStorage( return [value, setValue] } + +export function useFetch( + url: RequestInfo | URL, + options?: { + request?: RequestInit, + parse?: (response: Response) => T | Promise; + initialState?: T | (() => T), + execute?: boolean; + onError?: (error: unknown) => void; + onData?: (data: T) => void; + onWillExecute?: (input: RequestInfo | URL, init?: RequestInit) => void; + }, +): AsyncState & { + revalidate: () => void; + mutate: MutatePromiseFn; +}; +export function useFetch( + url: RequestInfo | URL, + options: { + request?: RequestInit, + parse?: (response: Response) => V | Promise; + map: (result: V) => T | Promise; + initialState?: T | (() => T), + execute?: boolean; + onError?: (error: unknown) => void; + onData?: (data: T) => void; + onWillExecute?: (input: RequestInfo | URL, init?: RequestInit) => void; + }, +): AsyncState & { + revalidate: () => void; + mutate: MutatePromiseFn; +}; +export function useFetch( + url: RequestInfo | URL, + options?: { + request?: RequestInit, + parse?: (response: Response) => V | Promise; + map?: (result: V) => T | Promise; + initialState?: T | V | (() => T | V), + execute?: boolean; + onError?: (error: unknown) => void; + onData?: (data: T | V) => void; + onWillExecute?: (input: RequestInfo | URL, init?: RequestInit) => void; + }, +): AsyncState & { + revalidate: () => void; + mutate: MutatePromiseFn; +} { + const abortable = useRef(); + + return useCachedPromise( + async (inputParam: RequestInfo | URL): Promise => { + const response = await fetch(inputParam, { ...options?.request, signal: abortable.current?.signal }); + + if (options?.parse) { + const parsed: V = await options?.parse(response) + + if (options?.map) { + return options?.map(parsed) + } else { + return parsed + } + } else { + const content = response.headers.get("content-type"); + if (!content || !content.includes("application/json")) { + throw new Error("Content-Type is not 'application/json', please specify custom options.parse") + } + + const parsed: V = await response.json() + + if (options?.map) { + return options?.map(parsed) + } else { + return parsed + } + } + }, + [url], + { + initialState: options?.initialState, + abortable, + execute: options?.execute, + onError: options?.onError, + onData: options?.onData, + onWillExecute: options?.onWillExecute, + } + ) +} \ No newline at end of file From d350a3f05e3806dfbda8d8b55a0837f7f409c572 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 14 Sep 2024 14:49:48 +0200 Subject: [PATCH 058/540] Show Alt+K on bottom panel action button. Refine styling --- rust/client/src/ui/theme/container.rs | 4 ++ rust/client/src/ui/theme/mod.rs | 28 ++++---- rust/client/src/ui/widget.rs | 92 +++++++++++++++++---------- 3 files changed, 80 insertions(+), 44 deletions(-) diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index da4ed9b..b3e1805 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -44,6 +44,7 @@ pub enum ContainerStyle { PreferenceRequiredViewDescription, Root, RootBottomPanel, + RootBottomPanelActionButtonText, RootInner, RootTopPanel, Grid, @@ -352,6 +353,9 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau ContainerStyle::ListInner => { self.padding(theme.list_inner.padding.to_iced()) } + ContainerStyle::RootBottomPanelActionButtonText => { + self.padding(theme.root_bottom_panel_action_button_text.padding.to_iced()) + } }.into() } } diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index eb7f6a0..daeb0f5 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -24,7 +24,7 @@ mod loading_bar; pub type Element<'a, Message> = iced::Element<'a, Message, GauntletTheme>; -const CURRENT_COLOR_THEME_VERSION: u64 = 1; +const CURRENT_COLOR_THEME_VERSION: u64 = 2; const CURRENT_THEME_VERSION: u64 = 2; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -104,6 +104,7 @@ pub struct GauntletTheme { preference_required_view_description: ThemePaddingOnly, root_bottom_panel: ThemePaddingBackgroundColor, root_bottom_panel_action_button: ThemeButton, + root_bottom_panel_action_button_text: ThemePaddingOnly, root_content: ThemePaddingOnly, root_top_panel: ThemePaddingOnly, root_top_panel_button: ThemeButton, @@ -259,8 +260,8 @@ impl GauntletTheme { padding: padding_all(0.0) }, action_shortcut_modifier: ThemeActionShortcutModifier { - padding: padding_axis(0.0, 4.0), - spacing: 4.0, + padding: padding_axis(0.0, 8.0), + spacing: 8.0, background_color: background_lighter_color, border_radius: 4.0, border_width: 0.0, @@ -293,13 +294,6 @@ impl GauntletTheme { metadata_link_icon: ThemePaddingOnly { padding: padding_axis(0.0, 4.0), }, - root_bottom_panel: ThemePaddingBackgroundColor { - padding: padding_axis(4.0, 8.0), - background_color: background_overlay_color, - }, - root_top_panel: ThemePaddingOnly { - padding: padding_all(12.0), - }, list_item_subtitle: ThemePaddingTextColor { padding: padding_all(4.0), text_color: text_darker_color, @@ -350,6 +344,9 @@ impl GauntletTheme { metadata_separator: ThemePaddingOnly { padding: padding_axis(8.0, 0.0), }, + root_top_panel: ThemePaddingOnly { + padding: padding_all(12.0), + }, root_top_panel_button: ThemeButton { padding: padding_axis(3.0, 5.0), background_color: background_light_color, @@ -360,16 +357,23 @@ impl GauntletTheme { border_width: 0.0, border_color: TRANSPARENT, }, + root_bottom_panel: ThemePaddingBackgroundColor { + padding: padding_axis(6.0, 8.0), + background_color: background_overlay_color, + }, root_bottom_panel_action_button: ThemeButton { padding: padding_axis(3.0, 5.0), - background_color: background_light_color, - background_color_hovered: background_lighter_color, + background_color: TRANSPARENT, + background_color_hovered: background_light_color, text_color, text_color_hovered: text_color, border_radius: 6.0, border_width: 0.0, border_color: TRANSPARENT, }, + root_bottom_panel_action_button_text: ThemePaddingOnly { + padding: padding(0.0, 8.0, 0.0, 4.0), + }, list_item: ThemeButton { padding: padding_all(5.0), background_color: TRANSPARENT, diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 853a993..0ef09ab 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -17,7 +17,7 @@ use iced_aw::floating_element::Offset; use iced_aw::helpers::{date_picker, grid, grid_row, wrap_horizontal}; use itertools::Itertools; -use common::model::{PhysicalShortcut, PluginId, UiPropertyValue, UiPropertyValueToEnum, UiPropertyValueToStruct, UiWidgetId}; +use common::model::{PhysicalKey, PhysicalShortcut, PluginId, UiPropertyValue, UiPropertyValueToEnum, UiPropertyValueToStruct, UiWidgetId}; use common_ui::shortcut_to_text; use crate::model::UiViewEvent; @@ -242,36 +242,7 @@ impl ComponentWidgetWrapper { let shortcut_element: Option> = id.as_ref() .map(|id| action_shortcuts.get(id)) .flatten() - .map(|shortcut| { - let mut result = vec![]; - - let (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) = shortcut_to_text(shortcut); - - fn apply_modifier<'a, 'b>(result: &'a mut Vec>, modifier: Option>) { - if let Some(modifier) = modifier { - let modifier: Element<_> = container(modifier) - .themed(ContainerStyle::ActionShortcutModifier); - - let modifier: Element<_> = container(modifier) - .themed(ContainerStyle::ActionShortcutModifiersInit); - - result.push(modifier); - } - } - - apply_modifier(&mut result, meta_modifier_text); - apply_modifier(&mut result, control_modifier_text); - apply_modifier(&mut result, shift_modifier_text); - apply_modifier(&mut result, alt_modifier_text); - - let key_name: Element<_> = container(key_name) - .themed(ContainerStyle::ActionShortcutModifier); - - result.push(key_name); - - row(result) - .themed(RowStyle::ActionShortcut) - }); + .map(|shortcut| render_shortcut(shortcut)); let content: Element<_> = if let Some(shortcut_element) = shortcut_element { let text: Element<_> = text(title) @@ -1296,7 +1267,24 @@ fn render_root<'a>( .ok(); let (hide_action_panel, action_panel_element, bottom_panel) = if let Some(action_panel_element) = action_panel_element { - let action_panel_toggle: Element<_> = button(text("Actions")) + let actions_text: Element<_> = text("Actions") + .into(); + + let actions_text: Element<_> = container(actions_text) + .themed(ContainerStyle::RootBottomPanelActionButtonText); + + let shortcut = render_shortcut(&PhysicalShortcut { + physical_key: PhysicalKey::KeyK, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + }); + + let action_panel_toggle_content: Element<_> = row(vec![actions_text, shortcut]) + .into(); + + let action_panel_toggle: Element<_> = button(action_panel_toggle_content) .on_press(ComponentWidgetEvent::ToggleActionPanel { widget_id }) .themed(ButtonStyle::RootBottomPanelActionButton); @@ -1386,6 +1374,46 @@ fn render_text_part<'a>(value: &str, context: ComponentRenderContext) -> Element text.into() } +fn render_shortcut<'a>(shortcut: &PhysicalShortcut) -> Element<'a, ComponentWidgetEvent> { + let mut result = vec![]; + + let ( + key_name, + alt_modifier_text, + meta_modifier_text, + control_modifier_text, + shift_modifier_text + ) = shortcut_to_text(shortcut); + + fn apply_modifier<'result, 'element>( + result: &'result mut Vec>, + modifier: Option> + ) { + if let Some(modifier) = modifier { + let modifier: Element<_> = container(modifier) + .themed(ContainerStyle::ActionShortcutModifier); + + let modifier: Element<_> = container(modifier) + .themed(ContainerStyle::ActionShortcutModifiersInit); + + result.push(modifier); + } + } + + apply_modifier(&mut result, meta_modifier_text); + apply_modifier(&mut result, control_modifier_text); + apply_modifier(&mut result, shift_modifier_text); + apply_modifier(&mut result, alt_modifier_text); + + let key_name: Element<_> = container(key_name) + .themed(ContainerStyle::ActionShortcutModifier); + + result.push(key_name); + + row(result) + .themed(RowStyle::ActionShortcut) +} + fn render_children_string<'a>( content: &[ComponentWidgetWrapper], context: ComponentRenderContext From a6e056c060d2b866f2176240bcd5e9527a665046 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 14 Sep 2024 15:45:05 +0200 Subject: [PATCH 059/540] Updated numbat. Enable currency module --- Cargo.lock | 149 +++++++++++++------ rust/server/Cargo.toml | 2 +- rust/server/src/plugins/js/mod.rs | 11 +- rust/server/src/plugins/js/plugins/numbat.rs | 63 +++++--- 4 files changed, 158 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60a9004..8d0d889 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -463,15 +463,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "attohttpc" -version = "0.26.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f77d243921b0979fbbd728dd2d5162e68ac8252976797c24eb5b3a6af9090dc" +checksum = "184f5e6cce583a9db6b6f8d772a42cfce5b78e7c3ef26118cec3ce4c8c14969b" dependencies = [ - "http", + "http 1.1.0", "log", - "rustls 0.21.10", + "rustls 0.22.4", "url", - "webpki-roots 0.25.4", + "webpki-roots 0.26.3", ] [[package]] @@ -525,7 +525,7 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "http", + "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", "itoa", @@ -551,7 +551,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", + "http 0.2.12", "http-body 0.4.6", "mime", "rustversion", @@ -1013,7 +1013,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] @@ -1611,7 +1611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" dependencies = [ "bitflags 2.5.0", - "libloading 0.7.4", + "libloading 0.8.3", "winapi", ] @@ -1828,7 +1828,7 @@ dependencies = [ "deno_core", "deno_tls", "dyn-clone", - "http", + "http 0.2.12", "reqwest", "serde", "tokio", @@ -1890,7 +1890,7 @@ dependencies = [ "deno_websocket", "flate2", "fly-accept-encoding", - "http", + "http 0.2.12", "httparse", "hyper 0.14.28", "hyper 1.0.0-rc.4", @@ -2134,7 +2134,7 @@ dependencies = [ "filetime", "fs3", "fwdansi", - "http", + "http 0.2.12", "hyper 0.14.28", "libc", "log", @@ -2243,7 +2243,7 @@ dependencies = [ "deno_net", "deno_tls", "fastwebsockets", - "http", + "http 0.2.12", "hyper 0.14.28", "once_cell", "serde", @@ -2416,7 +2416,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.3", ] [[package]] @@ -2959,7 +2959,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3afa7516fdcfd8e5e93a938f8fec857785ced190a1f62d842d1fe1ffbe22ba8" dependencies = [ - "http", + "http 0.2.12", "itertools 0.10.5", "thiserror", ] @@ -3765,7 +3765,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap 2.2.6", "slab", "tokio", @@ -3826,7 +3826,7 @@ dependencies = [ "bitflags 2.5.0", "com", "libc", - "libloading 0.7.4", + "libloading 0.8.3", "thiserror", "widestring", "winapi", @@ -3920,6 +3920,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -3927,7 +3938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", "pin-project-lite", ] @@ -3938,7 +3949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "951dfc2e32ac02d67c90c0d65bd27009a635dc9b381a2cc7d284ab01e3a0150d" dependencies = [ "bytes", - "http", + "http 0.2.12", ] [[package]] @@ -3964,7 +3975,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", @@ -3987,7 +3998,7 @@ dependencies = [ "futures-channel", "futures-util", "h2", - "http", + "http 0.2.12", "http-body 1.0.0-rc.2", "httparse", "httpdate", @@ -4005,7 +4016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", + "http 0.2.12", "hyper 0.14.28", "rustls 0.21.10", "tokio", @@ -4535,15 +4546,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -4559,6 +4561,33 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jiff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a45489186a6123c128fdf6016183fcfab7113e1820eb813127e036e287233fb" +dependencies = [ + "jiff-tzdb-platform", + "js-sys", + "wasm-bindgen", + "windows-sys 0.59.0", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.21.1" @@ -5185,6 +5214,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mendeleev" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8312069eadd5b2cb9ff1bbd3874927d0cc60007ef608507cf8a59920b8a140fd" +dependencies = [ + "serde", +] + [[package]] name = "metal" version = "0.27.0" @@ -5569,22 +5607,27 @@ dependencies = [ [[package]] name = "numbat" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83726f560b5b5cedfe5121cb9087b25e09a1b55ea1fa9ae88733a554bd923dc" +checksum = "ad9d54727dbec99dde47300a528312ef872c9a198cdba705d1837b355a6cf24d" dependencies = [ "codespan-reporting", "heck 0.4.1", - "itertools 0.11.0", + "indexmap 2.2.6", + "itertools 0.12.1", + "jiff", "libc", + "mendeleev", "num-format", "num-integer", "num-rational", "num-traits", "numbat-exchange-rates", "pretty_dtoa", + "rand", "rust-embed", - "strsim 0.10.0", + "strfmt", + "strsim", "thiserror", "unicode-ident", "unicode-width", @@ -5593,9 +5636,9 @@ dependencies = [ [[package]] name = "numbat-exchange-rates" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6b00450c221baa0aa982e73bab4902cbf82f7bda8ab9b49aa497781a6340b" +checksum = "fd1e3c3e4f9f22d0d7cdcb413f01194f6506a302a9029d95deedcd1c25df7718" dependencies = [ "attohttpc", "quick-xml", @@ -6988,7 +7031,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", "hyper-rustls", @@ -7293,6 +7336,20 @@ dependencies = [ "sct", ] +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.5", + "subtle", + "zeroize", +] + [[package]] name = "rustls" version = "0.23.11" @@ -8311,6 +8368,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strfmt" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8348af2d9fc3258c8733b8d9d8db2e56f54b2363a4b5b81585c7875ed65e65" + [[package]] name = "strict-num" version = "0.1.1" @@ -8370,12 +8433,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -9401,7 +9458,7 @@ dependencies = [ "base64 0.21.7", "bytes", "h2", - "http", + "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", "hyper-timeout", @@ -10388,7 +10445,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.7.4", + "libloading 0.8.3", "log", "metal", "naga", diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 92cd34d..ba60e20 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -30,7 +30,7 @@ client = { path = "../client" } walkdir = "2.4.0" include_dir = "0.7.3" open = "5" -numbat = "1.9" +numbat = "1.13.0" uuid = "1.8" resvg = { version = "0.41", default-features = false} image = "0.25" diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index f385962..574d934 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -38,7 +38,7 @@ use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_r use crate::plugins::js::command_generators::get_command_generator_entrypoint_ids; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; use crate::plugins::js::plugins::applications::{list_applications, open_application}; -use crate::plugins::js::plugins::numbat::run_numbat; +use crate::plugins::js::plugins::numbat::{run_numbat, NumbatContext}; use crate::plugins::js::plugins::settings::open_settings; use crate::plugins::js::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; use crate::plugins::js::search::reload_search_index; @@ -334,6 +334,12 @@ async fn start_js_runtime( let core_url = "gauntlet:core".parse().expect("should be valid"); let unused_url = "gauntlet:unused".parse().expect("should be valid"); + let numbat_context = if plugin_id.to_string() == "builtin://calculator" { + Some(NumbatContext::new()) + } else { + None + }; + let mut worker = MainWorker::bootstrap_from_options( unused_url, permissions_container, @@ -347,6 +353,7 @@ async fn start_js_runtime( repository, search_index, icon_cache, + numbat_context )], // maybe_inspector_server: Some(inspector_server.clone()), // should_wait_for_inspector_session: true, @@ -534,6 +541,7 @@ deno_core::extension!( db_repository: DataDbRepository, search_index: SearchIndex, icon_cache: IconCache, + numbat_context: Option, }, state = |state, options| { state.put(options.event_receiver); @@ -543,6 +551,7 @@ deno_core::extension!( state.put(options.db_repository); state.put(options.search_index); state.put(options.icon_cache); + state.put(options.numbat_context); }, ); diff --git a/rust/server/src/plugins/js/plugins/numbat.rs b/rust/server/src/plugins/js/plugins/numbat.rs index f3e7830..66c89d5 100644 --- a/rust/server/src/plugins/js/plugins/numbat.rs +++ b/rust/server/src/plugins/js/plugins/numbat.rs @@ -1,12 +1,32 @@ -use numbat::InterpreterResult; -use numbat::markup::Formatter; +use anyhow::anyhow; +use deno_core::{op, OpState}; +use numbat::markup::{Formatter, PlainTextFormatter}; use numbat::module_importer::BuiltinModuleImporter; use numbat::pretty_print::PrettyPrint; -use numbat::value::Value; - -use anyhow::anyhow; -use deno_core::op; +use numbat::resolver::CodeSource; +use numbat::{Context, InterpreterResult}; use serde::Serialize; +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Clone)] +pub struct NumbatContext(Rc>); + +impl NumbatContext { + pub fn new() -> NumbatContext { + let mut context = Context::new(BuiltinModuleImporter::default()); + + context.load_currency_module_on_demand(true); + + if cfg!(feature = "release") { + Context::prefetch_exchange_rates(); + } + + let _ = context.interpret("use prelude", CodeSource::Internal); + + NumbatContext(Rc::new(RefCell::new(context))) + } +} #[derive(Debug, Serialize)] struct NumbatResult { @@ -15,15 +35,26 @@ struct NumbatResult { } #[op] -fn run_numbat(input: String) -> anyhow::Result { - // TODO add check for plugin id +fn run_numbat(state: Rc>, input: String) -> anyhow::Result { + let context = { + let state = state.borrow(); - let mut context = numbat::Context::new(BuiltinModuleImporter::default()); - let _ = context.interpret("use prelude", numbat::resolver::CodeSource::Internal); + let context = state + .borrow::>() + .clone(); - let (statements, result) = context.interpret(&input, numbat::resolver::CodeSource::Text)?; + context + }; - let formatter = numbat::markup::PlainTextFormatter; + let Some(context) = context else { + return Err(anyhow!("plugin id is not equal to 'builtin://calculator'")) + }; + + let mut context = context.0.borrow_mut(); + + let (statements, result) = context.interpret(&input, CodeSource::Text)?; + + let formatter = PlainTextFormatter; let expression = statements .iter() @@ -33,16 +64,10 @@ fn run_numbat(input: String) -> anyhow::Result { .replace('➞', "to"); let value = match result { - InterpreterResult::Value(value) => value, + InterpreterResult::Value(value) => format!("{}", value.pretty_print()), InterpreterResult::Continue => Err(anyhow!("numbat returned Continue"))? }; - let value = match value { - Value::Quantity(value) => value.to_string(), - Value::Boolean(value) => value.to_string(), - Value::String(value) => value, - }; - Ok(NumbatResult { left: expression, right: value From 1fe349db47c043cc2d8a1bee6bac3b448ce41810 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 14 Sep 2024 17:26:29 +0200 Subject: [PATCH 060/540] Add permissions for clipboard --- dev_plugin/gauntlet.toml | 3 +- rust/server/src/plugins/data_db_repository.rs | 20 +++++ rust/server/src/plugins/js/clipboard.rs | 86 +++++++++++++++++-- rust/server/src/plugins/js/mod.rs | 34 ++++++-- rust/server/src/plugins/loader.rs | 27 +++++- rust/server/src/plugins/mod.rs | 18 +++- tools | 2 +- 7 files changed, 172 insertions(+), 18 deletions(-) diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 9d36add..6b8018e 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -147,4 +147,5 @@ os = 'windows' [permissions] environment = ["RUST_LOG"] system = ["systemMemoryInfo"] -network = ["upload.wikimedia.org", "api.github.com"] \ No newline at end of file +network = ["upload.wikimedia.org", "api.github.com"] +clipboard = ["read", "write", "clear"] \ No newline at end of file diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index f447851..b868ced 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -114,14 +114,34 @@ pub enum DbPluginType { #[derive(Debug, Deserialize, Serialize)] pub struct DbPluginPermissions { + #[serde(default)] pub environment: Vec, + #[serde(default)] pub high_resolution_time: bool, + #[serde(default)] pub network: Vec, + #[serde(default)] pub ffi: Vec, + #[serde(default)] pub fs_read_access: Vec, + #[serde(default)] pub fs_write_access: Vec, + #[serde(default)] pub run_subprocess: Vec, + #[serde(default)] pub system: Vec, + #[serde(default)] + pub clipboard: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum DbPluginClipboardPermissions { + #[serde(rename = "read")] + Read, + #[serde(rename = "write")] + Write, + #[serde(rename = "clear")] + Clear } #[derive(Debug, Deserialize, Serialize)] diff --git a/rust/server/src/plugins/js/clipboard.rs b/rust/server/src/plugins/js/clipboard.rs index ec0b6d5..a013193 100644 --- a/rust/server/src/plugins/js/clipboard.rs +++ b/rust/server/src/plugins/js/clipboard.rs @@ -1,11 +1,13 @@ +use std::cell::RefCell; use std::io::Cursor; - +use std::rc::Rc; use anyhow::anyhow; use arboard::ImageData; -use deno_core::op; +use deno_core::{op, OpState}; use image::RgbaImage; use serde::{Deserialize, Serialize}; use tokio::task::spawn_blocking; +use crate::plugins::js::{PluginClipboardPermissions, PluginData}; fn unknown_err_clipboard(err: arboard::Error) -> anyhow::Error { anyhow!("UNKNOWN_ERROR: {:?}", err) @@ -26,7 +28,21 @@ struct ClipboardData { } #[op] -async fn clipboard_read() -> anyhow::Result { +async fn clipboard_read(state: Rc>) -> anyhow::Result { + { + let state = state.borrow(); + + let allow = state + .borrow::() + .permissions() + .clipboard + .contains(&PluginClipboardPermissions::Read); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); + } + } + spawn_blocking(|| { let mut clipboard = arboard::Clipboard::new() .map_err(|err| unknown_err_clipboard(err))?; @@ -74,7 +90,21 @@ async fn clipboard_read() -> anyhow::Result { #[op] -async fn clipboard_read_text() -> anyhow::Result> { +async fn clipboard_read_text(state: Rc>) -> anyhow::Result> { + { + let state = state.borrow(); + + let allow = state + .borrow::() + .permissions() + .clipboard + .contains(&PluginClipboardPermissions::Read); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); + } + } + spawn_blocking(|| { let mut clipboard = arboard::Clipboard::new() .map_err(|err| unknown_err_clipboard(err))?; @@ -96,7 +126,21 @@ async fn clipboard_read_text() -> anyhow::Result> { } #[op] -async fn clipboard_write(data: ClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2 +async fn clipboard_write(state: Rc>, data: ClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2 + { + let state = state.borrow(); + + let allow = state + .borrow::() + .permissions() + .clipboard + .contains(&PluginClipboardPermissions::Write); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); + } + } + spawn_blocking(|| { let mut clipboard = arboard::Clipboard::new() .map_err(|err| unknown_err_clipboard(err))?; @@ -134,7 +178,21 @@ async fn clipboard_write(data: ClipboardData) -> anyhow::Result<()> { // TODO de } #[op] -async fn clipboard_write_text(data: String) -> anyhow::Result<()> { +async fn clipboard_write_text(state: Rc>, data: String) -> anyhow::Result<()> { + { + let state = state.borrow(); + + let allow = state + .borrow::() + .permissions() + .clipboard + .contains(&PluginClipboardPermissions::Write); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); + } + } + spawn_blocking(|| { let mut clipboard = arboard::Clipboard::new() .map_err(|err| unknown_err_clipboard(err))?; @@ -147,7 +205,21 @@ async fn clipboard_write_text(data: String) -> anyhow::Result<()> { } #[op] -async fn clipboard_clear() -> anyhow::Result<()> { +async fn clipboard_clear(state: Rc>) -> anyhow::Result<()> { + { + let state = state.borrow(); + + let allow = state + .borrow::() + .permissions() + .clipboard + .contains(&PluginClipboardPermissions::Clear); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'clear' permission for clipboard")); + } + } + spawn_blocking(|| { let mut clipboard = arboard::Clipboard::new() .map_err(|err| unknown_err_clipboard(err))?; diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 574d934..c1c2c59 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -31,7 +31,7 @@ use component_model::{Children, Component, create_component_model, Property, Pro use crate::model::{IntermediateUiEvent, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData, JsUiWidget, PreferenceUserData}; use crate::plugins::applications::{DesktopEntry, get_apps}; -use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; +use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint, DbPluginClipboardPermissions}; use crate::plugins::icon_cache::IconCache; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; @@ -82,8 +82,22 @@ pub struct PluginPermissions { pub fs_write_access: Vec, pub run_subprocess: Vec, pub system: Vec, + pub clipboard: Vec, } +#[derive(Clone, Debug)] +pub struct PluginRuntimePermissions { + pub clipboard: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PluginClipboardPermissions { + Read, + Write, + Clear +} + + #[derive(Clone, Debug)] pub enum PluginCommand { One { @@ -340,6 +354,10 @@ async fn start_js_runtime( None }; + let runtime_permissions = PluginRuntimePermissions { + clipboard: permissions.clipboard, + }; + let mut worker = MainWorker::bootstrap_from_options( unused_url, permissions_container, @@ -347,7 +365,7 @@ async fn start_js_runtime( module_loader: Rc::new(CustomModuleLoader::new(code, dev_plugin)), extensions: vec![plugin_ext::init_ops_and_esm( EventReceiver::new(event_stream), - PluginData::new(plugin_id, plugin_uuid, inline_view_entrypoint_id), + PluginData::new(plugin_id, plugin_uuid, inline_view_entrypoint_id, runtime_permissions), frontend_api, ComponentModel::new(component_model), repository, @@ -682,15 +700,17 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { pub struct PluginData { plugin_id: PluginId, plugin_uuid: String, - inline_view_entrypoint_id: Option + inline_view_entrypoint_id: Option, + permissions: PluginRuntimePermissions } impl PluginData { - fn new(plugin_id: PluginId, plugin_uuid: String, inline_view_entrypoint_id: Option) -> Self { + fn new(plugin_id: PluginId, plugin_uuid: String, inline_view_entrypoint_id: Option, permissions: PluginRuntimePermissions) -> Self { Self { plugin_id, plugin_uuid, - inline_view_entrypoint_id + inline_view_entrypoint_id, + permissions } } @@ -705,6 +725,10 @@ impl PluginData { fn inline_view_entrypoint_id(&self) -> Option { self.inline_view_entrypoint_id.clone() } + + fn permissions(&self) -> &PluginRuntimePermissions { + &self.permissions + } } pub struct ComponentModel { diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index 332d64c..e25c333 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -14,7 +14,7 @@ use itertools::Itertools; use tracing_subscriber::fmt::format; use common::model::{DownloadStatus, PluginId}; use crate::model::ActionShortcutKey; -use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_to_str, db_plugin_type_to_str, DbCode, DbPluginAction, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbPluginType, DbPreferenceEnumValue, DbWritePlugin, DbWritePluginAssetData, DbWritePluginEntrypoint}; +use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_to_str, db_plugin_type_to_str, DbCode, DbPluginAction, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbPluginType, DbPreferenceEnumValue, DbWritePlugin, DbWritePluginAssetData, DbWritePluginEntrypoint, DbPluginClipboardPermissions}; use crate::plugins::download_status::DownloadStatusHolder; pub struct PluginLoader { @@ -321,6 +321,18 @@ impl PluginLoader { }) .collect(); + let clipboard = plugin_manifest.permissions + .clipboard + .into_iter() + .map(|permission| { + match permission { + PluginManifestClipboardPermissions::Read => DbPluginClipboardPermissions::Read, + PluginManifestClipboardPermissions::Write => DbPluginClipboardPermissions::Write, + PluginManifestClipboardPermissions::Clear => DbPluginClipboardPermissions::Clear, + } + }) + .collect(); + let permissions = DbPluginPermissions { environment: plugin_manifest.permissions.environment, high_resolution_time: plugin_manifest.permissions.high_resolution_time, @@ -330,6 +342,7 @@ impl PluginLoader { fs_write_access: plugin_manifest.permissions.fs_write_access, run_subprocess: plugin_manifest.permissions.run_subprocess, system: plugin_manifest.permissions.system, + clipboard, }; Ok(PluginDownloadData { @@ -814,4 +827,16 @@ pub struct PluginManifestPermissions { run_subprocess: Vec, #[serde(default)] system: Vec, + #[serde(default)] + clipboard: Vec, +} + +#[derive(Debug, Deserialize)] +pub enum PluginManifestClipboardPermissions { + #[serde(rename = "read")] + Read, + #[serde(rename = "write")] + Write, + #[serde(rename = "clear")] + Clear } diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 706ccf3..42a3d16 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -17,10 +17,10 @@ use utils::channel::RequestSender; use common::dirs::Dirs; use crate::model::ActionShortcutKey; use crate::plugins::config_reader::ConfigReader; -use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint}; +use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint, DbPluginClipboardPermissions}; use crate::plugins::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; use crate::plugins::icon_cache::IconCache; -use crate::plugins::js::{AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginPermissions, PluginRuntimeData, start_plugin_runtime}; +use crate::plugins::js::{AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginPermissions, PluginRuntimeData, start_plugin_runtime, PluginClipboardPermissions}; use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; use crate::search::SearchIndex; @@ -553,6 +553,17 @@ impl ApplicationManager { .await?; let receiver = self.command_broadcaster.subscribe(); + + let clipboard_permissions = plugin.permissions + .clipboard + .into_iter() + .map(|permission| match permission { + DbPluginClipboardPermissions::Read => PluginClipboardPermissions::Read, + DbPluginClipboardPermissions::Write => PluginClipboardPermissions::Write, + DbPluginClipboardPermissions::Clear => PluginClipboardPermissions::Clear, + }) + .collect(); + let data = PluginRuntimeData { id: plugin_id, uuid: plugin.uuid, @@ -566,7 +577,8 @@ impl ApplicationManager { fs_read_access: plugin.permissions.fs_read_access, fs_write_access: plugin.permissions.fs_write_access, run_subprocess: plugin.permissions.run_subprocess, - system: plugin.permissions.system + system: plugin.permissions.system, + clipboard: clipboard_permissions, }, command_receiver: receiver, db_repository: self.db_repository.clone(), diff --git a/tools b/tools index d85ac74..2370948 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit d85ac747727320956a20ee5d50505ad7fff61db0 +Subproject commit 23709487f8e98709fe3f5d03cb2a143a62026dca From ea9c795973368069fddbcae0d837c841e23087f2 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 14 Sep 2024 20:03:31 +0200 Subject: [PATCH 061/540] Require main_search_bar permission when entrypoint type 'inline-view' is used --- bundled_plugins/calculator/gauntlet.toml | 2 ++ dev_plugin/gauntlet.toml | 3 +- rust/server/src/plugins/data_db_repository.rs | 8 +++++ rust/server/src/plugins/js/mod.rs | 6 ++++ rust/server/src/plugins/loader.rs | 34 ++++++++++++++++++- rust/server/src/plugins/mod.rs | 13 +++++-- tools | 2 +- 7 files changed, 63 insertions(+), 5 deletions(-) diff --git a/bundled_plugins/calculator/gauntlet.toml b/bundled_plugins/calculator/gauntlet.toml index 678c27b..410c557 100644 --- a/bundled_plugins/calculator/gauntlet.toml +++ b/bundled_plugins/calculator/gauntlet.toml @@ -9,3 +9,5 @@ path = 'src/default.tsx' type = 'inline-view' description = '' +[permissions] +main_search_bar = ["read"] diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 6b8018e..1c66244 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -148,4 +148,5 @@ os = 'windows' environment = ["RUST_LOG"] system = ["systemMemoryInfo"] network = ["upload.wikimedia.org", "api.github.com"] -clipboard = ["read", "write", "clear"] \ No newline at end of file +clipboard = ["read", "write", "clear"] +main_search_bar = ["read"] \ No newline at end of file diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index b868ced..646bf5c 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -132,6 +132,8 @@ pub struct DbPluginPermissions { pub system: Vec, #[serde(default)] pub clipboard: Vec, + #[serde(default)] + pub main_search_bar: Vec, } #[derive(Debug, Deserialize, Serialize)] @@ -144,6 +146,12 @@ pub enum DbPluginClipboardPermissions { Clear } +#[derive(Debug, Deserialize, Serialize)] +pub enum DbPluginMainSearchBarPermissions { + #[serde(rename = "read")] + Read, +} + #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type")] pub enum DbPluginPreferenceUserData { diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index c1c2c59..6bf4b0b 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -83,6 +83,7 @@ pub struct PluginPermissions { pub run_subprocess: Vec, pub system: Vec, pub clipboard: Vec, + pub main_search_bar: Vec, } #[derive(Clone, Debug)] @@ -97,6 +98,11 @@ pub enum PluginClipboardPermissions { Clear } +#[derive(Clone, Debug)] +pub enum PluginMainSearchBarPermissions { + Read, +} + #[derive(Clone, Debug)] pub enum PluginCommand { diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index e25c333..61c7c29 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -14,7 +14,7 @@ use itertools::Itertools; use tracing_subscriber::fmt::format; use common::model::{DownloadStatus, PluginId}; use crate::model::ActionShortcutKey; -use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_to_str, db_plugin_type_to_str, DbCode, DbPluginAction, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbPluginType, DbPreferenceEnumValue, DbWritePlugin, DbWritePluginAssetData, DbWritePluginEntrypoint, DbPluginClipboardPermissions}; +use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_to_str, db_plugin_type_to_str, DbCode, DbPluginAction, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbPluginType, DbPreferenceEnumValue, DbWritePlugin, DbWritePluginAssetData, DbWritePluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions}; use crate::plugins::download_status::DownloadStatusHolder; pub struct PluginLoader { @@ -242,6 +242,18 @@ impl PluginLoader { } } + let has_inline_view = plugin_manifest.entrypoint + .iter() + .find(|entrypoint| matches!(entrypoint.entrypoint_type, PluginManifestEntrypointTypes::InlineView)) + .is_some(); + + if has_inline_view { + let main_search_bar = &permissions.main_search_bar; + if !main_search_bar.contains(&PluginManifestMainSearchBarPermissions::Read) { + return Err(anyhow!("Plugin uses entrypoint type 'inline-view' but doesn't specify main search bar 'read' permission")) + } + } + let plugin_name = plugin_manifest.gauntlet.name; let plugin_description = plugin_manifest.gauntlet.description; @@ -333,6 +345,16 @@ impl PluginLoader { }) .collect(); + let main_search_bar = plugin_manifest.permissions + .main_search_bar + .into_iter() + .map(|permission| { + match permission { + PluginManifestMainSearchBarPermissions::Read => DbPluginMainSearchBarPermissions::Read, + } + }) + .collect(); + let permissions = DbPluginPermissions { environment: plugin_manifest.permissions.environment, high_resolution_time: plugin_manifest.permissions.high_resolution_time, @@ -343,6 +365,7 @@ impl PluginLoader { run_subprocess: plugin_manifest.permissions.run_subprocess, system: plugin_manifest.permissions.system, clipboard, + main_search_bar, }; Ok(PluginDownloadData { @@ -829,6 +852,8 @@ pub struct PluginManifestPermissions { system: Vec, #[serde(default)] clipboard: Vec, + #[serde(default)] + main_search_bar: Vec, } #[derive(Debug, Deserialize)] @@ -840,3 +865,10 @@ pub enum PluginManifestClipboardPermissions { #[serde(rename = "clear")] Clear } + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub enum PluginManifestMainSearchBarPermissions { + #[serde(rename = "read")] + Read, +} + diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 42a3d16..7f4c05e 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -17,10 +17,10 @@ use utils::channel::RequestSender; use common::dirs::Dirs; use crate::model::ActionShortcutKey; use crate::plugins::config_reader::ConfigReader; -use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint, DbPluginClipboardPermissions}; +use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions}; use crate::plugins::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; use crate::plugins::icon_cache::IconCache; -use crate::plugins::js::{AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginPermissions, PluginRuntimeData, start_plugin_runtime, PluginClipboardPermissions}; +use crate::plugins::js::{AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginPermissions, PluginRuntimeData, start_plugin_runtime, PluginClipboardPermissions, PluginMainSearchBarPermissions}; use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; use crate::search::SearchIndex; @@ -564,6 +564,14 @@ impl ApplicationManager { }) .collect(); + let main_search_bar_permissions = plugin.permissions + .main_search_bar + .into_iter() + .map(|permission| match permission { + DbPluginMainSearchBarPermissions::Read => PluginMainSearchBarPermissions::Read, + }) + .collect(); + let data = PluginRuntimeData { id: plugin_id, uuid: plugin.uuid, @@ -579,6 +587,7 @@ impl ApplicationManager { run_subprocess: plugin.permissions.run_subprocess, system: plugin.permissions.system, clipboard: clipboard_permissions, + main_search_bar: main_search_bar_permissions }, command_receiver: receiver, db_repository: self.db_repository.clone(), diff --git a/tools b/tools index 2370948..0506c37 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit 23709487f8e98709fe3f5d03cb2a143a62026dca +Subproject commit 0506c376f15a220e571e5ef573e94781ad789077 From 29649aae51e4dacf46b30e726542d57764745d2d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 15 Sep 2024 10:50:16 +0200 Subject: [PATCH 062/540] Split preference name into id and name --- dev_plugin/gauntlet.toml | 24 ++++-- rust/common/src/model.rs | 7 ++ rust/common/src/rpc/backend_api.rs | 4 +- rust/common/src/rpc/backend_server.rs | 6 +- rust/common/src/rpc/grpc_convert.rs | 46 +++++++---- rust/management_client/src/views/plugins.rs | 14 ++-- .../src/views/plugins/preferences.rs | 54 ++++++------ rust/server/src/plugins/data_db_repository.rs | 13 ++- rust/server/src/plugins/loader.rs | 43 ++++++---- rust/server/src/plugins/mod.rs | 82 +++++++++++++++---- rust/server/src/rpc.rs | 4 +- schema/backend.proto | 3 +- tools | 2 +- 13 files changed, 197 insertions(+), 105 deletions(-) diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 1c66244..9511ee9 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -5,7 +5,8 @@ A reasonably long plugin description that doesn't contain any usefull informatio """ [[preferences]] -name = 'testBool' +id = 'testBool' +name = 'Test Boolean' type = 'bool' default = true description = "test bool description" @@ -31,42 +32,49 @@ description = "test action description 2" shortcut = { key = 'B', kind = 'main'} [[entrypoint.preferences]] -name = 'testBool' +id = 'testBool' +name = 'Test Boolean' type = 'bool' default = true description = "test bool description" [[entrypoint.preferences]] -name = 'testEnum' +id = 'testEnum' +name = 'Test Enum' type = 'enum' default = 'item' enum_values = [{ label = 'Item', value = 'item'}, { label = 'Item 2', value = 'item_2'}] description = "test enum description" [[entrypoint.preferences]] -name = 'testListOfStrings' +id = 'testListOfStrings' +name = 'Test List of Strings' type = 'list_of_strings' description = "test list of strings description" [[entrypoint.preferences]] -name = 'testListOfNumbers' +id = 'testListOfNumbers' +name = 'Test List of Numbers' type = 'list_of_numbers' description = "test list of numbers description" [[entrypoint.preferences]] -name = 'testListOfEnums' +id = 'testListOfEnums' +name = 'Test List of Enums' type = 'list_of_enums' description = "test list of enums description" enum_values = [{ label = 'Item', value = 'item'}, { label = 'Item 2', value = 'item_2'}] [[entrypoint.preferences]] -name = 'testNum' +id = 'testNum' +name = 'Test Num' type = 'number' default = 2 description = "test number description" [[entrypoint.preferences]] -name = 'testStr' +id = 'testStr' +name = 'Test Str' type = 'string' default = 'test_value' description = "test string description" diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index ad3fa54..ccb89fc 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -325,31 +325,38 @@ pub enum PluginPreferenceUserData { #[derive(Debug, Clone)] pub enum PluginPreference { Number { + name: String, default: Option, description: String, }, String { + name: String, default: Option, description: String, }, Enum { + name: String, default: Option, description: String, enum_values: Vec, }, Bool { + name: String, default: Option, description: String, }, ListOfStrings { + name: String, default: Option>, description: String, }, ListOfNumbers { + name: String, default: Option>, description: String, }, ListOfEnums { + name: String, default: Option>, enum_values: Vec, description: String, diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index c448d82..8686cb8 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -365,11 +365,11 @@ impl BackendApi { }) } - pub async fn set_preference_value(&mut self, plugin_id: PluginId, entrypoint_id: Option, name: String, user_data: PluginPreferenceUserData) -> Result<(), BackendApiError> { + pub async fn set_preference_value(&mut self, plugin_id: PluginId, entrypoint_id: Option, id: String, user_data: PluginPreferenceUserData) -> Result<(), BackendApiError> { let request = RpcSetPreferenceValueRequest { plugin_id: plugin_id.to_string(), entrypoint_id: entrypoint_id.map(|id| id.to_string()).unwrap_or_default(), - preference_name: name, + preference_id: id, preference_value: Some(plugin_preference_user_data_to_rpc(user_data)), }; diff --git a/rust/common/src/rpc/backend_server.rs b/rust/common/src/rpc/backend_server.rs index 747bae5..997b573 100644 --- a/rust/common/src/rpc/backend_server.rs +++ b/rust/common/src/rpc/backend_server.rs @@ -79,7 +79,7 @@ pub trait BackendServer { &self, plugin_id: PluginId, entrypoint_id: Option, - preference_name: String, + preference_id: String, preference_value: PluginPreferenceUserData ) -> anyhow::Result<()>; @@ -203,10 +203,10 @@ impl RpcBackend for RpcBackendServerImpl { Some(EntrypointId::from_string(request.entrypoint_id)) }; - let preference_name = request.preference_name; + let preference_id = request.preference_id; let preference_value = request.preference_value.unwrap(); - self.server.set_preference_value(plugin_id, entrypoint_id, preference_name, plugin_preference_user_data_from_rpc(preference_value)) + self.server.set_preference_value(plugin_id, entrypoint_id, preference_id, plugin_preference_user_data_from_rpc(preference_value)) .await .map_err(|err| Status::internal(format!("{:#}", err)))?; diff --git a/rust/common/src/rpc/grpc_convert.rs b/rust/common/src/rpc/grpc_convert.rs index 78e6762..ca5f46a 100644 --- a/rust/common/src/rpc/grpc_convert.rs +++ b/rust/common/src/rpc/grpc_convert.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use anyhow::anyhow; -use crate::model::{EntrypointId, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, UiPropertyValue, SearchResult, SearchResultEntrypointType, UiWidget}; -use crate::rpc::grpc::{rpc_ui_property_value, RpcEntrypointTypeSearchResult, RpcEnumValue, RpcPluginPreference, RpcPluginPreferenceUserData, RpcPluginPreferenceValueType, RpcSearchResult, RpcUiPropertyValue, RpcUiPropertyValueObject, RpcUiWidget, RpcUiWidgetId}; +use crate::model::{EntrypointId, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SearchResultEntrypointType, UiPropertyValue, UiWidget}; use crate::rpc::grpc::rpc_ui_property_value::Value; +use crate::rpc::grpc::{RpcEntrypointTypeSearchResult, RpcEnumValue, RpcPluginPreference, RpcPluginPreferenceUserData, RpcPluginPreferenceValueType, RpcSearchResult, RpcUiPropertyValue, RpcUiPropertyValueObject, RpcUiWidget, RpcUiWidgetId}; pub(crate) fn ui_widget_to_rpc(value: UiWidget) -> RpcUiWidget { let children = value.widget_children.into_iter() @@ -276,26 +276,29 @@ pub fn plugin_preference_user_data_to_rpc(value: PluginPreferenceUserData) -> Rp pub fn plugin_preference_to_rpc(value: PluginPreference) -> RpcPluginPreference { match value { - PluginPreference::Number { default, description } => { + PluginPreference::Number { name, default, description } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::Number.into(), - default: default.map(|value| RpcUiPropertyValue { value: Some(rpc_ui_property_value::Value::Number(value)) }), + default: default.map(|value| RpcUiPropertyValue { value: Some(Value::Number(value)) }), + name, description, ..RpcPluginPreference::default() } } - PluginPreference::String { default, description } => { + PluginPreference::String { name, default, description } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::String.into(), - default: default.map(|value| RpcUiPropertyValue { value: Some(rpc_ui_property_value::Value::String(value)) }), + default: default.map(|value| RpcUiPropertyValue { value: Some(Value::String(value)) }), + name, description, ..RpcPluginPreference::default() } } - PluginPreference::Enum { default, description, enum_values } => { + PluginPreference::Enum { name, default, description, enum_values } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::Enum.into(), - default: default.map(|value| RpcUiPropertyValue { value: Some(rpc_ui_property_value::Value::String(value)) }), + default: default.map(|value| RpcUiPropertyValue { value: Some(Value::String(value)) }), + name, description, enum_values: enum_values.into_iter() .map(|value| RpcEnumValue { label: value.label, value: value.value }) @@ -303,34 +306,38 @@ pub fn plugin_preference_to_rpc(value: PluginPreference) -> RpcPluginPreference ..RpcPluginPreference::default() } } - PluginPreference::Bool { default, description } => { + PluginPreference::Bool { name, default, description } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::Bool.into(), - default: default.map(|value| RpcUiPropertyValue { value: Some(rpc_ui_property_value::Value::Bool(value)) }), + default: default.map(|value| RpcUiPropertyValue { value: Some(Value::Bool(value)) }), + name, description, ..RpcPluginPreference::default() } } - PluginPreference::ListOfStrings { default, description } => { + PluginPreference::ListOfStrings { name, default, description } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::ListOfStrings.into(), - default_list: default.map(|value| value.into_iter().map(|value| RpcUiPropertyValue { value: Some(rpc_ui_property_value::Value::String(value)) }).collect()).unwrap_or(vec![]), + default_list: default.map(|value| value.into_iter().map(|value| RpcUiPropertyValue { value: Some(Value::String(value)) }).collect()).unwrap_or(vec![]), + name, description, ..RpcPluginPreference::default() } } - PluginPreference::ListOfNumbers { default, description } => { + PluginPreference::ListOfNumbers { name, default, description } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::ListOfNumbers.into(), - default_list: default.map(|value| value.into_iter().map(|value| RpcUiPropertyValue { value: Some(rpc_ui_property_value::Value::Number(value)) }).collect()).unwrap_or(vec![]), + default_list: default.map(|value| value.into_iter().map(|value| RpcUiPropertyValue { value: Some(Value::Number(value)) }).collect()).unwrap_or(vec![]), + name, description, ..RpcPluginPreference::default() } } - PluginPreference::ListOfEnums { default, enum_values, description } => { + PluginPreference::ListOfEnums { name, default, enum_values, description } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::ListOfEnums.into(), - default_list: default.map(|value| value.into_iter().map(|value| RpcUiPropertyValue { value: Some(rpc_ui_property_value::Value::String(value)) }).collect()).unwrap_or(vec![]), + default_list: default.map(|value| value.into_iter().map(|value| RpcUiPropertyValue { value: Some(Value::String(value)) }).collect()).unwrap_or(vec![]), + name, description, enum_values: enum_values.into_iter() .map(|value| RpcEnumValue { label: value.label, value: value.value }) @@ -355,6 +362,7 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc PluginPreference::Number { default, + name: value.name, description: value.description, } } @@ -369,6 +377,7 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc PluginPreference::String { default, + name: value.name, description: value.description, } } @@ -383,6 +392,7 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc PluginPreference::Enum { default, + name: value.name, description: value.description, enum_values: value.enum_values.into_iter() .map(|value| PreferenceEnumValue { label: value.label, value: value.value }) @@ -400,6 +410,7 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc PluginPreference::Bool { default, + name: value.name, description: value.description, } } @@ -423,6 +434,7 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc PluginPreference::ListOfStrings { default: default_list, + name: value.name, description: value.description, } } @@ -446,6 +458,7 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc PluginPreference::ListOfNumbers { default: default_list, + name: value.name, description: value.description, } } @@ -469,6 +482,7 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc PluginPreference::ListOfEnums { default: default_list, + name: value.name, enum_values: value.enum_values.into_iter() .map(|value| PreferenceEnumValue { label: value.label, value: value.value }) .collect(), diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index ead70a7..8beca8d 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -152,15 +152,15 @@ impl ManagementAppPluginsState { } ManagementAppPluginMsgIn::PluginPreferenceMsg(msg) => { match msg { - PluginPreferencesMsg::UpdatePreferenceValue { plugin_id, entrypoint_id, name, user_data } => { + PluginPreferencesMsg::UpdatePreferenceValue { plugin_id, entrypoint_id, id, user_data } => { self.preference_user_data - .insert((plugin_id.clone(), entrypoint_id.clone(), name.clone()), user_data.clone()); + .insert((plugin_id.clone(), entrypoint_id.clone(), id.clone()), user_data.clone()); let mut backend_api = backend_api.clone(); Command::perform( async move { - backend_api.set_preference_value(plugin_id, entrypoint_id, name, user_data.to_user_data()) + backend_api.set_preference_value(plugin_id, entrypoint_id, id, user_data.to_user_data()) .await?; Ok(()) @@ -228,13 +228,13 @@ impl ManagementAppPluginsState { .map(|(plugin_id, plugin)| { let mut result = vec![]; - for (name, user_data) in &plugin.preferences_user_data { - result.push(((plugin_id.clone(), None, name.clone()), PluginPreferenceUserDataState::from_user_data(user_data.clone()))) + for (id, user_data) in &plugin.preferences_user_data { + result.push(((plugin_id.clone(), None, id.clone()), PluginPreferenceUserDataState::from_user_data(user_data.clone()))) } for (entrypoint_id, entrypoint) in &plugin.entrypoints { - for (name, user_data) in &entrypoint.preferences_user_data { - result.push(((plugin_id.clone(), Some(entrypoint_id.clone()), name.clone()), PluginPreferenceUserDataState::from_user_data(user_data.clone()))) + for (id, user_data) in &entrypoint.preferences_user_data { + result.push(((plugin_id.clone(), Some(entrypoint_id.clone()), id.clone()), PluginPreferenceUserDataState::from_user_data(user_data.clone()))) } } diff --git a/rust/management_client/src/views/plugins/preferences.rs b/rust/management_client/src/views/plugins/preferences.rs index c5d11ea..faa0f0c 100644 --- a/rust/management_client/src/views/plugins/preferences.rs +++ b/rust/management_client/src/views/plugins/preferences.rs @@ -15,7 +15,7 @@ pub enum PluginPreferencesMsg { UpdatePreferenceValue { plugin_id: PluginId, entrypoint_id: Option, - name: String, + id: String, user_data: PluginPreferenceUserDataState }, } @@ -46,13 +46,25 @@ pub fn preferences_ui<'a>( preferences.sort_by_key(|(&ref key, _)| key); - for (preference_name, preference) in preferences { + for (preference_id, preference) in preferences { let plugin_id = plugin_id.clone(); let entrypoint_id = entrypoint_id.clone(); - let user_data = preference_user_data.get(&(plugin_id.clone(), entrypoint_id.clone(), preference_name.clone())); + let user_data = preference_user_data.get(&(plugin_id.clone(), entrypoint_id.clone(), preference_id.clone())); + let (preference_name, description) = match preference { + PluginPreference::Number { name, description, .. } => (name, description), + PluginPreference::String { name, description, .. } => (name, description), + PluginPreference::Enum { name, description, .. } => (name, description), + PluginPreference::Bool { name, description, .. } => (name, description), + PluginPreference::ListOfStrings { name, description, .. } => (name, description), + PluginPreference::ListOfNumbers { name, description, .. } => (name, description), + PluginPreference::ListOfEnums { name, description, .. } => (name, description), + }; + + let preference_id = preference_id.to_owned(); let preference_name = preference_name.to_owned(); + let description = description.to_owned(); let preference_label: Element<_> = text(&preference_name) .size(14) @@ -65,16 +77,6 @@ pub fn preferences_ui<'a>( column_content.push(preference_label); - let description = match preference { - PluginPreference::Number { description, .. } => description, - PluginPreference::String { description, .. } => description, - PluginPreference::Enum { description, .. } => description, - PluginPreference::Bool { description, .. } => description, - PluginPreference::ListOfStrings { description, .. } => description, - PluginPreference::ListOfNumbers { description, .. } => description, - PluginPreference::ListOfEnums { description, .. } => description, - }; - if !description.trim().is_empty() { let description = container(text(description)) .padding(Padding::from([4.0, 8.0])) @@ -102,7 +104,7 @@ pub fn preferences_ui<'a>( PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::Number { value: Some(value), }, @@ -130,7 +132,7 @@ pub fn preferences_ui<'a>( PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::String { value: Some(value), }, @@ -166,7 +168,7 @@ pub fn preferences_ui<'a>( Box::new(move |item: SelectItem| PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::Enum { value: Some(item.value), }, @@ -194,7 +196,7 @@ pub fn preferences_ui<'a>( PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::Bool { value: Some(value), }, @@ -239,7 +241,7 @@ pub fn preferences_ui<'a>( .on_press(PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::ListOfStrings { value, new_value: new_value.clone(), @@ -279,7 +281,7 @@ pub fn preferences_ui<'a>( Some(PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::ListOfStrings { value: Some(save_value), new_value: "".to_owned(), @@ -305,7 +307,7 @@ pub fn preferences_ui<'a>( .on_input(move |new_value| PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::ListOfStrings { value: value.clone(), new_value, @@ -356,7 +358,7 @@ pub fn preferences_ui<'a>( .on_press(PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::ListOfNumbers { value, new_value: new_value.clone(), @@ -399,7 +401,7 @@ pub fn preferences_ui<'a>( .on_press(PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::ListOfNumbers { value: Some(save_value), new_value: 0.0, @@ -421,7 +423,7 @@ pub fn preferences_ui<'a>( PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::ListOfNumbers { value: value.clone(), new_value, @@ -471,7 +473,7 @@ pub fn preferences_ui<'a>( .on_press(PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::ListOfEnums { value, new_value: new_value.clone(), @@ -511,7 +513,7 @@ pub fn preferences_ui<'a>( Some(PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::ListOfEnums { value: Some(save_value), new_value: None, @@ -545,7 +547,7 @@ pub fn preferences_ui<'a>( Box::new(move |new_value: SelectItem| PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), - name: preference_name.to_owned(), + id: preference_id.to_owned(), user_data: PluginPreferenceUserDataState::ListOfEnums { value: value.clone(), new_value: Some(new_value), diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 646bf5c..187412f 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -231,37 +231,44 @@ pub enum DbPluginActionShortcutKind { pub enum DbPluginPreference { #[serde(rename = "number")] Number { + name: Option, // option for db backwards compatibility, in settings id will be shown default: Option, description: String, }, #[serde(rename = "string")] String { + name: Option, default: Option, description: String, }, #[serde(rename = "enum")] Enum { + name: Option, default: Option, description: String, enum_values: Vec, }, #[serde(rename = "bool")] Bool { + name: Option, default: Option, description: String, }, #[serde(rename = "list_of_strings")] ListOfStrings { + name: Option, default: Option>, description: String, }, #[serde(rename = "list_of_numbers")] ListOfNumbers { + name: Option, default: Option>, description: String, }, #[serde(rename = "list_of_enums")] ListOfEnums { + name: Option, default: Option>, enum_values: Vec, description: String, @@ -793,7 +800,7 @@ impl DataDbRepository { } } - pub async fn set_preference_value(&self, plugin_id: String, entrypoint_id: Option, user_data_name: String, user_data_value: DbPluginPreferenceUserData) -> anyhow::Result<()> { + pub async fn set_preference_value(&self, plugin_id: String, entrypoint_id: Option, preference_id: String, value: DbPluginPreferenceUserData) -> anyhow::Result<()> { let mut tx = self.pool.begin().await?; match entrypoint_id { @@ -802,7 +809,7 @@ impl DataDbRepository { .await? .preferences_user_data; - user_data.insert(user_data_name, user_data_value); + user_data.insert(preference_id, value); // language=SQLite sqlx::query("UPDATE plugin SET preferences_user_data = ?1 WHERE id = ?2") @@ -816,7 +823,7 @@ impl DataDbRepository { .await? .preferences_user_data; - user_data.insert(user_data_name, user_data_value); + user_data.insert(preference_id, value); // language=SQLite sqlx::query("UPDATE plugin_entrypoint SET preferences_user_data = ?1 WHERE id = ?2 AND plugin_id = ?3") diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index 61c7c29..81fbe7f 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -273,24 +273,24 @@ impl PluginLoader { preferences: entrypoint.preferences .into_iter() .map(|preference| match preference { - PluginManifestPreference::Number { name, default, description } => (name, DbPluginPreference::Number { default, description }), - PluginManifestPreference::String { name, default, description } => (name, DbPluginPreference::String { default, description }), - PluginManifestPreference::Enum { name, default, description, enum_values } => { + PluginManifestPreference::Number { id, name, default, description } => (id, DbPluginPreference::Number { name: Some(name), default, description }), + PluginManifestPreference::String { id, name, default, description } => (id, DbPluginPreference::String { name: Some(name), default, description }), + PluginManifestPreference::Enum { id, name, default, description, enum_values } => { let enum_values = enum_values.into_iter() .map(|PluginManifestPreferenceEnumValue { label, value } | DbPreferenceEnumValue { label, value }) .collect(); - (name, DbPluginPreference::Enum { default, description, enum_values }) + (id, DbPluginPreference::Enum { name: Some(name), default, description, enum_values }) }, - PluginManifestPreference::Bool { name, default, description } => (name, DbPluginPreference::Bool { default, description }), - PluginManifestPreference::ListOfStrings { name, description } => (name, DbPluginPreference::ListOfStrings { default: None, description }), - PluginManifestPreference::ListOfNumbers { name, description } => (name, DbPluginPreference::ListOfNumbers { default: None, description }), - PluginManifestPreference::ListOfEnums { name, description, enum_values } => { + PluginManifestPreference::Bool { id, name, default, description } => (id, DbPluginPreference::Bool { name: Some(name), default, description }), + PluginManifestPreference::ListOfStrings { id, name, description } => (id, DbPluginPreference::ListOfStrings { name: Some(name), default: None, description }), + PluginManifestPreference::ListOfNumbers { id, name, description } => (id, DbPluginPreference::ListOfNumbers { name: Some(name), default: None, description }), + PluginManifestPreference::ListOfEnums { id, name, description, enum_values } => { let enum_values = enum_values.into_iter() .map(|PluginManifestPreferenceEnumValue { label, value } | DbPreferenceEnumValue { label, value }) .collect(); - (name, DbPluginPreference::ListOfEnums { default: None, description, enum_values }) + (id, DbPluginPreference::ListOfEnums { name: Some(name), default: None, description, enum_values }) }, }) .collect(), @@ -311,24 +311,24 @@ impl PluginLoader { let plugin_preferences = plugin_manifest.preferences .into_iter() .map(|preference| match preference { - PluginManifestPreference::Number { name, default, description } => (name, DbPluginPreference::Number { default, description }), - PluginManifestPreference::String { name, default, description } => (name, DbPluginPreference::String { default, description }), - PluginManifestPreference::Enum { name, default, description, enum_values } => { + PluginManifestPreference::Number { id, name, default, description } => (id, DbPluginPreference::Number { name: Some(name), default, description }), + PluginManifestPreference::String { id, name, default, description } => (id, DbPluginPreference::String { name: Some(name), default, description }), + PluginManifestPreference::Enum { id, name, default, description, enum_values } => { let enum_values = enum_values.into_iter() .map(|PluginManifestPreferenceEnumValue { label, value } | DbPreferenceEnumValue { label, value }) .collect(); - (name, DbPluginPreference::Enum { default, description, enum_values }) + (id, DbPluginPreference::Enum { name: Some(name), default, description, enum_values }) }, - PluginManifestPreference::Bool { name, default, description } => (name, DbPluginPreference::Bool { default, description }), - PluginManifestPreference::ListOfStrings { name, description } => (name, DbPluginPreference::ListOfStrings { default: None, description }), - PluginManifestPreference::ListOfNumbers { name, description } => (name, DbPluginPreference::ListOfNumbers { default: None, description }), - PluginManifestPreference::ListOfEnums { name, description, enum_values } => { + PluginManifestPreference::Bool { id, name, default, description } => (id, DbPluginPreference::Bool { name: Some(name), default, description }), + PluginManifestPreference::ListOfStrings { id, name, description } => (id, DbPluginPreference::ListOfStrings { name: Some(name), default: None, description }), + PluginManifestPreference::ListOfNumbers { id, name, description } => (id, DbPluginPreference::ListOfNumbers { name: Some(name), default: None, description }), + PluginManifestPreference::ListOfEnums { id, name, description, enum_values } => { let enum_values = enum_values.into_iter() .map(|PluginManifestPreferenceEnumValue { label, value } | DbPreferenceEnumValue { label, value }) .collect(); - (name, DbPluginPreference::ListOfEnums { default: None, description, enum_values }) + (id, DbPluginPreference::ListOfEnums { name: Some(name), default: None, description, enum_values }) }, }) .collect(); @@ -429,18 +429,21 @@ struct PluginManifestEntrypoint { enum PluginManifestPreference { #[serde(rename = "number")] Number { + id: String, name: String, default: Option, description: String, }, #[serde(rename = "string")] String { + id: String, name: String, default: Option, description: String, }, #[serde(rename = "enum")] Enum { + id: String, name: String, default: Option, description: String, @@ -448,24 +451,28 @@ enum PluginManifestPreference { }, #[serde(rename = "bool")] Bool { + id: String, name: String, default: Option, description: String, }, #[serde(rename = "list_of_strings")] ListOfStrings { + id: String, name: String, // default: Option>, description: String, }, #[serde(rename = "list_of_numbers")] ListOfNumbers { + id: String, name: String, // default: Option>, description: String, }, #[serde(rename = "list_of_enums")] ListOfEnums { + id: String, name: String, // default: Option>, enum_values: Vec, diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 7f4c05e..76df4e7 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -179,7 +179,10 @@ impl ApplicationManager { DbPluginEntrypointType::CommandGenerator => SettingsEntrypointType::CommandGenerator, }.into(), preferences: entrypoint.preferences.into_iter() - .map(|(key, value)| (key, plugin_preference_from_db(value))) + .map(|(key, value)| { + let preference = plugin_preference_from_db(&key, value); + (key, preference) + }) .collect(), preferences_user_data: entrypoint.preferences_user_data.into_iter() .map(|(key, value)| (key, plugin_preference_user_data_from_db(value))) @@ -197,7 +200,10 @@ impl ApplicationManager { enabled: plugin.enabled, entrypoints, preferences: plugin.preferences.into_iter() - .map(|(key, value)| (key, plugin_preference_from_db(value))) + .map(|(key, value)| { + let preference = plugin_preference_from_db(&key, value); + (key, preference) + }) .collect(), preferences_user_data: plugin.preferences_user_data.into_iter() .map(|(key, value)| (key, plugin_preference_user_data_from_db(value))) @@ -264,10 +270,10 @@ impl ApplicationManager { self.db_repository.get_global_shortcut().await } - pub async fn set_preference_value(&self, plugin_id: PluginId, entrypoint_id: Option, preference_name: String, preference_value: PluginPreferenceUserData) -> anyhow::Result<()> { + pub async fn set_preference_value(&self, plugin_id: PluginId, entrypoint_id: Option, preference_id: String, preference_value: PluginPreferenceUserData) -> anyhow::Result<()> { let user_data = plugin_preference_user_data_to_db(preference_value); - self.db_repository.set_preference_value(plugin_id.to_string(), entrypoint_id.map(|id| id.to_string()), preference_name, user_data) + self.db_repository.set_preference_value(plugin_id.to_string(), entrypoint_id.map(|id| id.to_string()), preference_id, user_data) .await?; Ok(()) @@ -635,26 +641,66 @@ impl ApplicationManager { } } -fn plugin_preference_from_db(value: DbPluginPreference) -> PluginPreference { +fn plugin_preference_from_db(id: &str, value: DbPluginPreference) -> PluginPreference { match value { - DbPluginPreference::Number { default, description } => PluginPreference::Number { default, description }, - DbPluginPreference::String { default, description } => PluginPreference::String { default, description }, - DbPluginPreference::Enum { default, description, enum_values } => { - let enum_values = enum_values.into_iter() - .map(|value| PreferenceEnumValue { label: value.label, value: value.value }) - .collect(); - - PluginPreference::Enum { default, description, enum_values } + DbPluginPreference::Number { name, default, description } => { + PluginPreference::Number { + name: name.unwrap_or_else(|| id.to_string()), + default, + description + } }, - DbPluginPreference::Bool { default, description } => PluginPreference::Bool { default, description }, - DbPluginPreference::ListOfStrings { default, description } => PluginPreference::ListOfStrings { default, description }, - DbPluginPreference::ListOfNumbers { default, description } => PluginPreference::ListOfNumbers { default, description }, - DbPluginPreference::ListOfEnums { default, enum_values, description } => { + DbPluginPreference::String { name, default, description } => { + PluginPreference::String { + name: name.unwrap_or_else(|| id.to_string()), + default, + description + } + }, + DbPluginPreference::Enum { name, default, description, enum_values } => { let enum_values = enum_values.into_iter() .map(|value| PreferenceEnumValue { label: value.label, value: value.value }) .collect(); - PluginPreference::ListOfEnums { default, enum_values, description } + PluginPreference::Enum { + name: name.unwrap_or_else(|| id.to_string()), + default, + description, + enum_values + } + }, + DbPluginPreference::Bool { name, default, description } => { + PluginPreference::Bool { + name: name.unwrap_or_else(|| id.to_string()), + default, + description + } + }, + DbPluginPreference::ListOfStrings { name, default, description } => { + PluginPreference::ListOfStrings { + name: name.unwrap_or_else(|| id.to_string()), + default, + description + } + }, + DbPluginPreference::ListOfNumbers { name, default, description } => { + PluginPreference::ListOfNumbers { + name: name.unwrap_or_else(|| id.to_string()), + default, + description + } + }, + DbPluginPreference::ListOfEnums { name, default, enum_values, description } => { + let enum_values = enum_values.into_iter() + .map(|value| PreferenceEnumValue { label: value.label, value: value.value }) + .collect(); + + PluginPreference::ListOfEnums { + name: name.unwrap_or_else(|| id.to_string()), + default, + enum_values, + description + } }, } } diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index 060b51c..df3bc5b 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -85,8 +85,8 @@ impl BackendServer for BackendServerImpl { Ok(result) } - async fn set_preference_value(&self, plugin_id: PluginId, entrypoint_id: Option, preference_name: String, preference_value: PluginPreferenceUserData) -> anyhow::Result<()> { - let result = self.application_manager.set_preference_value(plugin_id, entrypoint_id, preference_name, preference_value) + async fn set_preference_value(&self, plugin_id: PluginId, entrypoint_id: Option, preference_id: String, preference_value: PluginPreferenceUserData) -> anyhow::Result<()> { + let result = self.application_manager.set_preference_value(plugin_id, entrypoint_id, preference_id, preference_value) .await; if let Err(err) = &result { diff --git a/schema/backend.proto b/schema/backend.proto index 7418b77..f9f1074 100644 --- a/schema/backend.proto +++ b/schema/backend.proto @@ -93,7 +93,7 @@ message RpcGetGlobalShortcutResponse { message RpcSetPreferenceValueRequest { string plugin_id = 1; string entrypoint_id = 2; - string preference_name = 3; + string preference_id = 3; RpcPluginPreferenceUserData preference_value = 4; } message RpcSetPreferenceValueResponse { @@ -215,6 +215,7 @@ message RpcPluginPreference { bool default_list_exists = 4; string description = 5; repeated RpcEnumValue enum_values = 6; + string name = 7; } message RpcEnumValue { diff --git a/tools b/tools index 0506c37..e441fa0 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit 0506c376f15a220e571e5ef573e94781ad789077 +Subproject commit e441fa089a42f2bb4192a2fe7de0d8e3b7bb2f32 From 8c8ade8a89886265505b096cbd0d15038c068e70 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 15 Sep 2024 11:44:27 +0200 Subject: [PATCH 063/540] Replace onSelectionChange and id on List or Grid itself with onClick on item --- dev_plugin/src/grid-view.tsx | 10 +- dev_plugin/src/hooks-view.tsx | 270 ++++++------------ dev_plugin/src/list-view.tsx | 22 +- dev_plugin/src/test-list-detail.tsx | 20 +- docs/js/components/grid_item/props/id.md | 1 - .../props/onClick.md} | 3 +- .../list/props/onSelectionChange.md | 2 - docs/js/components/list_item/props/id.md | 1 - docs/js/components/list_item/props/onClick.md | 1 + js/api/src/gen/components.tsx | 20 +- rust/client/src/ui/widget.rs | 38 +-- rust/component_model/src/lib.rs | 10 +- 12 files changed, 134 insertions(+), 264 deletions(-) delete mode 100644 docs/js/components/grid_item/props/id.md rename docs/js/components/{grid/props/onSelectionChange.md => grid_item/props/onClick.md} (52%) delete mode 100644 docs/js/components/list/props/onSelectionChange.md delete mode 100644 docs/js/components/list_item/props/id.md create mode 100644 docs/js/components/list_item/props/onClick.md diff --git a/dev_plugin/src/grid-view.tsx b/dev_plugin/src/grid-view.tsx index 1180676..f051a2f 100644 --- a/dev_plugin/src/grid-view.tsx +++ b/dev_plugin/src/grid-view.tsx @@ -11,10 +11,10 @@ export default function GridView(): ReactElement { const [val4, setValue4] = useStorage("test", ""); return ( - {}}> + { numbers.map(value => ( - + Test Paragraph {value} @@ -24,7 +24,7 @@ export default function GridView(): ReactElement { )) } - + Test Paragraph Section 1 1 @@ -33,14 +33,14 @@ export default function GridView(): ReactElement { - + Test Paragraph Section 2 1 - + Test Paragraph Section 2 2 diff --git a/dev_plugin/src/hooks-view.tsx b/dev_plugin/src/hooks-view.tsx index babc285..5a0852c 100644 --- a/dev_plugin/src/hooks-view.tsx +++ b/dev_plugin/src/hooks-view.tsx @@ -6,77 +6,20 @@ export default function ListView(): ReactElement { const { pushView } = useNavigation(); return ( - { - switch (id) { - case "UsePromiseTestBasic": { - pushView() - break; - } - case "UsePromiseTestExecuteFalse": { - pushView() - break; - } - case "UsePromiseTestRevalidate": { - pushView() - break; - } - case "UsePromiseTestAbortableRevalidate": { - pushView() - break; - } - case "UsePromiseTestMutate": { - pushView() - break; - } - case "UsePromiseTestMutateOptimistic": { - pushView() - break; - } - case "UsePromiseTestMutateOptimisticRollback": { - pushView() - break; - } - case "UsePromiseTestMutateNoRevalidate": { - pushView() - break; - } - case "UsePromiseTestThrow": { - pushView() - break; - } - case "UseCachedPromiseBasic": { - pushView() - break; - } - case "UseCachedPromiseInitialState": { - pushView() - break; - } - case "UseFetchBasic": { - pushView() - break; - } - case "UseFetchMap": { - pushView() - break; - } - } - }} - > - - - - - - - - - - - - - + + pushView()}/> + pushView()}/> + pushView()}/> + pushView()}/> + pushView()}/> + pushView()}/> + pushView()}/> + pushView()}/> + pushView()}/> + pushView()}/> + pushView()}/> + pushView()}/> + pushView()}/> ) } @@ -91,12 +34,9 @@ function UsePromiseTestBasic(): ReactElement { printState(data, error, isLoading) return ( - + - + popView()}/> ) @@ -112,12 +52,9 @@ function UseCachedPromiseBasic(): ReactElement { printState(data, error, isLoading) return ( - + - + popView()}/> ) @@ -136,12 +73,9 @@ function UseCachedPromiseInitialState(): ReactElement { printState(data, error, isLoading) return ( - + - + popView()}/> ) @@ -160,12 +94,9 @@ function UsePromiseTestExecuteFalse(): ReactElement { printState(data, error, isLoading) return ( - + - + popView()}/> ) @@ -182,13 +113,10 @@ function UsePromiseTestRevalidate(): ReactElement { printState(data, error, isLoading) return ( - revalidate())} - > + - - + revalidate()}/> + popView()}/> ) @@ -211,13 +139,10 @@ function UsePromiseTestAbortableRevalidate(): ReactElement { printState(data, error, isLoading) return ( - revalidate())} - > + - - + revalidate()}/> + popView()}/> ) @@ -232,16 +157,14 @@ function UsePromiseTestMutate(): ReactElement { printState(data, error, isLoading) + const onClick = async () => { + await mutate(inNSec(5)) + }; return ( - { - await mutate(inNSec(5)) - })} - > + - - + + popView()}/> ) @@ -256,21 +179,20 @@ function UsePromiseTestMutateOptimistic(): ReactElement { printState(data, error, isLoading) + const onClick = async () => { + await mutate( + inNSec(5), + { + optimisticUpdate: data1 => data1 + " optimistic", + } + ) + }; + return ( - { - await mutate( - inNSec(5), - { - optimisticUpdate: data1 => data1 + " optimistic", - } - ) - })} - > + - - + + popView()}/> ) @@ -285,29 +207,28 @@ function UsePromiseTestMutateOptimisticRollback(): ReactElement { printState(data, error, isLoading) - return ( - { - await mutate( - new Promise((_resolve, reject) => { - setTimeout( - () => { - reject("fail") - }, - 5 * 1000 - ); - }), - { - optimisticUpdate: data1 => data1 + " optimistic", - rollbackOnError: data1 => data1 + " failed", - } + const onClick = async () => { + await mutate( + new Promise((_resolve, reject) => { + setTimeout( + () => { + reject("fail") + }, + 5 * 1000 ); - })} - > + }), + { + optimisticUpdate: data1 => data1 + " optimistic", + rollbackOnError: data1 => data1 + " failed", + } + ); + }; + + return ( + - - + + popView()}/> ) @@ -323,20 +244,17 @@ function UsePromiseTestMutateNoRevalidate(): ReactElement { printState(data, error, isLoading) return ( - { - await mutate( - inNSec(5), - { - shouldRevalidateAfter: false, - } - ) - })} - > + - - + async () => { + await mutate( + inNSec(5), + { + shouldRevalidateAfter: false, + } + ) + }}/> + popView()}/> ) @@ -354,12 +272,9 @@ function UsePromiseTestThrow(): ReactElement { printState(data, error, isLoading) return ( - + - + popView()}/> ) @@ -379,12 +294,9 @@ function UseFetchBasic(): ReactElement { printState(data, error, isLoading) return ( - + - + popView()}/> ) @@ -406,32 +318,14 @@ function UseFetchMap(): ReactElement { printState(data, error, isLoading) return ( - + - + popView()}/> ) } -function onSelectionChangeHandler(popView: () => void, handler?: () => void): (id: string) => void { - return (id) => { - switch (id) { - case "back": { - popView() - break; - } - case "run": { - handler?.() - break; - } - } - } -} - async function inNSec(n: number): Promise { return new Promise(resolve => { setTimeout( diff --git a/dev_plugin/src/list-view.tsx b/dev_plugin/src/list-view.tsx index ac0f04a..225962d 100644 --- a/dev_plugin/src/list-view.tsx +++ b/dev_plugin/src/list-view.tsx @@ -5,25 +5,25 @@ export default function ListView(): ReactElement { const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; const [id, setId] = useState("default"); + const onClick = () => { + console.log("onClick " + id) + setId(id); + }; + return ( - { - console.log("onSelectionChange " + id) - setId(id); - }} - > + { numbers.map(value => ( - + )) } - + - - - + + + ) diff --git a/dev_plugin/src/test-list-detail.tsx b/dev_plugin/src/test-list-detail.tsx index 72d6a6b..d5d5854 100644 --- a/dev_plugin/src/test-list-detail.tsx +++ b/dev_plugin/src/test-list-detail.tsx @@ -4,16 +4,16 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - - - - - - - - - - + + + + + + + + + + Sentient diff --git a/docs/js/components/grid_item/props/id.md b/docs/js/components/grid_item/props/id.md deleted file mode 100644 index eb10e7e..0000000 --- a/docs/js/components/grid_item/props/id.md +++ /dev/null @@ -1 +0,0 @@ -Identifier of the grid item. Used in the grid event functions \ No newline at end of file diff --git a/docs/js/components/grid/props/onSelectionChange.md b/docs/js/components/grid_item/props/onClick.md similarity index 52% rename from docs/js/components/grid/props/onSelectionChange.md rename to docs/js/components/grid_item/props/onClick.md index cc61cc0..4463e3d 100644 --- a/docs/js/components/grid/props/onSelectionChange.md +++ b/docs/js/components/grid_item/props/onClick.md @@ -1,2 +1 @@ -Function that will be called when user selects an item on the grid. -The only parameter is an id of the selected grid item \ No newline at end of file +Function that will be called when user selects an item on the grid. \ No newline at end of file diff --git a/docs/js/components/list/props/onSelectionChange.md b/docs/js/components/list/props/onSelectionChange.md deleted file mode 100644 index 2d2edf7..0000000 --- a/docs/js/components/list/props/onSelectionChange.md +++ /dev/null @@ -1,2 +0,0 @@ -Function that will be called when user selects an item on the list. -The only parameter is an id of the selected list item \ No newline at end of file diff --git a/docs/js/components/list_item/props/id.md b/docs/js/components/list_item/props/id.md deleted file mode 100644 index 3c98fc0..0000000 --- a/docs/js/components/list_item/props/id.md +++ /dev/null @@ -1 +0,0 @@ -Identifier of the list item. Used in the list event functions \ No newline at end of file diff --git a/docs/js/components/list_item/props/onClick.md b/docs/js/components/list_item/props/onClick.md new file mode 100644 index 0000000..4463e3d --- /dev/null +++ b/docs/js/components/list_item/props/onClick.md @@ -0,0 +1 @@ +Function that will be called when user selects an item on the grid. \ No newline at end of file diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index 410f2d2..046ff65 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -124,10 +124,10 @@ declare global { image?: ImageSource | Icons; }; ["gauntlet:list_item"]: { - id: string; title: string; subtitle?: string; icon?: ImageSource | Icons; + onClick?: () => void; }; ["gauntlet:list_section"]: { children?: ElementComponent; @@ -137,13 +137,12 @@ declare global { ["gauntlet:list"]: { children?: ElementComponent; isLoading?: boolean; - onSelectionChange?: (id: string) => void; }; ["gauntlet:grid_item"]: { children?: ElementComponent; - id: string; title: string; subtitle?: string; + onClick?: () => void; }; ["gauntlet:grid_section"]: { children?: ElementComponent; @@ -155,7 +154,6 @@ declare global { children?: ElementComponent; isLoading?: boolean; columns?: number; - onSelectionChange?: (id: string) => void; }; } } @@ -637,13 +635,13 @@ export const EmptyView: FC = (props: EmptyViewProps): ReactNode return ; }; export interface ListItemProps { - id: string; title: string; subtitle?: string; icon?: ImageSource | Icons; + onClick?: () => void; } export const ListItem: FC = (props: ListItemProps): ReactNode => { - return ; + return ; }; export interface ListSectionProps { children?: ElementComponent; @@ -660,7 +658,6 @@ export interface ListProps { children?: ElementComponent; actions?: ElementComponent; isLoading?: boolean; - onSelectionChange?: (id: string) => void; } export const List: FC & { EmptyView: typeof EmptyView; @@ -668,7 +665,7 @@ export const List: FC & { Item: typeof ListItem; Section: typeof ListSection; } = (props: ListProps): ReactNode => { - return {props.actions as any}{props.children}; + return {props.actions as any}{props.children}; }; List.EmptyView = EmptyView; List.Detail = Detail; @@ -676,14 +673,14 @@ List.Item = ListItem; List.Section = ListSection; export interface GridItemProps { children?: ElementComponent; - id: string; title: string; subtitle?: string; + onClick?: () => void; } export const GridItem: FC & { Content: typeof Content; } = (props: GridItemProps): ReactNode => { - return {props.children}; + return {props.children}; }; GridItem.Content = Content; export interface GridSectionProps { @@ -703,14 +700,13 @@ export interface GridProps { isLoading?: boolean; actions?: ElementComponent; columns?: number; - onSelectionChange?: (id: string) => void; } export const Grid: FC & { EmptyView: typeof EmptyView; Item: typeof GridItem; Section: typeof GridSection; } = (props: GridProps): ReactNode => { - return {props.actions as any}{props.children}; + return {props.actions as any}{props.children}; }; Grid.EmptyView = EmptyView; Grid.Item = GridItem; diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 0ef09ab..abd28a7 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -883,11 +883,7 @@ impl ComponentWidgetWrapper { .center_y() .into() } - ComponentWidget::ListItem { id, title, subtitle, icon } => { - let ComponentRenderContext::List { widget_id: list_widget_id } = context else { - panic!("not supposed to be passed to list item: {:?}", context) - }; - + ComponentWidget::ListItem { title, subtitle, icon } => { let icon: Option> = icon.as_ref() .map(|icon| { match icon { @@ -931,7 +927,7 @@ impl ComponentWidgetWrapper { .into(); button(content) - .on_press(ComponentWidgetEvent::SelectListItem { list_widget_id, item_id: id.to_owned() }) + .on_press(ComponentWidgetEvent::ListItemClick { widget_id }) .width(Length::Fill) .themed(ButtonStyle::ListItem) } @@ -1031,11 +1027,7 @@ impl ComponentWidgetWrapper { render_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } - ComponentWidget::GridItem { children, id, title, subtitle } => { - let ComponentRenderContext::Grid { widget_id: grid_widget_id } = context else { - panic!("not supposed to be passed to grid item: {:?}", context) - }; - + ComponentWidget::GridItem { children, title, subtitle } => { let content: Element<_> = column(render_children(children, ComponentRenderContext::GridItem)) .height(130) // TODO dynamic height .into(); @@ -1055,7 +1047,7 @@ impl ComponentWidgetWrapper { .into(); let content: Element<_> = button(content) - .on_press(ComponentWidgetEvent::SelectGridItem { grid_widget_id, item_id: id.to_owned() }) + .on_press(ComponentWidgetEvent::GridItemClick { widget_id }) .themed(ButtonStyle::GridItem); content @@ -1521,13 +1513,11 @@ pub enum ComponentWidgetEvent { ToggleActionPanel { widget_id: UiWidgetId, }, - SelectListItem { - list_widget_id: UiWidgetId, - item_id: String, + ListItemClick { + widget_id: UiWidgetId, }, - SelectGridItem { - grid_widget_id: UiWidgetId, - item_id: String, + GridItemClick { + widget_id: UiWidgetId, }, PreviousView, } @@ -1639,11 +1629,11 @@ impl ComponentWidgetEvent { None } - ComponentWidgetEvent::SelectListItem { list_widget_id, item_id } => { - Some(create_list_on_selection_change_event(list_widget_id, item_id)) + ComponentWidgetEvent::ListItemClick { widget_id } => { + Some(create_list_item_on_click_event(widget_id)) } - ComponentWidgetEvent::SelectGridItem { grid_widget_id, item_id } => { - Some(create_grid_on_selection_change_event(grid_widget_id, item_id)) + ComponentWidgetEvent::GridItemClick { widget_id } => { + Some(create_grid_item_on_click_event(widget_id)) } ComponentWidgetEvent::PreviousView => { panic!("handle event on PreviousView event is not supposed to be called") @@ -1664,8 +1654,8 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::OnChangeTextField { widget_id, .. } => widget_id, ComponentWidgetEvent::OnChangePasswordField { widget_id, .. } => widget_id, ComponentWidgetEvent::ToggleActionPanel { widget_id } => widget_id, - ComponentWidgetEvent::SelectListItem { list_widget_id, .. } => list_widget_id, - ComponentWidgetEvent::SelectGridItem { grid_widget_id, .. } => grid_widget_id, + ComponentWidgetEvent::ListItemClick { widget_id, .. } => widget_id, + ComponentWidgetEvent::GridItemClick { widget_id, .. } => widget_id, ComponentWidgetEvent::PreviousView => panic!("widget_id on PreviousView event is not supposed to be called"), }.to_owned() } diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index de3e8b8..223632a 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -925,11 +925,11 @@ pub fn create_component_model() -> Vec { mark_doc!("/list_item/description.md"), "ListItem", [ - property("id", mark_doc!("/list_item/props/id.md"),false, PropertyType::String), property("title", mark_doc!("/list_item/props/title.md"),false, PropertyType::String), property("subtitle", mark_doc!("/list_item/props/subtitle.md"),true, PropertyType::String), property("icon", mark_doc!("/list_item/props/icon.md"),true, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] }), // accessories + event("onClick", mark_doc!("/list_item/props/onClick.md"), true, []) ], children_none(), ); @@ -954,9 +954,6 @@ pub fn create_component_model() -> Vec { [ property("actions", mark_doc!("/list/props/actions.md"), true, component_ref(&action_panel_component)), property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), - event("onSelectionChange", mark_doc!("/list/props/onSelectionChange.md"), true, [ - property("id", "".to_string(), false, PropertyType::String) - ]), ], children_members([ member("EmptyView", &empty_view_component), @@ -971,10 +968,10 @@ pub fn create_component_model() -> Vec { mark_doc!("/grid_item/description.md"), "GridItem", [ - property("id", mark_doc!("/grid_item/props/id.md"), false, PropertyType::String), property("title", mark_doc!("/grid_item/props/title.md"), false, PropertyType::String), property("subtitle", mark_doc!("/grid_item/props/subtitle.md"), true, PropertyType::String), // accessories + event("onClick", mark_doc!("/grid_item/props/onClick.md"), true, []) ], children_members([ member("Content", &content_component), @@ -1009,9 +1006,6 @@ pub fn create_component_model() -> Vec { property("columns", mark_doc!("/grid/props/columns.md"),true, PropertyType::Number), // TODO default // fit // inset - event("onSelectionChange", mark_doc!("/grid/props/onSelectionChange.md"), true, [ - property("id", "".to_string(), false, PropertyType::String) - ]), ], children_members([ member("EmptyView", &empty_view_component), From dd8e754a26fd273979d5632341ea4bdfe3202dfc Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 15 Sep 2024 12:45:22 +0200 Subject: [PATCH 064/540] Include more directories in macOS application search --- rust/server/src/plugins/applications/macos.rs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/rust/server/src/plugins/applications/macos.rs b/rust/server/src/plugins/applications/macos.rs index b9417a0..0792db6 100644 --- a/rust/server/src/plugins/applications/macos.rs +++ b/rust/server/src/plugins/applications/macos.rs @@ -16,9 +16,10 @@ pub fn get_apps() -> Vec { let core_services_applications = get_applications_in_dir(PathBuf::from("/System/Library/CoreServices/Applications")); - let user_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::User); - let local_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::Local); - let system_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::Domain); + // these are covered by recursion + // let user_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::User); + // let local_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::Local); + // let system_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::Domain); let user_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::User); let local_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::Local); @@ -28,9 +29,9 @@ pub fn get_apps() -> Vec { finder_application, finder_applications, core_services_applications, - user_admin_applications_dir, - local_admin_applications_dir, - system_admin_applications_dir, + // user_admin_applications_dir, + // local_admin_applications_dir, + // system_admin_applications_dir, user_applications_dir, local_applications_dir, system_applications_dir @@ -72,6 +73,8 @@ fn get_applications_with_kind(file_manager: &FileManager, directory: SearchPathD let applications_dir = url.to_file_path() .expect("returned application url is not a file path"); + tracing::debug!("reading {:?} {:?} directory: {:?}", directory, mask, &applications_dir); + get_applications_in_dir(applications_dir) } Err(err) => { @@ -90,7 +93,17 @@ fn get_applications_in_dir(path: PathBuf) -> Vec { .unwrap_or_default() .into_iter() .map(|entry| entry.path()) - .filter(|entry_path| entry_path.is_dir() && entry_path.extension() == Some(OsStr::new("app"))) + .flat_map(|entry_path| { + if entry_path.is_dir() { + if entry_path.extension() == Some(OsStr::new("app")) { + vec![entry_path] + } else { + get_applications_in_dir(entry_path) + } + } else { + vec![] + } + }) .collect::>() } Err(_) => vec![] From ffd91f0b8532f0db52ea7e1da629ca81248cc1d1 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 15 Sep 2024 14:39:27 +0200 Subject: [PATCH 065/540] Extend macOS Application plugin with System settings items like Sound, Display, etc --- rust/server/src/plugins/applications/macos.rs | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/rust/server/src/plugins/applications/macos.rs b/rust/server/src/plugins/applications/macos.rs index 0792db6..405188d 100644 --- a/rust/server/src/plugins/applications/macos.rs +++ b/rust/server/src/plugins/applications/macos.rs @@ -16,6 +16,10 @@ pub fn get_apps() -> Vec { let core_services_applications = get_applications_in_dir(PathBuf::from("/System/Library/CoreServices/Applications")); + let user_pref_panes_dir = get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::User); + let local_pref_panes_dir = get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Local); + let system_pref_panes_dir = get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Domain); + // these are covered by recursion // let user_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::User); // let local_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::Local); @@ -29,6 +33,9 @@ pub fn get_apps() -> Vec { finder_application, finder_applications, core_services_applications, + user_pref_panes_dir, + local_pref_panes_dir, + system_pref_panes_dir, // user_admin_applications_dir, // local_admin_applications_dir, // system_admin_applications_dir, @@ -67,15 +74,35 @@ pub fn get_apps() -> Vec { all_applications } +fn get_pref_panes_with_kind(file_manager: &FileManager, directory: SearchPathDirectory, mask: SearchPathDomainMask) -> Vec { + get_items_with_kind(file_manager, directory, mask, Some("PreferencePanes"), |dir| get_pref_panes_in_dir(dir)) +} + fn get_applications_with_kind(file_manager: &FileManager, directory: SearchPathDirectory, mask: SearchPathDomainMask) -> Vec { + get_items_with_kind(file_manager, directory, mask, None, |dir| get_applications_in_dir(dir)) +} + +fn get_items_with_kind( + file_manager: &FileManager, + directory: SearchPathDirectory, + mask: SearchPathDomainMask, + suffix: Option<&'static str>, + read_fn: F +) -> Vec where F: Fn(PathBuf) -> Vec +{ match file_manager.get_directory(directory.clone(), mask.clone()) { Ok(url) => { let applications_dir = url.to_file_path() .expect("returned application url is not a file path"); + let applications_dir = match suffix { + Some(suffix) => applications_dir.join(suffix), + None => applications_dir + }; + tracing::debug!("reading {:?} {:?} directory: {:?}", directory, mask, &applications_dir); - get_applications_in_dir(applications_dir) + read_fn(applications_dir) } Err(err) => { tracing::error!("error reading {:?} {:?} directory: {:?}", directory, mask, err); @@ -85,7 +112,15 @@ fn get_applications_with_kind(file_manager: &FileManager, directory: SearchPathD } } +fn get_pref_panes_in_dir(path: PathBuf) -> Vec { + get_items_in_dir(path, "prefPane") +} + fn get_applications_in_dir(path: PathBuf) -> Vec { + get_items_in_dir(path, "app") +} + +fn get_items_in_dir(path: PathBuf, extension: &str) -> Vec { match path.read_dir() { Ok(read_dir) => { read_dir @@ -95,10 +130,10 @@ fn get_applications_in_dir(path: PathBuf) -> Vec { .map(|entry| entry.path()) .flat_map(|entry_path| { if entry_path.is_dir() { - if entry_path.extension() == Some(OsStr::new("app")) { + if entry_path.extension() == Some(OsStr::new(extension)) { vec![entry_path] } else { - get_applications_in_dir(entry_path) + get_items_in_dir(entry_path, extension) } } else { vec![] From 0caea4f4562caa31241de0c6f1664bf8edea27de Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 15 Sep 2024 17:56:05 +0200 Subject: [PATCH 066/540] Support macOS 13+ system extension settings --- rust/server/src/plugins/applications/macos.rs | 178 ++++++++++++++++-- 1 file changed, 166 insertions(+), 12 deletions(-) diff --git a/rust/server/src/plugins/applications/macos.rs b/rust/server/src/plugins/applications/macos.rs index 405188d..b504edd 100644 --- a/rust/server/src/plugins/applications/macos.rs +++ b/rust/server/src/plugins/applications/macos.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::error::Error; use std::ffi::OsStr; use std::path::{Path, PathBuf}; @@ -5,44 +6,56 @@ use cacao::filesystem::{FileManager, SearchPathDirectory, SearchPathDomainMask}; use cacao::url::Url; use deno_runtime::deno_http::compressible::is_content_compressible; use plist::Dictionary; +use regex::Regex; use serde::Deserialize; use crate::plugins::applications::{DesktopEntry, resize_icon}; pub fn get_apps() -> Vec { let file_manager = FileManager::default(); + let all_items = [ + get_settings(&file_manager), + get_applications(&file_manager) + ]; + + all_items + .into_iter() + .flatten() + .collect() +} + +fn get_applications(file_manager: &FileManager) -> Vec { + let finder_application = vec![PathBuf::from("/System/Library/CoreServices/Finder.app")]; let finder_applications = get_applications_in_dir(PathBuf::from("/System/Library/CoreServices/Finder.app/Contents/Applications")); let core_services_applications = get_applications_in_dir(PathBuf::from("/System/Library/CoreServices/Applications")); - let user_pref_panes_dir = get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::User); - let local_pref_panes_dir = get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Local); - let system_pref_panes_dir = get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Domain); - - // these are covered by recursion + // these are covered by recursion on SearchPathDirectory::Applications // let user_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::User); // let local_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::Local); // let system_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::Domain); - let user_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::User); - let local_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::Local); - let system_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::Domain); + let user_applications_dir = get_applications_with_kind(file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::User); + let local_applications_dir = get_applications_with_kind(file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::Local); + let system_applications_dir = get_applications_with_kind(file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::Domain); let all_applications = [ finder_application, finder_applications, core_services_applications, - user_pref_panes_dir, - local_pref_panes_dir, - system_pref_panes_dir, // user_admin_applications_dir, // local_admin_applications_dir, // system_admin_applications_dir, user_applications_dir, local_applications_dir, system_applications_dir - ].concat(); + ]; + + let all_applications: Vec<_> = all_applications + .into_iter() + .flatten() + .collect(); tracing::debug!("Found following macOS applications: {:?}", all_applications); @@ -74,6 +87,123 @@ pub fn get_apps() -> Vec { all_applications } +fn get_settings(file_manager: &FileManager) -> Vec { + let system_version: SystemVersion = plist::from_file("/System/Library/CoreServices/SystemVersion.plist") + .expect("SystemVersion.plist doesn't follow expected format"); + + let regex = Regex::new(r"^(?\d+).\d+.\d+$") + .expect("This regex cannot be invalid"); + + let captures = regex.captures(&system_version.product_version) + .expect("SystemVersion.plist ProductVersion doesn't match expected format"); + + let major_version: u8 = captures["major"] + .parse() + .expect("SystemVersion.plist ProductVersion major doesn't match expected format"); + + if major_version >= 13 { // Ventura and higher + let sidebar: Vec = plist::from_file("/System/Applications/System Settings.app/Contents/Resources/Sidebar.plist") + .expect("Sidebar.plist doesn't follow expected format"); + + let preferences_ids: Vec<_> = sidebar.into_iter() + .flat_map(|section| match section { + SidebarSection::Content { content } => content, + SidebarSection::Title { .. } => vec![] + }) + .collect(); + + tracing::debug!("Found following macOS setting preference ids: {:?}", &preferences_ids); + + let extensions: HashMap<_, _> = get_extensions_in_dir(PathBuf::from("/System/Library/ExtensionKit/Extensions")) + .into_iter() + .map(|path| { + let name = path.file_stem() + .expect(&format!("invalid path: {:?}", path)) + .to_string_lossy() + .to_string(); + + let info_path = path.join("Contents").join("Info.plist"); + + let info = plist::from_file::(info_path) + .expect("Unexpected Info.plist for System Extensions"); + + let name = info.bundle_display_name + .clone() + .or_else(|| info.bundle_name.clone()) + .unwrap_or(name); + + (info.bundle_id, name) + }) + .collect(); + + tracing::debug!("Found following macOS setting extensions: {:?}", &preferences_ids); + + preferences_ids.into_iter() + .filter_map(|preferences_id| { + match extensions.get(&preferences_id) { + None => { + // todo some settings panel items return none here + tracing::debug!("Unknown preference id found: {}", &preferences_id); + + None + } + Some(name) => { + Some( + DesktopEntry { + name: name.to_string(), + icon: None, + command: vec![ + "open".to_string(), + format!("x-apple.systempreferences:{}", preferences_id) + ], + } + ) + } + } + }) + .collect() + } else { + let user_pref_panes_dir = get_pref_panes_with_kind(file_manager, SearchPathDirectory::Library, SearchPathDomainMask::User); + let local_pref_panes_dir = get_pref_panes_with_kind(file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Local); + let system_pref_panes_dir = get_pref_panes_with_kind(file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Domain); + + let all_settings = [ + user_pref_panes_dir, + local_pref_panes_dir, + system_pref_panes_dir, + ]; + + let all_settings: Vec<_> = all_settings + .into_iter() + .flatten() + .collect(); + + tracing::debug!("Found following macOS settings: {:?}", all_settings); + + let all_settings = all_settings.into_iter() + .map(|path| { + let name = path.file_stem() // TODO is there a proper way? + .expect(&format!("invalid path: {:?}", path)) + .to_string_lossy() + .to_string(); + + DesktopEntry { + name, + icon: None, + command: vec![ + "open".to_string(), + "-b".to_string(), + "com.apple.systempreferences".to_string(), + path.to_string_lossy().to_string() + ], + } + }) + .collect(); + + all_settings + } +} + fn get_pref_panes_with_kind(file_manager: &FileManager, directory: SearchPathDirectory, mask: SearchPathDomainMask) -> Vec { get_items_with_kind(file_manager, directory, mask, Some("PreferencePanes"), |dir| get_pref_panes_in_dir(dir)) } @@ -120,6 +250,10 @@ fn get_applications_in_dir(path: PathBuf) -> Vec { get_items_in_dir(path, "app") } +fn get_extensions_in_dir(path: PathBuf) -> Vec { + get_items_in_dir(path, "appex") +} + fn get_items_in_dir(path: PathBuf, extension: &str) -> Vec { match path.read_dir() { Ok(read_dir) => { @@ -208,6 +342,9 @@ fn get_png_from_icon_path(icon_path: PathBuf) -> Option> { #[derive(Deserialize)] struct Info { + #[serde(rename = "CFBundleIdentifier")] + bundle_id: String, + #[serde(rename = "CFBundleDisplayName")] bundle_display_name: Option, #[serde(rename = "CFBundleName")] @@ -218,4 +355,21 @@ struct Info { bundle_icon_file: Option, #[serde(rename = "CFBundleIconName")] bundle_icon_name: Option, +} + +#[derive(Deserialize)] +struct SystemVersion { + #[serde(rename = "ProductVersion")] + product_version: String, +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum SidebarSection { + Content { + content: Vec + }, + Title { + title: String + } } \ No newline at end of file From ba86afac6dcede09fb7608be361344d37bd04ecc Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:38:18 +0200 Subject: [PATCH 067/540] Update CHANGELOG.md, README.md and THEME.md --- CHANGELOG.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 43 ++++++++++++++++++++++++++++++------------ docs/THEME.md | 4 ++-- 3 files changed, 85 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e38448b..10130c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,58 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +### Plugin API +- New React Hooks + - `usePromise` + - Helper to run promises in a context of React view + - Returns `AsyncState` object which contains `isLoading`, `error` and `data` properties + - `useStorage` + - Helper to store data between entrypoint, plugin and application runs + - Follows API similar to `useState` built-in React Hook + - Uses `localStorage` internally + - `useCache` + - Helper to store data between entrypoint runs but will be reset when plugin or application is restarted + - Follows API similar to `useState` built-in React Hook + - Uses `sessionStorage` internally + - `useCachedPromise` + - Helper to run promises with caching done automatically + - Follows `stale-while-revalidate` caching strategy + - Uses `usePromise` and `useCache` Hooks internally + - `useFetch` + - Helper to run `fetch()` with caching done automatically + - Follows `stale-while-revalidate` caching strategy + - Uses `useCachedPromise` Hook internally +- Add `isLoading` property on ``, `

`, `` and `` + - If passed `true` the loading indicator will be shown above view content +- **BREAKING CHANGE**: To use `Clipboard` api, new permission `permissions.clipboard` is required to be specified in plugin manifest + - `permissions.clipboard` manifest property accepts a list that can include one or multiple of `"read"`, `"write"` or `"clear"` values +- **BREAKING CHANGE**: To use plugin entrypoint of type `inline-view`, new permission `permissions.main_search_bar` is required to be specified in plugin manifest + - `permissions.main_search_bar` manifest property accepts a list that can include `"read"` value +- **BREAKING CHANGE**: Plugin and Entrypoint Preference `name` properties in plugin manifest was split into 2 properties + - `preferences.name` is split into `preferences.name` and `preferences.id` + - `entrypoint.preferences.name` is split into `entrypoint.preferences.name` and `entrypoint.preferences.id` + - To preserve value set by user in settings please set the previous value of `name` to `id` +- **BREAKING CHANGE**: Replaced `onSelectionChange` and `id` properties on `` and `` with `onClick` on `` and `` + +### UI/UX Improvements +- Added ALT + K (OPT + K on macOS) label to Action Panel button in bottom panel in plugin views + - Refined styling to accommodate this change + - **BREAKING CHANGE**: Current color theme version increased to `2` + - **BREAKING CHANGE**: Current everything theme version increased to `2` + +### `Applications` plugin +- Add System settings items like Sound, Network, etc + - Both pre- and post-Ventura macOS settings are supported +- Fixed macOS applications, that are nested more than one directory level deep in `Applications` directory, not being added + +### `Calculator` plugin +- Updated `numbat` dependency to [1.13.0](https://github.com/sharkdp/numbat/releases/tag/v1.13.0) +- Enabled currency exchange rate module + +### Fixes +- Fix application crash when refreshing plugin via `npm run dev` from tools +- Fix plugin runtime shutting down when exception is thrown inside a promise handler + ## [8] - 2024-09-07 ### Plugin API diff --git a/README.md b/README.md index 3aa636b..9ecceb0 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,6 @@ https://github.com/user-attachments/assets/1aa790dc-fce9-4ac5-97d8-3b81b83acf2e ##### OS Support -###### Implemented - - Linux - Both X11 and Wayland (via LayerShell protocol) are supported - macOS @@ -79,8 +77,10 @@ https://github.com/user-attachments/assets/1aa790dc-fce9-4ac5-97d8-3b81b83acf2e - Action Panel - List - Grid -- Separate settings window -- Stack-based Navigation +- Inline + - View directly under main search bar + - Requires separate permission to be explicitly specified in manifest because it reads everything user enters in main search bar +- Settings window - Action Shortcuts - Theming @@ -88,16 +88,36 @@ https://github.com/user-attachments/assets/1aa790dc-fce9-4ac5-97d8-3b81b83acf2e See [#13](https://github.com/project-gauntlet/gauntlet/issues/13) -- Keyboard only navigation in plugin-views -- Vim motions - ##### APIs -###### Implemented - +- Stack-based Navigation +- Assets + - Files placed into `assets` directory in root of plugin repository are accessible at plugin runtime using `assetData` function - Preferences -- Inline views under main search bar + - Preferences defined in plugin manifest can be set by user and are accessible at plugin runtime using `pluginPreferences` and `entrypointPreferences` functions - Clipboard + - Accessible via `Clipboard` api + - Requires separate permission to be explicitly specified in manifest +- React Helper Hooks + - `usePromise` + - Helper to run promises in a context of React view + - Returns `AsyncState` object which contains `isLoading`, `error` and `data` properties + - `useStorage` + - Helper to store data between entrypoint, plugin and application runs + - Follows API similar to `useState` built-in React Hook + - Uses `localStorage` internally + - `useCache` + - Helper to store data between entrypoint runs but will be reset when plugin or application is restarted + - Follows API similar to `useState` built-in React Hook + - Uses `sessionStorage` internally + - `useCachedPromise` + - Helper to run promises with caching done automatically + - Follows `stale-while-revalidate` caching strategy + - Uses `usePromise` and `useCache` Hooks internally + - `useFetch` + - Helper to run `fetch()` with caching done automatically + - Follows `stale-while-revalidate` caching strategy + - Uses `useCachedPromise` Hook internally ###### Planned @@ -301,8 +321,7 @@ In plugin manifest it is possible to configure permissions which will allow plug network, environment variables, ffi or subprocess execution. Server saves plugins themselves and state of plugins into SQLite database. -Frontend is GUI application that uses [iced-rs](https://github.com/iced-rs/iced) as a GUI framework. -It is also exposes gRPC server that is used by server to render views +Frontend is GUI module that uses [iced-rs](https://github.com/iced-rs/iced) as a GUI framework. It is run in the same process as a server. Plugins can create UI using [React](https://github.com/facebook/react). Server implements custom React Reconciler (similar to React Native) which renders GUI components to frontend. diff --git a/docs/THEME.md b/docs/THEME.md index 8bddc4a..303fb6b 100644 --- a/docs/THEME.md +++ b/docs/THEME.md @@ -13,8 +13,8 @@ theme will stop working until it is updated. This may change in the future Current theme version: -- Color: `1` -- Everything: `1` +- Color: `2` +- Everything: `2` Theming is only applied to main window and doesn't affect settings From 2cf4816c6914646fb38f2d719bd07391c22fa5fd Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 15 Sep 2024 17:47:40 +0000 Subject: [PATCH 068/540] Prepare for v9 release --- CHANGELOG.md | 2 ++ VERSION | 2 +- js/api/package.json | 2 +- js/deno/package.json | 2 +- package-lock.json | 4 ++-- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10130c8..e67fad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +## [9] - 2024-09-15 + ### Plugin API - New React Hooks - `usePromise` diff --git a/VERSION b/VERSION index 301160a..f11c82a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8 \ No newline at end of file +9 \ No newline at end of file diff --git a/js/api/package.json b/js/api/package.json index 1085889..ba0c04e 100644 --- a/js/api/package.json +++ b/js/api/package.json @@ -1,6 +1,6 @@ { "name": "@project-gauntlet/api", - "version": "0.8.0", + "version": "0.9.0", "type": "module", "exports": { "./components": { diff --git a/js/deno/package.json b/js/deno/package.json index 5516d4d..293b7ce 100644 --- a/js/deno/package.json +++ b/js/deno/package.json @@ -1,6 +1,6 @@ { "name": "@project-gauntlet/deno", - "version": "0.8.0", + "version": "0.9.0", "type": "module", "exports": { ".": { diff --git a/package-lock.json b/package-lock.json index 57affe1..3c64a1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ }, "js/api": { "name": "@project-gauntlet/api", - "version": "0.8.0", + "version": "0.9.0", "devDependencies": { "@project-gauntlet/typings": "*", "typescript": "^5.3.3" @@ -123,7 +123,7 @@ }, "js/deno": { "name": "@project-gauntlet/deno", - "version": "0.8.0", + "version": "0.9.0", "devDependencies": { "@types/node": "^18.17.1", "typescript": "^5.3.3" From 18dea0d56d53276986d379e627c79544a033df82 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 15 Sep 2024 20:47:05 +0200 Subject: [PATCH 069/540] Add Numbat mention to README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9ecceb0..dd54426 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ https://github.com/user-attachments/assets/1aa790dc-fce9-4ac5-97d8-3b81b83acf2e - Currently, 3 bundled plugins are provided - Applications: provides list of applications - Calculator: shows result of mathematical operations directly under main search bar + - Powered by [Numbat](https://github.com/sharkdp/numbat) - Settings: open Gauntlet Settings from Gauntlet itself - Plugins are distributed as separate branch in Git repository, meaning plugin distribution doesn't need any central server From 007a6aa4d8798b9ae10bf265ff99e00d418b0560 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:32:13 +0200 Subject: [PATCH 070/540] Clarify macOS system settings item in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e67fad2..efd0d75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git - **BREAKING CHANGE**: Current everything theme version increased to `2` ### `Applications` plugin -- Add System settings items like Sound, Network, etc +- Add macOS System settings items like Sound, Network, etc - Both pre- and post-Ventura macOS settings are supported - Fixed macOS applications, that are nested more than one directory level deep in `Applications` directory, not being added From 079386cd806a942883f520db92920e4c4ab127a9 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 21 Sep 2024 17:30:56 +0200 Subject: [PATCH 071/540] Rework permissions. Better validation. Remove ffi and high_resolution_time --- Cargo.lock | 7 + dev_plugin/gauntlet.toml | 10 +- rust/server/Cargo.toml | 1 + rust/server/src/plugins/data_db_repository.rs | 27 +- rust/server/src/plugins/js/clipboard.rs | 13 +- rust/server/src/plugins/js/mod.rs | 70 ++--- rust/server/src/plugins/js/permissions.rs | 140 ++++++++++ rust/server/src/plugins/loader.rs | 242 ++++++++++++++---- rust/server/src/plugins/mod.rs | 24 +- tools | 2 +- 10 files changed, 404 insertions(+), 132 deletions(-) create mode 100644 rust/server/src/plugins/js/permissions.rs diff --git a/Cargo.lock b/Cargo.lock index 8d0d889..1c21a2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7762,6 +7762,7 @@ dependencies = [ "tonic", "tracing", "tracing-subscriber", + "typed-path", "ureq", "utils", "uuid", @@ -9681,6 +9682,12 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" +[[package]] +name = "typed-path" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c0c7479c430935701ff2532e3091e6680ec03f2f89ffcd9988b08e885b90a5" + [[package]] name = "typenum" version = "1.17.0" diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 9511ee9..73bdea1 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -157,4 +157,12 @@ environment = ["RUST_LOG"] system = ["systemMemoryInfo"] network = ["upload.wikimedia.org", "api.github.com"] clipboard = ["read", "write", "clear"] -main_search_bar = ["read"] \ No newline at end of file +main_search_bar = ["read"] + +[permissions.filesystem] +read = ["C:\\ProgramFiles", "C:/ProgramFiles", "/home/exidex"] +write = ["/home/exidex/.test"] + +[permissions.exec] +command = ["ls"] +executable = ["/usr/bin/ls"] \ No newline at end of file diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index ba60e20..e158b8f 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -38,6 +38,7 @@ arboard = "3.4.0" global-hotkey = "0.4.2" ureq = "2.10.0" bytes = "1.6.0" +typed-path = "0.9" scenario_runner = { path = "../scenario_runner", optional = true } itertools = "0.10.5" diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 187412f..b92baa0 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -10,6 +10,7 @@ use sqlx::{Error, Executor, Pool, Row, Sqlite, SqlitePool}; use sqlx::migrate::Migrator; use sqlx::sqlite::SqliteConnectOptions; use sqlx::types::Json; +use typed_path::TypedPathBuf; use uuid::Uuid; use common::model::{PhysicalKey, PhysicalShortcut}; use common::dirs::Dirs; @@ -117,17 +118,11 @@ pub struct DbPluginPermissions { #[serde(default)] pub environment: Vec, #[serde(default)] - pub high_resolution_time: bool, - #[serde(default)] pub network: Vec, #[serde(default)] - pub ffi: Vec, + pub filesystem: DbPluginPermissionsFileSystem, #[serde(default)] - pub fs_read_access: Vec, - #[serde(default)] - pub fs_write_access: Vec, - #[serde(default)] - pub run_subprocess: Vec, + pub exec: DbPluginPermissionsExec, #[serde(default)] pub system: Vec, #[serde(default)] @@ -136,6 +131,22 @@ pub struct DbPluginPermissions { pub main_search_bar: Vec, } +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct DbPluginPermissionsFileSystem { + #[serde(default)] + pub read: Vec, + #[serde(default)] + pub write: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct DbPluginPermissionsExec { + #[serde(default)] + pub command: Vec, + #[serde(default)] + pub executable: Vec, +} + #[derive(Debug, Deserialize, Serialize)] pub enum DbPluginClipboardPermissions { #[serde(rename = "read")] diff --git a/rust/server/src/plugins/js/clipboard.rs b/rust/server/src/plugins/js/clipboard.rs index a013193..350f31c 100644 --- a/rust/server/src/plugins/js/clipboard.rs +++ b/rust/server/src/plugins/js/clipboard.rs @@ -7,7 +7,8 @@ use deno_core::{op, OpState}; use image::RgbaImage; use serde::{Deserialize, Serialize}; use tokio::task::spawn_blocking; -use crate::plugins::js::{PluginClipboardPermissions, PluginData}; +use crate::plugins::js::permissions::PluginPermissionsClipboard; +use crate::plugins::js::PluginData; fn unknown_err_clipboard(err: arboard::Error) -> anyhow::Error { anyhow!("UNKNOWN_ERROR: {:?}", err) @@ -36,7 +37,7 @@ async fn clipboard_read(state: Rc>) -> anyhow::Result() .permissions() .clipboard - .contains(&PluginClipboardPermissions::Read); + .contains(&PluginPermissionsClipboard::Read); if !allow { return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); @@ -98,7 +99,7 @@ async fn clipboard_read_text(state: Rc>) -> anyhow::Result() .permissions() .clipboard - .contains(&PluginClipboardPermissions::Read); + .contains(&PluginPermissionsClipboard::Read); if !allow { return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); @@ -134,7 +135,7 @@ async fn clipboard_write(state: Rc>, data: ClipboardData) -> an .borrow::() .permissions() .clipboard - .contains(&PluginClipboardPermissions::Write); + .contains(&PluginPermissionsClipboard::Write); if !allow { return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); @@ -186,7 +187,7 @@ async fn clipboard_write_text(state: Rc>, data: String) -> anyh .borrow::() .permissions() .clipboard - .contains(&PluginClipboardPermissions::Write); + .contains(&PluginPermissionsClipboard::Write); if !allow { return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); @@ -213,7 +214,7 @@ async fn clipboard_clear(state: Rc>) -> anyhow::Result<()> { .borrow::() .permissions() .clipboard - .contains(&PluginClipboardPermissions::Clear); + .contains(&PluginPermissionsClipboard::Clear); if !allow { return Err(anyhow!("Plugin doesn't have 'clear' permission for clipboard")); diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 6bf4b0b..d0b8757 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -1,20 +1,23 @@ use std::cell::RefCell; use std::collections::HashMap; use std::fs::File; +use std::hash::Hash; use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::rc::Rc; +use std::str::FromStr; use std::time::Duration; use anyhow::{anyhow, Context}; -use deno_core::{FastString, futures, ModuleLoader, ModuleSource, ModuleSourceFuture, ModuleType, op, OpState, ResolutionKind, serde_v8, StaticModuleLoader, v8}; -use deno_core::futures::{FutureExt, Stream, StreamExt}; use deno_core::futures::executor::block_on; +use deno_core::futures::{FutureExt, Stream, StreamExt}; use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; +use deno_core::{futures, op, serde_v8, v8, FastString, ModuleLoader, ModuleSource, ModuleSourceFuture, ModuleType, OpState, ResolutionKind, StaticModuleLoader}; +use deno_runtime::BootstrapOptions; use deno_runtime::deno_core::ModuleSpecifier; use deno_runtime::deno_io::{Stdio, StdioPipe}; -use deno_runtime::permissions::{Permissions, PermissionsContainer, PermissionsOptions}; +use deno_runtime::permissions::{Descriptor, EnvDescriptor, NetDescriptor, Permissions, PermissionsContainer, PermissionsOptions, ReadDescriptor, UnaryPermission, WriteDescriptor}; use deno_runtime::worker::MainWorker; use deno_runtime::worker::WorkerOptions; use indexmap::IndexMap; @@ -27,16 +30,17 @@ use tokio_util::sync::CancellationToken; use common::dirs::Dirs; use common::model::{EntrypointId, PhysicalKey, PluginId, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidget, UiWidgetId}; use common::rpc::frontend_api::FrontendApi; -use component_model::{Children, Component, create_component_model, Property, PropertyType, SharedType}; +use component_model::{create_component_model, Children, Component, Property, PropertyType, SharedType}; use crate::model::{IntermediateUiEvent, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData, JsUiWidget, PreferenceUserData}; -use crate::plugins::applications::{DesktopEntry, get_apps}; -use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint, DbPluginClipboardPermissions}; +use crate::plugins::applications::{get_apps, DesktopEntry}; +use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; use crate::plugins::js::command_generators::get_command_generator_entrypoint_ids; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; +use crate::plugins::js::permissions::{permissions_to_deno, PluginPermissions, PluginPermissionsClipboard}; use crate::plugins::js::plugins::applications::{list_applications, open_application}; use crate::plugins::js::plugins::numbat::{run_numbat, NumbatContext}; use crate::plugins::js::plugins::settings::open_settings; @@ -54,6 +58,7 @@ mod preferences; mod search; mod command_generators; mod clipboard; +pub mod permissions; pub struct PluginRuntimeData { pub id: PluginId, @@ -73,37 +78,11 @@ pub struct PluginCode { pub js: HashMap, } -pub struct PluginPermissions { - pub environment: Vec, - pub high_resolution_time: bool, - pub network: Vec, - pub ffi: Vec, - pub fs_read_access: Vec, - pub fs_write_access: Vec, - pub run_subprocess: Vec, - pub system: Vec, - pub clipboard: Vec, - pub main_search_bar: Vec, -} - #[derive(Clone, Debug)] pub struct PluginRuntimePermissions { - pub clipboard: Vec, + pub clipboard: Vec, } -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum PluginClipboardPermissions { - Read, - Write, - Clear -} - -#[derive(Clone, Debug)] -pub enum PluginMainSearchBarPermissions { - Read, -} - - #[derive(Clone, Debug)] pub enum PluginCommand { One { @@ -306,25 +285,6 @@ async fn start_js_runtime( icon_cache: IconCache, dirs: Dirs, ) -> anyhow::Result<()> { - let permissions_container = PermissionsContainer::new(Permissions::from_options(&PermissionsOptions { - allow_env: if permissions.environment.is_empty() { None } else { Some(permissions.environment) }, - deny_env: None, - allow_hrtime: permissions.high_resolution_time, - deny_hrtime: false, - allow_net: if permissions.network.is_empty() { None } else { Some(permissions.network) }, - deny_net: None, - allow_ffi: if permissions.ffi.is_empty() { None } else { Some(permissions.ffi) }, - deny_ffi: None, - allow_read: if permissions.fs_read_access.is_empty() { None } else { Some(permissions.fs_read_access) }, - deny_read: None, - allow_run: if permissions.run_subprocess.is_empty() { None } else { Some(permissions.run_subprocess) }, - deny_run: None, - allow_sys: if permissions.system.is_empty() { None } else { Some(permissions.system) }, - deny_sys: None, - allow_write: if permissions.fs_write_access.is_empty() { None } else { Some(permissions.fs_write_access) }, - deny_write: None, - prompt: false, - })?); let dev_plugin = plugin_id.to_string().starts_with("file://"); @@ -360,6 +320,8 @@ async fn start_js_runtime( None }; + let permissions_container = permissions_to_deno(&permissions); + let runtime_permissions = PluginRuntimePermissions { clipboard: permissions.clipboard, }; @@ -368,6 +330,10 @@ async fn start_js_runtime( unused_url, permissions_container, WorkerOptions { + bootstrap: BootstrapOptions { + is_tty: false, + ..Default::default() + }, module_loader: Rc::new(CustomModuleLoader::new(code, dev_plugin)), extensions: vec![plugin_ext::init_ops_and_esm( EventReceiver::new(event_stream), diff --git a/rust/server/src/plugins/js/permissions.rs b/rust/server/src/plugins/js/permissions.rs new file mode 100644 index 0000000..565713d --- /dev/null +++ b/rust/server/src/plugins/js/permissions.rs @@ -0,0 +1,140 @@ +use deno_runtime::permissions::{Descriptor, EnvDescriptor, NetDescriptor, Permissions, PermissionsContainer, ReadDescriptor, RunDescriptor, SysDescriptor, UnaryPermission, WriteDescriptor}; +use std::collections::HashSet; +use std::hash::Hash; +use std::path::PathBuf; +use std::str::FromStr; + +pub struct PluginPermissions { + pub environment: Vec, + pub network: Vec, + pub filesystem: PluginPermissionsFileSystem, + pub exec: PluginPermissionsExec, + pub system: Vec, + pub clipboard: Vec, + pub main_search_bar: Vec, +} + +pub struct PluginPermissionsFileSystem { + pub read: Vec, + pub write: Vec, +} + +pub struct PluginPermissionsExec { + pub command: Vec, + pub executable: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PluginPermissionsClipboard { + Read, + Write, + Clear +} + +#[derive(Clone, Debug)] +pub enum PluginPermissionsMainSearchBar { + Read, +} + +pub fn permissions_to_deno(permissions: &PluginPermissions) -> PermissionsContainer { + PermissionsContainer::new(Permissions { + read: path_permission(&permissions.filesystem.read, ReadDescriptor), + write: path_permission(&permissions.filesystem.write, WriteDescriptor), + net: net_permission(&permissions.network), + env: env_permission(&permissions.environment), + sys: sys_permission(&permissions.system), + run: run_permission(&permissions.exec), + ffi: Permissions::new_ffi(&None, &None, false).expect("new_ffi should always succeed"), + hrtime: Permissions::new_hrtime(true, false), + }) +} + +fn path_permission( + paths: &[String], + to_permission: fn(PathBuf) -> T +) -> UnaryPermission { + let granted = paths + .into_iter() + .map(|path| to_permission(PathBuf::from(path))) + .collect(); + + UnaryPermission { + prompt: false, + granted_global: false, + flag_denied_global: false, + granted_list: granted, + ..Default::default() + } +} + +fn net_permission(domain_and_ports: &[String]) -> UnaryPermission { + let granted = domain_and_ports + .into_iter() + .map(|domain_and_port| { + NetDescriptor::from_str(&domain_and_port) + .expect("should be validated when loading") + }) + .collect(); + + UnaryPermission { + prompt: false, + granted_global: false, + flag_denied_global: false, + granted_list: granted, + ..Default::default() + } +} + +fn env_permission(envs: &[String]) -> UnaryPermission { + let granted = envs + .into_iter() + .map(|env| EnvDescriptor::new(env)) + .collect(); + + UnaryPermission { + prompt: false, + granted_global: false, + flag_denied_global: false, + granted_list: granted, + ..Default::default() + } +} + +fn sys_permission(system: &[String]) -> UnaryPermission { + let granted = system + .into_iter() + .map(|system| SysDescriptor(system.to_owned())) + .collect(); + + UnaryPermission { + prompt: false, + granted_global: false, + flag_denied_global: false, + granted_list: granted, + ..Default::default() + } +} + +fn run_permission(permissions: &PluginPermissionsExec) -> UnaryPermission { + let granted_executable = permissions.executable + .iter() + .map(|path| RunDescriptor::Path(PathBuf::from(path))) + .collect::>(); + + let granted_command = permissions.command + .iter() + .map(|cmd| RunDescriptor::Name(cmd.to_owned())) + .collect::>(); + + let mut granted = HashSet::new(); + granted.extend(granted_executable); + granted.extend(granted_command); + + UnaryPermission { + prompt: false, + granted_global: false, + flag_denied_global: false, + granted_list: granted, + ..Default::default() + } +} diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index 81fbe7f..a727537 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -6,16 +6,19 @@ use std::path::{Path, PathBuf}; use std::thread; use anyhow::{anyhow, Context}; +use deno_core::url; use include_dir::Dir; use serde::{Deserialize, Serialize}; use uuid::Uuid; use walkdir::WalkDir; use itertools::Itertools; use tracing_subscriber::fmt::format; +use typed_path::{TypedPathBuf, Utf8TypedPath, Utf8UnixComponent, Utf8WindowsComponent}; use common::model::{DownloadStatus, PluginId}; use crate::model::ActionShortcutKey; -use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_to_str, db_plugin_type_to_str, DbCode, DbPluginAction, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbPluginType, DbPreferenceEnumValue, DbWritePlugin, DbWritePluginAssetData, DbWritePluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions}; +use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_to_str, db_plugin_type_to_str, DbCode, DbPluginAction, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbPluginType, DbPreferenceEnumValue, DbWritePlugin, DbWritePluginAssetData, DbWritePluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions, DbPluginPermissionsFileSystem, DbPluginPermissionsExec}; use crate::plugins::download_status::DownloadStatusHolder; +use crate::plugins::js::permissions::{PluginPermissionsExec, PluginPermissionsFileSystem}; pub struct PluginLoader { db_repository: DataDbRepository, @@ -213,46 +216,8 @@ impl PluginLoader { tracing::debug!("Plugin config read: {:?}", plugin_manifest); - let permissions = &plugin_manifest.permissions; - - let env_exists = !permissions.environment.is_empty(); - let ffi_exists = !permissions.ffi.is_empty(); - let fs_read_exists = !permissions.fs_read_access.is_empty(); - let fs_write_exists = !permissions.fs_write_access.is_empty(); - let run_exists = !permissions.run_subprocess.is_empty(); - let system_exists = !permissions.system.is_empty(); - - let os_required = env_exists || ffi_exists || fs_read_exists || fs_write_exists || run_exists || system_exists; - - if os_required { - let current_system = if cfg!(target_os = "linux") { - PluginManifestSupportedSystem::Linux - } else if cfg!(target_os = "macos") { - PluginManifestSupportedSystem::MacOS - } else if cfg!(target_os = "windows") { - PluginManifestSupportedSystem::Windows - } else { - panic!("OS not supported") - }; - - let supported_system = &plugin_manifest.supported_system; - if !supported_system.contains(¤t_system) { - let supported_system = supported_system.iter().format(", "); - return Err(anyhow!("Plugin doesn't support current operating system. Operating systems supported by plugin: [{}]", supported_system)) - } - } - - let has_inline_view = plugin_manifest.entrypoint - .iter() - .find(|entrypoint| matches!(entrypoint.entrypoint_type, PluginManifestEntrypointTypes::InlineView)) - .is_some(); - - if has_inline_view { - let main_search_bar = &permissions.main_search_bar; - if !main_search_bar.contains(&PluginManifestMainSearchBarPermissions::Read) { - return Err(anyhow!("Plugin uses entrypoint type 'inline-view' but doesn't specify main search bar 'read' permission")) - } - } + // todo path permissions variables + Self::validate_manifest(&plugin_manifest)?; let plugin_name = plugin_manifest.gauntlet.name; let plugin_description = plugin_manifest.gauntlet.description; @@ -357,12 +322,15 @@ impl PluginLoader { let permissions = DbPluginPermissions { environment: plugin_manifest.permissions.environment, - high_resolution_time: plugin_manifest.permissions.high_resolution_time, network: plugin_manifest.permissions.network, - ffi: plugin_manifest.permissions.ffi, - fs_read_access: plugin_manifest.permissions.fs_read_access, - fs_write_access: plugin_manifest.permissions.fs_write_access, - run_subprocess: plugin_manifest.permissions.run_subprocess, + filesystem: DbPluginPermissionsFileSystem { + read: plugin_manifest.permissions.filesystem.read, + write: plugin_manifest.permissions.filesystem.write, + }, + exec: DbPluginPermissionsExec { + command: plugin_manifest.permissions.exec.command, + executable: plugin_manifest.permissions.exec.executable, + }, system: plugin_manifest.permissions.system, clipboard, main_search_bar, @@ -382,6 +350,162 @@ impl PluginLoader { preferences_user_data: HashMap::new() }) } + + fn validate_manifest(plugin_manifest: &PluginManifest) -> anyhow::Result<()> { + let supported_systems = &plugin_manifest.supported_system; + let supported_systems_str = supported_systems.iter().format(", "); + + let supports_linux = &supported_systems.iter().any(|system| matches!(system, PluginManifestSupportedSystem::Linux)); + let supports_macos = &supported_systems.iter().any(|system| matches!(system, PluginManifestSupportedSystem::MacOS)); + let supports_windows = &supported_systems.iter().any(|system| matches!(system, PluginManifestSupportedSystem::Windows)); + + let permissions = &plugin_manifest.permissions; + + Self::validate_string_permissions(&permissions.environment)?; + Self::validate_network_permissions(&permissions.network)?; + Self::validate_path_permissions(&permissions.filesystem.read, supports_linux, supports_macos, supports_windows)?; + Self::validate_path_permissions(&permissions.filesystem.write, supports_linux, supports_macos, supports_windows)?; + Self::validate_string_permissions(&permissions.exec.command)?; + Self::validate_path_permissions(&permissions.exec.executable, supports_linux, supports_macos, supports_windows)?; + + // even though system accepts a list of predefined values + // unknown values are ignored to allow for easier + // adoption to breaking changes in deno + // TODO do a warning + Self::validate_string_permissions(&permissions.system)?; + + let env_exists = !permissions.environment.is_empty(); + let fs_read_exists = !permissions.filesystem.read.is_empty(); + let fs_write_exists = !permissions.filesystem.write.is_empty(); + let command_exists = !permissions.exec.command.is_empty(); + let executable_exists = !permissions.exec.executable.is_empty(); + let system_exists = !permissions.system.is_empty(); + + let os_required = env_exists || fs_read_exists || fs_write_exists || command_exists || executable_exists || system_exists; + + if os_required { + let current_system = if cfg!(target_os = "linux") { + PluginManifestSupportedSystem::Linux + } else if cfg!(target_os = "macos") { + PluginManifestSupportedSystem::MacOS + } else if cfg!(target_os = "windows") { + PluginManifestSupportedSystem::Windows + } else { + panic!("OS not supported") + }; + + if !supported_systems.contains(¤t_system) { + return Err(anyhow!("Plugin doesn't support current operating system. Operating systems supported by plugin: [{}]", supported_systems_str)) + } + } + + let has_inline_view = plugin_manifest.entrypoint + .iter() + .find(|entrypoint| matches!(entrypoint.entrypoint_type, PluginManifestEntrypointTypes::InlineView)) + .is_some(); + + if has_inline_view { + let main_search_bar = &permissions.main_search_bar; + if !main_search_bar.contains(&PluginManifestMainSearchBarPermissions::Read) { + return Err(anyhow!("Plugin uses entrypoint type 'inline-view' but doesn't specify main search bar 'read' permission")) + } + } + + Ok(()) + } + + fn validate_path_permissions(paths: &[String], supports_linux: &bool, supports_macos: &bool, supports_windows: &bool) -> anyhow::Result<()> { + for path in paths { + if path.is_empty() { + Err(anyhow!("Empty path is not allowed in permissions"))? + } + + let path = Utf8TypedPath::derive(path); + + if !path.is_absolute() { + Err(anyhow!("Relative path is not allowed in permissions: {}", path))? + } + + match path { + Utf8TypedPath::Unix(path) => { + if !supports_macos && !supports_linux { + Err(anyhow!("When using unix-style path in permissions, plugin is required to include \"linux\" or \"macos\" in \"supported_system\" manifest property: {}", path))? + } + + if !path.is_valid() { + Err(anyhow!("Path is not valid: {}", path))? + } + + for component in path.components() { + match component { + Utf8UnixComponent::Normal(_) | Utf8UnixComponent::RootDir => {} + Utf8UnixComponent::CurDir => { + Err(anyhow!("Current directory '.' segment is not allowed in permission path: {}", path))? + } + Utf8UnixComponent::ParentDir => { + Err(anyhow!("Parent directory '..' segment is not allowed in permission path: {}", path))? + } + } + } + } + Utf8TypedPath::Windows(path) => { + if !supports_windows { + Err(anyhow!("When using windows-style path in permissions, plugin is required to include \"windows\" in \"supported_system\" manifest property: {}", path))? + } + + if !path.is_valid() { + Err(anyhow!("Path is not valid: {}", path))? + } + + for component in path.components() { + match component { + Utf8WindowsComponent::Normal(_) | Utf8WindowsComponent::RootDir | Utf8WindowsComponent::Prefix(_) => {} + Utf8WindowsComponent::CurDir => { + Err(anyhow!("Current directory '.' segment is not allowed in permission path: {}", path))? + } + Utf8WindowsComponent::ParentDir => { + Err(anyhow!("Parent directory '..' segment is not allowed in permission path: {}", path))? + } + } + } + } + } + } + + Ok(()) + } + + fn validate_string_permissions(values: &[String]) -> anyhow::Result<()> { + for value in values { + if value.is_empty() { + Err(anyhow!("Empty string value is not allowed in permissions"))? + } + } + + Ok(()) + } + + fn validate_network_permissions(values: &[String]) -> anyhow::Result<()> { + for value in values { + if value.is_empty() { + Err(anyhow!("Empty string value is not allowed in permissions"))? + } + + let url = url::Url::parse(&format!("http://{value}"))?; + + let contains_username = !url.username().is_empty(); + let contains_password = matches!(url.password(), Some(_)); + let contains_path = url.path() != "/"; + let contains_query = matches!(url.query(), Some(_)); + let contains_fragment = matches!(url.fragment(), Some(_)); + + // allow only domain and optional port + if contains_username || contains_password || contains_path || contains_query || contains_fragment { + Err(anyhow!("Network permission can only contain domain and optionally port: {}", value))? + } + } + Ok(()) + } } struct PluginDownloadData { @@ -844,17 +968,11 @@ pub struct PluginManifestPermissions { #[serde(default)] environment: Vec, #[serde(default)] - high_resolution_time: bool, - #[serde(default)] network: Vec, #[serde(default)] - ffi: Vec, + filesystem: PluginManifestPermissionsFileSystem, #[serde(default)] - fs_read_access: Vec, - #[serde(default)] - fs_write_access: Vec, - #[serde(default)] - run_subprocess: Vec, + exec: PluginManifestPermissionsExec, #[serde(default)] system: Vec, #[serde(default)] @@ -863,6 +981,22 @@ pub struct PluginManifestPermissions { main_search_bar: Vec, } +#[derive(Debug, Deserialize, Default)] +pub struct PluginManifestPermissionsFileSystem { + #[serde(default)] + pub read: Vec, + #[serde(default)] + pub write: Vec, +} + +#[derive(Debug, Deserialize, Default)] +pub struct PluginManifestPermissionsExec { + #[serde(default)] + pub command: Vec, + #[serde(default)] + pub executable: Vec, +} + #[derive(Debug, Deserialize)] pub enum PluginManifestClipboardPermissions { #[serde(rename = "read")] diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 76df4e7..7035a4c 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -20,7 +20,8 @@ use crate::plugins::config_reader::ConfigReader; use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions}; use crate::plugins::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; use crate::plugins::icon_cache::IconCache; -use crate::plugins::js::{AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginPermissions, PluginRuntimeData, start_plugin_runtime, PluginClipboardPermissions, PluginMainSearchBarPermissions}; +use crate::plugins::js::{AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginRuntimeData, start_plugin_runtime}; +use crate::plugins::js::permissions::{PluginPermissions, PluginPermissionsClipboard, PluginPermissionsExec, PluginPermissionsFileSystem, PluginPermissionsMainSearchBar}; use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; use crate::search::SearchIndex; @@ -564,9 +565,9 @@ impl ApplicationManager { .clipboard .into_iter() .map(|permission| match permission { - DbPluginClipboardPermissions::Read => PluginClipboardPermissions::Read, - DbPluginClipboardPermissions::Write => PluginClipboardPermissions::Write, - DbPluginClipboardPermissions::Clear => PluginClipboardPermissions::Clear, + DbPluginClipboardPermissions::Read => PluginPermissionsClipboard::Read, + DbPluginClipboardPermissions::Write => PluginPermissionsClipboard::Write, + DbPluginClipboardPermissions::Clear => PluginPermissionsClipboard::Clear, }) .collect(); @@ -574,7 +575,7 @@ impl ApplicationManager { .main_search_bar .into_iter() .map(|permission| match permission { - DbPluginMainSearchBarPermissions::Read => PluginMainSearchBarPermissions::Read, + DbPluginMainSearchBarPermissions::Read => PluginPermissionsMainSearchBar::Read, }) .collect(); @@ -585,12 +586,15 @@ impl ApplicationManager { inline_view_entrypoint_id, permissions: PluginPermissions { environment: plugin.permissions.environment, - high_resolution_time: plugin.permissions.high_resolution_time, network: plugin.permissions.network, - ffi: plugin.permissions.ffi, - fs_read_access: plugin.permissions.fs_read_access, - fs_write_access: plugin.permissions.fs_write_access, - run_subprocess: plugin.permissions.run_subprocess, + filesystem: PluginPermissionsFileSystem { + read: plugin.permissions.filesystem.read, + write: plugin.permissions.filesystem.write, + }, + exec: PluginPermissionsExec { + command: plugin.permissions.exec.command, + executable: plugin.permissions.exec.executable, + }, system: plugin.permissions.system, clipboard: clipboard_permissions, main_search_bar: main_search_bar_permissions diff --git a/tools b/tools index e441fa0..6fc6230 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit e441fa089a42f2bb4192a2fe7de0d8e3b7bb2f32 +Subproject commit 6fc6230976ff22719e39a746617da7c1ad901d61 From 45015f8f8dd4c778daf9f5c985d1d5185cd79a55 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 21 Sep 2024 17:36:40 +0200 Subject: [PATCH 072/540] Update package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 3c64a1b..e881d53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3040,7 +3040,7 @@ }, "tools": { "name": "@project-gauntlet/tools", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", From faa1c9e7b99b3b5a5cf6888edaa4e60b9e5c34dd Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 21 Sep 2024 17:41:08 +0200 Subject: [PATCH 073/540] Update nodejs version in Actions to 20 --- .github/workflows/setup-macos.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/setup-macos.yaml b/.github/workflows/setup-macos.yaml index 0843161..1b61063 100644 --- a/.github/workflows/setup-macos.yaml +++ b/.github/workflows/setup-macos.yaml @@ -25,7 +25,7 @@ jobs: - run: git pull - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 registry-url: "https://registry.npmjs.org" scope: '@project-gauntlet' From 1bb08e0f35db1a62181c92d805614e54f527bdcd Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 21 Sep 2024 20:17:06 +0200 Subject: [PATCH 074/540] Update nodejs version to 22 to fix deadlock when running npm tasks in github actions on macos --- .github/workflows/setup-macos.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/setup-macos.yaml b/.github/workflows/setup-macos.yaml index 1b61063..14c3fc6 100644 --- a/.github/workflows/setup-macos.yaml +++ b/.github/workflows/setup-macos.yaml @@ -25,7 +25,7 @@ jobs: - run: git pull - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: '@project-gauntlet' From b582d7ba7f7bafc7758c6739c90041bce79046a8 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 21 Sep 2024 20:36:31 +0200 Subject: [PATCH 075/540] Variables in paths of permissions --- dev_plugin/gauntlet.toml | 9 +- rust/common/src/dirs.rs | 15 ++- rust/server/src/plugins/applications/linux.rs | 6 +- rust/server/src/plugins/js/mod.rs | 2 +- rust/server/src/plugins/js/permissions.rs | 116 +++++++++++++++--- rust/server/src/plugins/loader.rs | 73 ++++++++++- 6 files changed, 193 insertions(+), 28 deletions(-) diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 73bdea1..e41f919 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -160,7 +160,14 @@ clipboard = ["read", "write", "clear"] main_search_bar = ["read"] [permissions.filesystem] -read = ["C:\\ProgramFiles", "C:/ProgramFiles", "/home/exidex"] +read = [ + "C:\\ProgramFiles\\test", + "C:/ProgramFiles/test", + "{windows:user-home}\\test", + "{windows:user-home}/test", + "{linux:user-home}/test", + "/etc/test" +] write = ["/home/exidex/.test"] [permissions.exec] diff --git a/rust/common/src/dirs.rs b/rust/common/src/dirs.rs index 3da1480..7216a65 100644 --- a/rust/common/src/dirs.rs +++ b/rust/common/src/dirs.rs @@ -15,12 +15,13 @@ impl Dirs { } } - pub fn home_dir(&self) -> Option { - let path = BaseDirs::new()? + pub fn home_dir(&self) -> PathBuf { + let path = BaseDirs::new() + .expect("System didn't report any home directory") .home_dir() .to_path_buf(); - Some(path) + path } pub fn data_db_file(&self) -> anyhow::Result { @@ -28,6 +29,10 @@ impl Dirs { Ok(path) } + pub fn plugin_data(&self, plugin_uuid: &str) -> anyhow::Result { + Ok(self.data_dir()?.join("plugins").join(&plugin_uuid)) + } + pub fn data_dir(&self) -> anyhow::Result { let data_dir = if cfg!(feature = "release") || cfg!(feature = "scenario_runner") { self.inner.data_dir().to_path_buf() @@ -75,6 +80,10 @@ impl Dirs { self.cache_dir().join("icons") } + pub fn plugin_cache(&self, plugin_uuid: &str) -> PathBuf { + self.cache_dir().join("plugins").join(&plugin_uuid) + } + pub fn cache_dir(&self) -> PathBuf { let cache_dir = if cfg!(feature = "release") || cfg!(feature = "scenario_runner") { self.inner.cache_dir().to_path_buf() diff --git a/rust/server/src/plugins/applications/linux.rs b/rust/server/src/plugins/applications/linux.rs index eaca975..f6c34d8 100644 --- a/rust/server/src/plugins/applications/linux.rs +++ b/rust/server/src/plugins/applications/linux.rs @@ -19,8 +19,10 @@ fn find_application_dirs() -> Option> { }, None => { let dirs = Dirs::new(); - let home = dirs.home_dir()?; - home.join(".local/share") + + dirs.home_dir() + .join(".local") + .join("share") } }; let extra_data_dirs = match env::var_os("XDG_DATA_DIRS") { diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index d0b8757..5758648 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -320,7 +320,7 @@ async fn start_js_runtime( None }; - let permissions_container = permissions_to_deno(&permissions); + let permissions_container = permissions_to_deno(&permissions, &dirs, &plugin_uuid)?; let runtime_permissions = PluginRuntimePermissions { clipboard: permissions.clipboard, diff --git a/rust/server/src/plugins/js/permissions.rs b/rust/server/src/plugins/js/permissions.rs index 565713d..071b067 100644 --- a/rust/server/src/plugins/js/permissions.rs +++ b/rust/server/src/plugins/js/permissions.rs @@ -3,6 +3,11 @@ use std::collections::HashSet; use std::hash::Hash; use std::path::PathBuf; use std::str::FromStr; +use anyhow::anyhow; +use typed_path::Utf8TypedPath; +use common::dirs::Dirs; +use common::model::PluginId; +use crate::plugins::loader::VARIABLE_PATTERN; pub struct PluginPermissions { pub environment: Vec, @@ -36,35 +41,43 @@ pub enum PluginPermissionsMainSearchBar { Read, } -pub fn permissions_to_deno(permissions: &PluginPermissions) -> PermissionsContainer { - PermissionsContainer::new(Permissions { - read: path_permission(&permissions.filesystem.read, ReadDescriptor), - write: path_permission(&permissions.filesystem.write, WriteDescriptor), +pub fn permissions_to_deno(permissions: &PluginPermissions, dirs: &Dirs, plugin_uuid: &str) -> anyhow::Result { + Ok(PermissionsContainer::new(Permissions { + read: path_permission(&permissions.filesystem.read, ReadDescriptor, dirs, plugin_uuid)?, + write: path_permission(&permissions.filesystem.write, WriteDescriptor, dirs, plugin_uuid)?, net: net_permission(&permissions.network), env: env_permission(&permissions.environment), sys: sys_permission(&permissions.system), - run: run_permission(&permissions.exec), + run: run_permission(&permissions.exec, dirs, plugin_uuid)?, ffi: Permissions::new_ffi(&None, &None, false).expect("new_ffi should always succeed"), hrtime: Permissions::new_hrtime(true, false), - }) + })) } fn path_permission( paths: &[String], - to_permission: fn(PathBuf) -> T -) -> UnaryPermission { + to_permission: fn(PathBuf) -> T, + dirs: &Dirs, + plugin_uuid: &str +) -> anyhow::Result> { let granted = paths .into_iter() - .map(|path| to_permission(PathBuf::from(path))) - .collect(); + .map(|path| { + augment_path(path, dirs, plugin_uuid) + .map(|path| path.map(|path| to_permission(path))) + }) + .collect::>>()? + .into_iter() + .filter_map(std::convert::identity) + .collect::>(); - UnaryPermission { + Ok(UnaryPermission { prompt: false, granted_global: false, flag_denied_global: false, granted_list: granted, ..Default::default() - } + }) } fn net_permission(domain_and_ports: &[String]) -> UnaryPermission { @@ -115,11 +128,17 @@ fn sys_permission(system: &[String]) -> UnaryPermission { } } -fn run_permission(permissions: &PluginPermissionsExec) -> UnaryPermission { +fn run_permission(permissions: &PluginPermissionsExec, dirs: &Dirs, plugin_uuid: &str) -> anyhow::Result> { let granted_executable = permissions.executable .iter() - .map(|path| RunDescriptor::Path(PathBuf::from(path))) - .collect::>(); + .map(|path| { + augment_path(path, dirs, plugin_uuid) + .map(|path| path.map(|path| RunDescriptor::Path(path))) + }) + .collect::>>()? + .into_iter() + .filter_map(std::convert::identity) + .collect::>(); let granted_command = permissions.command .iter() @@ -130,11 +149,74 @@ fn run_permission(permissions: &PluginPermissionsExec) -> UnaryPermission anyhow::Result> { + if let Some(matches) = VARIABLE_PATTERN.captures(path) { + let namespace = &matches["namespace"]; + let name = &matches["name"]; + + let replacement = match (namespace, name) { + ("macos", "user-home") => { + if cfg!(target_os = "macos") { + Some(dirs.home_dir()) + } else { + None + } + }, + ("linux", "user-home") => { + if cfg!(target_os = "linux") { + Some(dirs.home_dir()) + } else { + None + } + }, + ("windows", "user-home") => { + if cfg!(windows) { + Some(dirs.home_dir()) + } else { + None + } + }, + ("common", "plugin-data") => Some(dirs.plugin_data(plugin_uuid)?), + ("common", "plugin-cache") => Some(dirs.plugin_cache(plugin_uuid)), + (_, _) => { + Err(anyhow!("Trying to load plugin with unknown variable in path in manifest permissions: {}", path))? + } + }; + + match replacement { + None => Ok(None), + Some(replacement) => { + let replacement = replacement.to_str() + .expect("non-utf8 file paths are not supported"); + + Ok(Some(PathBuf::from(VARIABLE_PATTERN.replace(path, replacement).to_string()))) + } + } + } else { + match Utf8TypedPath::derive(&path) { + Utf8TypedPath::Unix(_) => { + if cfg!(unix) { + Ok(Some(PathBuf::from(path))) + } else { + Ok(None) + } + } + Utf8TypedPath::Windows(_) => { + if cfg!(windows) { + Ok(Some(PathBuf::from(path))) + } else { + Ok(None) + } + } + } + } +} \ No newline at end of file diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index a727537..61886b7 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -12,8 +12,10 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use walkdir::WalkDir; use itertools::Itertools; +use once_cell::sync::Lazy; +use regex::{Match, Regex}; use tracing_subscriber::fmt::format; -use typed_path::{TypedPathBuf, Utf8TypedPath, Utf8UnixComponent, Utf8WindowsComponent}; +use typed_path::{TypedPathBuf, Utf8TypedPath, Utf8UnixComponent, Utf8WindowsComponent, Utf8WindowsPrefix, Utf8WindowsPrefixComponent}; use common::model::{DownloadStatus, PluginId}; use crate::model::ActionShortcutKey; use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_to_str, db_plugin_type_to_str, DbCode, DbPluginAction, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbPluginType, DbPreferenceEnumValue, DbWritePlugin, DbWritePluginAssetData, DbWritePluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions, DbPluginPermissionsFileSystem, DbPluginPermissionsExec}; @@ -25,6 +27,8 @@ pub struct PluginLoader { download_status_holder: DownloadStatusHolder } +pub static VARIABLE_PATTERN: Lazy = Lazy::new(|| Regex::new(r"\{(?.+?):(?.+?)}").expect("invalid regex")); + impl PluginLoader { pub fn new(db_repository: DataDbRepository) -> Self { Self { @@ -216,7 +220,6 @@ impl PluginLoader { tracing::debug!("Plugin config read: {:?}", plugin_manifest); - // todo path permissions variables Self::validate_manifest(&plugin_manifest)?; let plugin_name = plugin_manifest.gauntlet.name; @@ -420,7 +423,57 @@ impl PluginLoader { Err(anyhow!("Empty path is not allowed in permissions"))? } - let path = Utf8TypedPath::derive(path); + // TODO custom parser for fun? for better error reporting, that will include cross-platform path parser + + let matches = VARIABLE_PATTERN.captures_iter(path).collect::>(); + let augmented_path = match matches.as_slice() { + [] => path.to_owned(), + [variable] => { + // TODO replace when https://github.com/rust-lang/regex/issues/1146 is resolved + let pattern_match = variable.get(0).unwrap(); + + if pattern_match.start() != 0 { + Err(anyhow!("Variable can only be used in the beginning of the path: {}", path))? + } + + let mut path_bytes = path.bytes(); + path_bytes.nth(pattern_match.end() - 1).expect("end of match should always exist"); + + let windows_like_path = match path_bytes.next() { + Some(b'\\') => true, + Some(b'/') | None => false, + Some(byte) => { + // this is done to prohibit "{linux:user-home}test" which for variable "/home/user" would result into "/home/usertest" + Err(anyhow!("Variable should always be followed with a slash or end of string, instead followed with {}, path: {}", byte as char, path))? + } + }; + + let namespace = &variable["namespace"]; + let name = &variable["name"]; + + let windows_like_path = match (namespace, name) { + ("macos", "user-home") => false, + ("linux", "user-home") => false, + ("windows", "user-home") => windows_like_path, + ("common", "plugin-data") => windows_like_path, + ("common", "plugin-cache") => windows_like_path, + (namespace, name) => { + Err(anyhow!("Unknown variable namespace and name combination in path in permissions: {}:{}", namespace, name))? + } + }; + + if windows_like_path { + VARIABLE_PATTERN.replace(path, "C:\\dummy-root").to_string() + } else { + VARIABLE_PATTERN.replace(path, "/dummy-root").to_string() + } + } + [_, ..] => { + Err(anyhow!("Path includes more than one variable: {}", path))? + } + }; + + let path = Utf8TypedPath::derive(&augmented_path); if !path.is_absolute() { Err(anyhow!("Relative path is not allowed in permissions: {}", path))? @@ -457,7 +510,19 @@ impl PluginLoader { Err(anyhow!("Path is not valid: {}", path))? } - for component in path.components() { + let components = path.components(); + + let prefix = components.prefix() + .expect("prefix should always be present for absolute paths"); + + match prefix.kind() { + Utf8WindowsPrefix::Disk('C') => {} + _ => { + Err(anyhow!("Only C:/ drive prefix in windows paths is supported, prefix: {}", prefix.as_str()))? + } + } + + for component in components { match component { Utf8WindowsComponent::Normal(_) | Utf8WindowsComponent::RootDir | Utf8WindowsComponent::Prefix(_) => {} Utf8WindowsComponent::CurDir => { From f697a40c4ee2807609bacb2bc09996e406448041 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 22 Sep 2024 09:42:03 +0200 Subject: [PATCH 076/540] Create plugin data and cache directories when requested by plugin --- rust/common/src/dirs.rs | 16 +++++++++++++--- rust/server/src/plugins/js/permissions.rs | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/rust/common/src/dirs.rs b/rust/common/src/dirs.rs index 7216a65..18c9b7f 100644 --- a/rust/common/src/dirs.rs +++ b/rust/common/src/dirs.rs @@ -30,7 +30,12 @@ impl Dirs { } pub fn plugin_data(&self, plugin_uuid: &str) -> anyhow::Result { - Ok(self.data_dir()?.join("plugins").join(&plugin_uuid)) + let plugin_data_dir = self.data_dir()?.join("plugins").join(&plugin_uuid); + + std::fs::create_dir_all(&plugin_data_dir) + .context("Unable to create plugin data directory")?; + + Ok(plugin_data_dir) } pub fn data_dir(&self) -> anyhow::Result { @@ -80,8 +85,13 @@ impl Dirs { self.cache_dir().join("icons") } - pub fn plugin_cache(&self, plugin_uuid: &str) -> PathBuf { - self.cache_dir().join("plugins").join(&plugin_uuid) + pub fn plugin_cache(&self, plugin_uuid: &str) -> anyhow::Result { + let plugin_cache_dir = self.cache_dir().join("plugins").join(&plugin_uuid); + + std::fs::create_dir_all(&plugin_cache_dir) + .context("Unable to create plugin cache directory")?; + + Ok(plugin_cache_dir) } pub fn cache_dir(&self) -> PathBuf { diff --git a/rust/server/src/plugins/js/permissions.rs b/rust/server/src/plugins/js/permissions.rs index 071b067..bca14dd 100644 --- a/rust/server/src/plugins/js/permissions.rs +++ b/rust/server/src/plugins/js/permissions.rs @@ -186,7 +186,7 @@ fn augment_path(path: &String, dirs: &Dirs, plugin_uuid: &str) -> anyhow::Result } }, ("common", "plugin-data") => Some(dirs.plugin_data(plugin_uuid)?), - ("common", "plugin-cache") => Some(dirs.plugin_cache(plugin_uuid)), + ("common", "plugin-cache") => Some(dirs.plugin_cache(plugin_uuid)?), (_, _) => { Err(anyhow!("Trying to load plugin with unknown variable in path in manifest permissions: {}", path))? } From 5420aafadfd3f642be6457024878240c1524ee2b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 22 Sep 2024 14:18:07 +0200 Subject: [PATCH 077/540] Make title of grid item optional --- dev_plugin/src/grid-view.tsx | 17 ++++++++++++----- js/api/src/gen/components.tsx | 4 ++-- rust/client/src/ui/widget.rs | 9 ++++++--- rust/component_model/src/lib.rs | 2 +- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/dev_plugin/src/grid-view.tsx b/dev_plugin/src/grid-view.tsx index f051a2f..f8a0525 100644 --- a/dev_plugin/src/grid-view.tsx +++ b/dev_plugin/src/grid-view.tsx @@ -5,10 +5,10 @@ import { useStorage } from "@project-gauntlet/api/hooks"; export default function GridView(): ReactElement { const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; - const [val1, setValue1] = useStorage("test", undefined); - const [val2, setValue2] = useStorage("test", { " test": "test" }); - const [val3, setValue3] = useStorage("test", ""); - const [val4, setValue4] = useStorage("test", ""); + const [val1, setValue1] = useStorage("grid-view-test-1", undefined); + const [val2, setValue2] = useStorage("grid-view-test-2", { " test": "test" }); + const [val3, setValue3] = useStorage("grid-view-test-3", ""); + const [val4, setValue4] = useStorage("grid-view-test-4", ""); return ( @@ -40,7 +40,14 @@ export default function GridView(): ReactElement { - + + + + Test Paragraph Section 2 2 + + + + Test Paragraph Section 2 2 diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index 046ff65..3731faa 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -140,7 +140,7 @@ declare global { }; ["gauntlet:grid_item"]: { children?: ElementComponent; - title: string; + title?: string; subtitle?: string; onClick?: () => void; }; @@ -673,7 +673,7 @@ List.Item = ListItem; List.Section = ListSection; export interface GridItemProps { children?: ElementComponent; - title: string; + title?: string; subtitle?: string; onClick?: () => void; } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index abd28a7..c59a392 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1032,13 +1032,16 @@ impl ComponentWidgetWrapper { .height(130) // TODO dynamic height .into(); - let title: Element<_> = text(title) - .into(); + let title: Option> = title.as_ref() + .map(|title| text(title).into()); let subtitle: Option> = subtitle.as_ref() .map(|subtitle| text(subtitle).into()); - let mut content = vec![content, title]; + let mut content = vec![content]; + if let Some(title) = title { + content.push(title); + } if let Some(subtitle) = subtitle { content.push(subtitle); } diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index 223632a..bb3c194 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -968,7 +968,7 @@ pub fn create_component_model() -> Vec { mark_doc!("/grid_item/description.md"), "GridItem", [ - property("title", mark_doc!("/grid_item/props/title.md"), false, PropertyType::String), + property("title", mark_doc!("/grid_item/props/title.md"), true, PropertyType::String), property("subtitle", mark_doc!("/grid_item/props/subtitle.md"), true, PropertyType::String), // accessories event("onClick", mark_doc!("/grid_item/props/onClick.md"), true, []) From 468013397dcd8dec070ee9dd992871c7253551fe Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 22 Sep 2024 21:02:19 +0200 Subject: [PATCH 078/540] Grid and List accessories. Refine grid styling --- dev_plugin/src/grid-view.tsx | 10 +- dev_plugin/src/list-view.tsx | 14 +- .../components/accessory_icon/description.md | 1 + .../components/accessory_icon/props/icon.md | 1 + .../accessory_icon/props/tooltip.md | 1 + .../components/accessory_text/description.md | 1 + .../components/accessory_text/props/icon.md | 1 + .../components/accessory_text/props/text.md | 1 + .../accessory_text/props/tooltip.md | 1 + .../components/grid_item/props/accessory.md | 1 + .../components/list_item/props/accessories.md | 1 + js/api/src/gen/components.tsx | 33 ++- js/api_build/src/index.ts | 152 +++++++++-- js/typings/index.d.ts | 6 +- rust/client/build.rs | 55 ++-- rust/client/src/ui/theme/container.rs | 13 + rust/client/src/ui/theme/mod.rs | 39 ++- rust/client/src/ui/theme/row.rs | 4 + rust/client/src/ui/theme/text.rs | 16 ++ rust/client/src/ui/widget.rs | 242 +++++++++++++++--- rust/common/src/model.rs | 6 + rust/common/src/rpc/grpc_convert.rs | 132 +--------- rust/common/src/scenario_convert.rs | 14 + rust/common/src/scenario_model.rs | 1 + rust/component_model/src/lib.rs | 97 ++++++- rust/server/src/plugins/js/mod.rs | 2 +- rust/server/src/plugins/js/ui.rs | 10 + 27 files changed, 627 insertions(+), 228 deletions(-) create mode 100644 docs/js/components/accessory_icon/description.md create mode 100644 docs/js/components/accessory_icon/props/icon.md create mode 100644 docs/js/components/accessory_icon/props/tooltip.md create mode 100644 docs/js/components/accessory_text/description.md create mode 100644 docs/js/components/accessory_text/props/icon.md create mode 100644 docs/js/components/accessory_text/props/text.md create mode 100644 docs/js/components/accessory_text/props/tooltip.md create mode 100644 docs/js/components/grid_item/props/accessory.md create mode 100644 docs/js/components/list_item/props/accessories.md diff --git a/dev_plugin/src/grid-view.tsx b/dev_plugin/src/grid-view.tsx index f8a0525..4b4b3d0 100644 --- a/dev_plugin/src/grid-view.tsx +++ b/dev_plugin/src/grid-view.tsx @@ -1,4 +1,4 @@ -import { Grid } from "@project-gauntlet/api/components"; +import { Grid, IconAccessory, Icons, TextAccessory } from "@project-gauntlet/api/components"; import { ReactElement } from "react"; import { useStorage } from "@project-gauntlet/api/hooks"; @@ -40,14 +40,18 @@ export default function GridView(): ReactElement { - + } + > Test Paragraph Section 2 2 - + }> Test Paragraph Section 2 2 diff --git a/dev_plugin/src/list-view.tsx b/dev_plugin/src/list-view.tsx index 225962d..fe8e57f 100644 --- a/dev_plugin/src/list-view.tsx +++ b/dev_plugin/src/list-view.tsx @@ -1,4 +1,4 @@ -import { Icons, List } from "@project-gauntlet/api/components"; +import { IconAccessory, Icons, List, TextAccessory } from "@project-gauntlet/api/components"; import { ReactElement, useState } from "react"; export default function ListView(): ReactElement { @@ -17,13 +17,21 @@ export default function ListView(): ReactElement { )) } - + - + , + + ]} + /> ) diff --git a/docs/js/components/accessory_icon/description.md b/docs/js/components/accessory_icon/description.md new file mode 100644 index 0000000..d2f833c --- /dev/null +++ b/docs/js/components/accessory_icon/description.md @@ -0,0 +1 @@ +An accessory with icon and optional tooltip \ No newline at end of file diff --git a/docs/js/components/accessory_icon/props/icon.md b/docs/js/components/accessory_icon/props/icon.md new file mode 100644 index 0000000..a2797f9 --- /dev/null +++ b/docs/js/components/accessory_icon/props/icon.md @@ -0,0 +1 @@ +Icon of the accessory \ No newline at end of file diff --git a/docs/js/components/accessory_icon/props/tooltip.md b/docs/js/components/accessory_icon/props/tooltip.md new file mode 100644 index 0000000..086a43e --- /dev/null +++ b/docs/js/components/accessory_icon/props/tooltip.md @@ -0,0 +1 @@ +A tooltip shown when the accessory is hovered. \ No newline at end of file diff --git a/docs/js/components/accessory_text/description.md b/docs/js/components/accessory_text/description.md new file mode 100644 index 0000000..94070b1 --- /dev/null +++ b/docs/js/components/accessory_text/description.md @@ -0,0 +1 @@ +An accessory with text and optional icon and tooltip \ No newline at end of file diff --git a/docs/js/components/accessory_text/props/icon.md b/docs/js/components/accessory_text/props/icon.md new file mode 100644 index 0000000..cbb19ee --- /dev/null +++ b/docs/js/components/accessory_text/props/icon.md @@ -0,0 +1 @@ +An icon that is displayed to the left side of the text \ No newline at end of file diff --git a/docs/js/components/accessory_text/props/text.md b/docs/js/components/accessory_text/props/text.md new file mode 100644 index 0000000..b9158ef --- /dev/null +++ b/docs/js/components/accessory_text/props/text.md @@ -0,0 +1 @@ +Text of the accessory \ No newline at end of file diff --git a/docs/js/components/accessory_text/props/tooltip.md b/docs/js/components/accessory_text/props/tooltip.md new file mode 100644 index 0000000..086a43e --- /dev/null +++ b/docs/js/components/accessory_text/props/tooltip.md @@ -0,0 +1 @@ +A tooltip shown when the accessory is hovered. \ No newline at end of file diff --git a/docs/js/components/grid_item/props/accessory.md b/docs/js/components/grid_item/props/accessory.md new file mode 100644 index 0000000..4d4c1a5 --- /dev/null +++ b/docs/js/components/grid_item/props/accessory.md @@ -0,0 +1 @@ +Accessory displayed on the right bottom of the grid item \ No newline at end of file diff --git a/docs/js/components/list_item/props/accessories.md b/docs/js/components/list_item/props/accessories.md new file mode 100644 index 0000000..b24a937 --- /dev/null +++ b/docs/js/components/list_item/props/accessories.md @@ -0,0 +1 @@ +List of accessories displayed on the right side of the list item \ No newline at end of file diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index 3731faa..7d81c0e 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -123,7 +123,17 @@ declare global { description?: string; image?: ImageSource | Icons; }; + ["gauntlet:accessory_icon"]: { + icon: ImageSource | Icons; + tooltip?: string; + }; + ["gauntlet:accessory_text"]: { + text: string; + icon?: ImageSource | Icons; + tooltip?: string; + }; ["gauntlet:list_item"]: { + children?: ElementComponent; title: string; subtitle?: string; icon?: ImageSource | Icons; @@ -139,7 +149,7 @@ declare global { isLoading?: boolean; }; ["gauntlet:grid_item"]: { - children?: ElementComponent; + children?: ElementComponent; title?: string; subtitle?: string; onClick?: () => void; @@ -634,14 +644,30 @@ export interface EmptyViewProps { export const EmptyView: FC = (props: EmptyViewProps): ReactNode => { return ; }; +export interface IconAccessoryProps { + icon: ImageSource | Icons; + tooltip?: string; +} +export const IconAccessory: FC = (props: IconAccessoryProps): ReactNode => { + return ; +}; +export interface TextAccessoryProps { + text: string; + icon?: ImageSource | Icons; + tooltip?: string; +} +export const TextAccessory: FC = (props: TextAccessoryProps): ReactNode => { + return ; +}; export interface ListItemProps { title: string; subtitle?: string; icon?: ImageSource | Icons; + accessories?: (ElementComponent | ElementComponent)[]; onClick?: () => void; } export const ListItem: FC = (props: ListItemProps): ReactNode => { - return ; + return {props.accessories as any}; }; export interface ListSectionProps { children?: ElementComponent; @@ -675,12 +701,13 @@ export interface GridItemProps { children?: ElementComponent; title?: string; subtitle?: string; + accessory?: ElementComponent; onClick?: () => void; } export const GridItem: FC & { Content: typeof Content; } = (props: GridItemProps): ReactNode => { - return {props.children}; + return {props.accessory as any}{props.children}; }; GridItem.Content = Content; export interface GridSectionProps { diff --git a/js/api_build/src/index.ts b/js/api_build/src/index.ts index 7322513..99af186 100644 --- a/js/api_build/src/index.ts +++ b/js/api_build/src/index.ts @@ -354,7 +354,7 @@ function makeComponents(modelInput: Component[]): ts.SourceFile { const properties = component.props .map(prop => { - if (prop.type.type === "component") { + if (!isInProperty(prop.type)) { return null } @@ -372,22 +372,23 @@ function makeComponents(modelInput: Component[]): ts.SourceFile { .filter((prop): prop is ts.JsxAttribute => prop != null); const children = [] - if (component.children.type != "none") { - const componentProps = component.props.filter(prop => prop.type.type === "component"); - if (componentProps.length !== 0) { - children.push( - ...componentProps.map(prop => ( - ts.factory.createAsExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier("props"), - ts.factory.createIdentifier(prop.name) - ), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) - ) - )) - ); - } + const componentProps = component.props.filter(prop => !isInProperty(prop.type)); + if (componentProps.length !== 0) { + children.push( + ...componentProps.map(prop => ( + ts.factory.createAsExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier("props"), + ts.factory.createIdentifier(prop.name) + ), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) + ) + )) + ); + } + + if (component.children.type != "none") { children.push(ts.factory.createPropertyAccessExpression( ts.factory.createIdentifier("props"), ts.factory.createIdentifier("children") @@ -545,7 +546,7 @@ function makeComponents(modelInput: Component[]): ts.SourceFile { function makePropertyTypes(component: StandardComponent, componentPropsInChildren: boolean): ts.TypeElement[] { const props = component.props - .filter(property => property.type.type === "component" ? !componentPropsInChildren : true) + .filter(property => !isInProperty(property.type) ? !componentPropsInChildren : true) .map(property => { return ts.factory.createPropertySignature( undefined, @@ -555,12 +556,16 @@ function makePropertyTypes(component: StandardComponent, componentPropsInChildre ) }); - const additionalComponentRefs = component.props - .map(property => property.type) - .filter((type): type is TypeComponent => componentPropsInChildren && type.type === "component") - .map(type => type.reference) + let additionalComponentRefs: ComponentRef[]; + if (componentPropsInChildren) { + additionalComponentRefs = component.props + .map(property => property.type) + .flatMap(type => collectAllComponentRefs(type)); + } else { + additionalComponentRefs = []; + } - if (component.children.type != "none") { + if (component.children.type != "none" || additionalComponentRefs.length > 0) { props.unshift(ts.factory.createPropertySignature( undefined, ts.factory.createIdentifier("children"), @@ -612,7 +617,23 @@ function makeChildrenType(type: Children, additionalComponentRefs: ComponentRef[ ) } case "none": { - throw new Error("Cannot construct none children") + if (additionalComponentRefs.length > 0) { + return ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("ElementComponent"), + [ + ts.factory.createUnionTypeNode( + additionalComponentRefs.map(member => ( + ts.factory.createTypeQueryNode( + ts.factory.createIdentifier(member.componentName), + undefined + ) + )) + ) + ] + ) + } else { + throw new Error("Cannot construct none children") + } } } } @@ -664,6 +685,9 @@ function makeType(type: PropertyType): ts.TypeNode { undefined ) } + case "array": { + return ts.factory.createArrayTypeNode(makeType(type.item)) + } case "enum": { return ts.factory.createTypeReferenceNode( ts.factory.createIdentifier(type.name), @@ -686,6 +710,88 @@ function makeType(type: PropertyType): ts.TypeNode { } +function isInProperty(propertyType: PropertyType) { + switch (propertyType.type) { + case "boolean": { + return true + } + case "number": { + return true + } + case "string": { + return true + } + case "function": { + return true // different from the rust side + } + case "component": { + return false + } + case "image_source": { + return true + } + case "array": { + return isInProperty(propertyType.item) + } + case "enum": { + return true + } + case "object": { + return true + } + case "union": { + if (propertyType.items.every(value => isInProperty(value))) { + return true + } else if (propertyType.items.every(value => !isInProperty(value))) { + return false + } else { + throw new Error("") + } + } + default: { + throw new Error(`unsupported type ${JSON.stringify(propertyType)}`) + } + } +} + +function collectAllComponentRefs(propertyType: PropertyType): ComponentRef[] { + switch (propertyType.type) { + case "boolean": { + return [] + } + case "number": { + return [] + } + case "string": { + return [] + } + case "function": { + return [] + } + case "component": { + return [propertyType.reference] + } + case "image_source": { + return [] + } + case "array": { + return collectAllComponentRefs(propertyType.item) + } + case "enum": { + return [] + } + case "object": { + return [] + } + case "union": { + return propertyType.items.flatMap(value => collectAllComponentRefs(value)) + } + default: { + throw new Error(`unsupported type ${JSON.stringify(propertyType)}`) + } + } +} + const genDir = "../api/src/gen"; if (!existsSync(genDir)) { mkdirSync(genDir); diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index ad58fd7..052c03a 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -185,7 +185,7 @@ type ComponentRef = { componentName: string, } -type PropertyType = TypeString | TypeNumber | TypeBoolean | TypeComponent | TypeFunction | TypeImageSource | TypeImageEnum | TypeImageUnion | TypeImageObject +type PropertyType = TypeString | TypeNumber | TypeBoolean | TypeComponent | TypeFunction | TypeImageSource | TypeImageEnum | TypeImageArray | TypeImageUnion | TypeImageObject type TypeString = { type: "string" @@ -215,6 +215,10 @@ type TypeImageUnion = { type: "union" items: PropertyType[] } +type TypeImageArray = { + type: "array" + item: PropertyType +} type TypeImageObject = { type: "object" name: string diff --git a/rust/client/build.rs b/rust/client/build.rs index 9681d0d..a87b674 100644 --- a/rust/client/build.rs +++ b/rust/client/build.rs @@ -23,7 +23,7 @@ fn main() -> anyhow::Result<()> { Component::Standard { name, props, children, .. } => { output.push_str(&format!(" {}", name)); - let has_children = !matches!(children, Children::None); + let has_children = !matches!(children, Children::None) || props.iter().any(|prop| prop.property_type.is_in_children()); if !props.is_empty() || has_children { output.push_str(" {\n"); @@ -33,23 +33,21 @@ fn main() -> anyhow::Result<()> { Children::StringOrMembers { .. } => "Vec".to_owned(), Children::Members { .. } => "Vec".to_owned(), Children::String { .. } => "Vec".to_owned(), - Children::None => panic!("cannot create type for Children::None") + Children::None => { + if props.iter().any(|prop| prop.property_type.is_in_children()) { + "Vec".to_owned() + } else { + panic!("cannot create type for Children::None") + } + } }; output.push_str(&format!(" children: {},\n", string)); } for prop in props { - match prop.property_type { - PropertyType::Function { .. } => { - // client know about functions in properties - } - PropertyType::Component { .. } => { - // component properties are found in children array - } - _ => { - output.push_str(&format!(" {}: {},\n", prop.name, generate_type(&prop, name))); - } + if prop.property_type.is_in_property() { + output.push_str(&format!(" {}: {},\n", prop.name, generate_type(&prop, name))); } } output.push_str(" },\n"); @@ -83,6 +81,10 @@ fn main() -> anyhow::Result<()> { output.push_str(&format!("enum {}{} {{\n", component_name, prop.name.to_case(Case::Pascal))); for (index, property_type) in items.iter().enumerate() { + if !property_type.is_in_property() { + continue + } + match property_type { PropertyType::Union { .. } => panic!("nested union should not be used"), _ => { @@ -139,7 +141,7 @@ fn main() -> anyhow::Result<()> { Component::Standard { internal_name, name, props, children, .. } => { output.push_str(&format!(" \"gauntlet:{}\" => ComponentWidget::{}", internal_name, name)); - let has_children = !matches!(children, Children::None); + let has_children = !matches!(children, Children::None) || props.iter().any(|prop| prop.property_type.is_in_children()); if !props.is_empty() || has_children { output.push_str(&" {\n"); @@ -149,7 +151,11 @@ fn main() -> anyhow::Result<()> { } for prop in props { - match prop.property_type { + if !prop.property_type.is_in_property() { + continue + } + + match &prop.property_type { PropertyType::String => { if prop.optional { output.push_str(&format!(" {}: parse_optional_string(&properties, \"{}\")?,\n", prop.name, prop.name)); @@ -171,12 +177,6 @@ fn main() -> anyhow::Result<()> { output.push_str(&format!(" {}: parse_boolean(&properties, \"{}\")?,\n", prop.name, prop.name)); } }, - PropertyType::Function { .. } => { - // client know about functions in properties - } - PropertyType::Component { .. } => { - // component properties are found in a children array - } PropertyType::ImageSource => { if prop.optional { output.push_str(&format!(" {}: parse_bytes_optional(&properties, \"{}\")?,\n", prop.name, prop.name)); @@ -205,6 +205,16 @@ fn main() -> anyhow::Result<()> { output.push_str(&format!(" {}: parse_object(&properties, \"{}\")?,\n", prop.name, prop.name)); } } + PropertyType::Array { .. } => { + if prop.optional { + output.push_str(&format!(" {}: parse_array_optional(&properties, \"{}\")?,\n", prop.name, prop.name)); + } else { + output.push_str(&format!(" {}: parse_array(&properties, \"{}\")?,\n", prop.name, prop.name)); + } + } + _ => { + unreachable!() + } }; } output.push_str(" },\n"); @@ -519,10 +529,11 @@ fn generate_required_type(property_type: &PropertyType, union_name: String) -> S PropertyType::Number => "f64".to_owned(), PropertyType::Boolean => "bool".to_owned(), PropertyType::Function { .. } => panic!("client doesn't know about functions in properties"), - PropertyType::Component { .. } => panic!("component properties are found in children array"), + PropertyType::Component { .. } => panic!("component should not be present in properties"), PropertyType::ImageSource => "bytes::Bytes".to_owned(), PropertyType::Union { .. } => union_name, PropertyType::Object { name } => name.to_owned(), - PropertyType::Enum { name } => name.to_owned() + PropertyType::Enum { name } => name.to_owned(), + PropertyType::Array { item } => format!("Vec<{}>", generate_required_type(item, "SHOULD NOT BE USED".to_string())) } } \ No newline at end of file diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index b3e1805..219eb35 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -51,6 +51,9 @@ pub enum ContainerStyle { GridInner, List, ListInner, + TextAccessory, + TextAccessoryIcon, + IconAccessory, } #[derive(Default)] @@ -356,6 +359,16 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau ContainerStyle::RootBottomPanelActionButtonText => { self.padding(theme.root_bottom_panel_action_button_text.padding.to_iced()) } + ContainerStyle::TextAccessory => { + self.padding(theme.text_accessory.padding.to_iced()) + } + ContainerStyle::TextAccessoryIcon => { + let horizontal_spacing = theme.text_accessory.spacing; + self.padding(Padding::from([0.0, horizontal_spacing, 0.0, 0.0])) + } + ContainerStyle::IconAccessory => { + self.padding(theme.icon_accessory.padding.to_iced()) + } }.into() } } diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index daeb0f5..3e2ff3a 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -25,7 +25,7 @@ mod loading_bar; pub type Element<'a, Message> = iced::Element<'a, Message, GauntletTheme>; const CURRENT_COLOR_THEME_VERSION: u64 = 2; -const CURRENT_THEME_VERSION: u64 = 2; +const CURRENT_THEME_VERSION: u64 = 3; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GauntletColorTheme { @@ -77,6 +77,8 @@ pub struct GauntletTheme { list: ThemePaddingOnly, list_inner: ThemePaddingOnly, grid_item: ThemeButton, + grid_item_title: ThemePaddingTextColor, + grid_item_subtitle: ThemeTextColor, grid_section_title: ThemePaddingTextColor, inline: ThemePaddingOnly, list_item: ThemeButton, @@ -113,6 +115,8 @@ pub struct GauntletTheme { scrollbar: ThemeScrollbar, tooltip: ThemeTooltip, loading_bar: ThemeLoadingBar, + text_accessory: ThemeTextAccessory, + icon_accessory: ThemeIconAccessory, } impl Default for GauntletTheme { @@ -331,6 +335,13 @@ impl GauntletTheme { border_width: 0.0, border_color: TRANSPARENT, }, + grid_item_title: ThemePaddingTextColor { + padding: padding_axis(4.0, 0.0), + text_color, + }, + grid_item_subtitle: ThemeTextColor { + text_color: text_darker_color, + }, content_horizontal_break: ThemePaddingOnly { padding: padding_axis(8.0, 0.0), }, @@ -425,11 +436,11 @@ impl GauntletTheme { padding: padding_axis(4.0, 12.0), }, list_section_title: ThemePaddingTextColor { - padding: padding_axis(4.0, 8.0), + padding: padding(12.0, 8.0, 4.0, 8.0), text_color: text_darker_color, }, grid_section_title: ThemePaddingTextColor { - padding: padding_axis(4.0, 0.0), + padding: padding(12.0, 0.0, 4.0, 0.0), text_color: text_darker_color, }, main_list_item: ThemeButton { @@ -556,6 +567,15 @@ impl GauntletTheme { loading_bar_color: primary_color, background_color: background_light_color, }, + text_accessory: ThemeTextAccessory { + padding: padding(4.0, 4.0, 4.0, 16.0), + text_color: text_darker_color, + spacing: 8.0, + }, + icon_accessory: ThemeIconAccessory { + padding: padding(4.0, 4.0, 4.0, 16.0), + icon_color: text_darker_color, + }, } } } @@ -804,6 +824,19 @@ pub struct ExternalThemeGrid { spacing: f32, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThemeIconAccessory { + padding: ThemePadding, + icon_color: ThemeColor, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThemeTextAccessory { + padding: ThemePadding, + text_color: ThemeColor, + spacing: f32, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExternalThemeSize { width: f32, diff --git a/rust/client/src/ui/theme/row.rs b/rust/client/src/ui/theme/row.rs index ffc55dc..c43cc1d 100644 --- a/rust/client/src/ui/theme/row.rs +++ b/rust/client/src/ui/theme/row.rs @@ -7,6 +7,7 @@ pub enum RowStyle { FormInput, ListSectionTitle, GridSectionTitle, + GridItemTitle, } impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletTheme, Renderer> { @@ -28,6 +29,9 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletT RowStyle::GridSectionTitle => { self.padding(theme.grid_section_title.padding.to_iced()) } + RowStyle::GridItemTitle => { + self.padding(theme.grid_item_title.padding.to_iced()) + } }.into() } } \ No newline at end of file diff --git a/rust/client/src/ui/theme/text.rs b/rust/client/src/ui/theme/text.rs index dd81ce4..d9c05ba 100644 --- a/rust/client/src/ui/theme/text.rs +++ b/rust/client/src/ui/theme/text.rs @@ -15,6 +15,10 @@ pub enum TextStyle { GridSectionTitle, MainListItemSubtext, MetadataItemLabel, + TextAccessory, + IconAccessory, + GridItemTitle, + GridItemSubTitle, } impl<'a, Message: 'a> ThemableWidget<'a, Message> for Text<'a, GauntletTheme, Renderer> { @@ -60,6 +64,18 @@ impl text::StyleSheet for GauntletTheme { }, TextStyle::MetadataItemLabel => Appearance { color: Some(self.metadata_item_label.text_color.to_iced()), + }, + TextStyle::TextAccessory => Appearance { + color: Some(self.text_accessory.text_color.to_iced()), + }, + TextStyle::IconAccessory => Appearance { + color: Some(self.icon_accessory.icon_color.to_iced()), + }, + TextStyle::GridItemTitle => Appearance { + color: Some(self.grid_item_title.text_color.to_iced()), + }, + TextStyle::GridItemSubTitle => Appearance { + color: Some(self.grid_item_subtitle.text_color.to_iced()), } } } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index c59a392..53569a1 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -4,17 +4,17 @@ use std::str::FromStr; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; use anyhow::anyhow; -use iced::{Alignment, Font, Length}; -use iced::alignment::Horizontal; +use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; -use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, pick_list, row, scrollable, Space, text, text_input, tooltip, vertical_rule, mouse_area}; use iced::widget::image::Handle; use iced::widget::tooltip::Position; -use iced_aw::{floating_element, GridRow}; +use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, mouse_area, pick_list, row, scrollable, text, text_input, tooltip, vertical_rule, Space}; +use iced::{Alignment, Font, Length}; use iced_aw::core::icons; use iced_aw::date_picker::Date; use iced_aw::floating_element::Offset; use iced_aw::helpers::{date_picker, grid, grid_row, wrap_horizontal}; +use iced_aw::{floating_element, GridRow}; use itertools::Itertools; use common::model::{PhysicalKey, PhysicalShortcut, PluginId, UiPropertyValue, UiPropertyValueToEnum, UiPropertyValueToStruct, UiWidgetId}; @@ -22,7 +22,6 @@ use common_ui::shortcut_to_text; use crate::model::UiViewEvent; use crate::ui::custom_widgets::loading_bar::LoadingBar; -use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; use crate::ui::theme::date_picker::DatePickerStyle; @@ -34,6 +33,7 @@ use crate::ui::theme::rule::RuleStyle; use crate::ui::theme::text::TextStyle; use crate::ui::theme::text_input::TextInputStyle; use crate::ui::theme::tooltip::TooltipStyle; +use crate::ui::theme::{Element, ThemableWidget}; #[derive(Clone, Debug)] pub struct ComponentWidgetWrapper { @@ -883,7 +883,89 @@ impl ComponentWidgetWrapper { .center_y() .into() } - ComponentWidget::ListItem { title, subtitle, icon } => { + ComponentWidget::IconAccessory { icon, tooltip: tooltip_text } => { + let icon: Element<_> = match icon { + IconAccessoryIcon::_0(bytes) => { + image(Handle::from_memory(bytes.clone())) + .into() + }, + IconAccessoryIcon::_1(icon) => { + text(icon_to_bootstrap(icon)) + .font(icons::BOOTSTRAP_FONT) + .themed(TextStyle::IconAccessory) + } + }; + + let content = container(icon) + .center_x() + .center_y() + .themed(ContainerStyle::IconAccessory); + + match tooltip_text.as_ref() { + None => content, + Some(tooltip_text) => { + let tooltip_text: Element<_> = text(tooltip_text) + .into(); + + tooltip(content, tooltip_text, Position::Top) + .themed(TooltipStyle::Tooltip) + } + } + }, + ComponentWidget::TextAccessory { text: text_value, icon, tooltip: tooltip_text } => { + let icon: Option> = icon.as_ref() + .map(|icon| { + match icon { + TextAccessoryIcon::_0(bytes) => { + image(Handle::from_memory(bytes.clone())) + .into() + }, + TextAccessoryIcon::_1(icon) => { + let icon = icon.to_owned(); + text(icon_to_bootstrap(icon)) + .font(icons::BOOTSTRAP_FONT) + .themed(TextStyle::TextAccessory) + } + } + }); + + let text_content: Element<_> = text(text_value) + .themed(TextStyle::TextAccessory); + + let mut content: Vec> = vec![]; + + if let Some(icon) = icon { + let icon: Element<_> = container(icon) + .themed(ContainerStyle::TextAccessoryIcon); + + content.push(icon) + } + + content.push(text_content); + + let content: Element<_> = row(content) + .align_items(Alignment::Center) + .into(); + + let content = container(content) + .center_x() + .center_y() + .themed(ContainerStyle::TextAccessory); + + match tooltip_text.as_ref() { + None => content, + Some(tooltip_text) => { + let tooltip_text: Element<_> = text(tooltip_text) + .into(); + + tooltip(content, tooltip_text, Position::Top) + .themed(TooltipStyle::Tooltip) + } + } + }, + ComponentWidget::ListItem { title, subtitle, icon, children } => { + let accessories = render_children_by_type(children, |widget| matches!(widget, ComponentWidget::TextAccessory { .. } | ComponentWidget::IconAccessory { .. }), ComponentRenderContext::None); + let icon: Option> = icon.as_ref() .map(|icon| { match icon { @@ -922,6 +1004,18 @@ impl ComponentWidgetWrapper { content.push(subtitle) } + + if accessories.len() > 0 { + let accessories: Element<_> = row(accessories) + .into(); + + let space = horizontal_space() + .into(); + + content.push(space); + content.push(accessories); + } + let content: Element<_> = row(content) .align_items(Alignment::Center) .into(); @@ -1028,31 +1122,58 @@ impl ComponentWidgetWrapper { render_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } ComponentWidget::GridItem { children, title, subtitle } => { - let content: Element<_> = column(render_children(children, ComponentRenderContext::GridItem)) + // TODO should be just one + let accessories = render_children_by_type(children, |widget| matches!(widget, ComponentWidget::IconAccessory { .. }), ComponentRenderContext::None); + + let content: Element<_> = column(render_children_by_type(children, |widget| matches!(widget, ComponentWidget::Content { .. }), ComponentRenderContext::GridItem)) .height(130) // TODO dynamic height .into(); - let title: Option> = title.as_ref() - .map(|title| text(title).into()); - - let subtitle: Option> = subtitle.as_ref() - .map(|subtitle| text(subtitle).into()); - - let mut content = vec![content]; - if let Some(title) = title { - content.push(title); - } - if let Some(subtitle) = subtitle { - content.push(subtitle); - } - - let content: Element<_> = column(content) - .into(); - let content: Element<_> = button(content) .on_press(ComponentWidgetEvent::GridItemClick { widget_id }) + .width(Length::Fill) .themed(ButtonStyle::GridItem); + let mut sub_content_left = vec![]; + + if let Some(title) = title { + // TODO text truncation when iced supports it + let title = text(title) + .themed(TextStyle::GridItemTitle); + + sub_content_left.push(title); + } + + if let Some(subtitle) = subtitle { + let subtitle = text(subtitle) + .themed(TextStyle::GridItemSubTitle); + + sub_content_left.push(subtitle); + } + + let mut sub_content_right = vec![]; + if accessories.len() > 0 { + let accessories: Element<_> = row(accessories) + .into(); + + sub_content_right.push(accessories); + } + + let sub_content_left: Element<_> = column(sub_content_left) + .width(Length::Fill) + .into(); + + let sub_content_right: Element<_> = column(sub_content_right) + .width(Length::Shrink) + .into(); + + let sub_content: Element<_> = row(vec![sub_content_left, sub_content_right]) + .themed(RowStyle::GridItemTitle); + + let content: Element<_> = column(vec![content, sub_content]) + .width(Length::Fill) + .into(); + content } ComponentWidget::GridSection { children, title, subtitle, columns } => { @@ -1204,6 +1325,7 @@ fn render_grid<'a>(children: &[ComponentWidgetWrapper], /*aspect_ratio: Option<& let grid: Element<_> = grid(rows) .width(Length::Fill) + .vertical_alignment(Vertical::Top) .themed(GridStyle::Default); grid @@ -1746,7 +1868,7 @@ fn parse_object_optional(properties: &HashMap Ok(None), Some(value) => { - let value = value.as_object().ok_or(anyhow::anyhow!("{} has to be a object", name))?; + let value = value.as_object().ok_or(anyhow::anyhow!("{} has to be an object", name))?; Ok(Some(value)) }, @@ -1757,6 +1879,21 @@ fn parse_object(properties: &HashMap(properties: &HashMap, name: &str) -> anyhow::Result>> { + match properties.get(name) { + None => Ok(None), + Some(value) => { + let value = value.as_array().ok_or(anyhow::anyhow!("{} has to be an array", name))?; + + Ok(Some(value)) + }, + } +} + +fn parse_array(properties: &HashMap, name: &str) -> anyhow::Result> { + parse_array_optional(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name)) +} + fn parse_union_optional(properties: &HashMap, name: &str) -> anyhow::Result> { match properties.get(name) { None => Ok(None), @@ -1998,10 +2135,11 @@ impl UiPropertyValueToEnum for ListItemIcon { match value { UiPropertyValue::String(value) => Ok(ListItemIcon::_1(Icons::from_str(value)?)), UiPropertyValue::Bytes(value) => Ok(ListItemIcon::_0(value.clone())), - UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number for ListItemIcon")), - UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool for ListItemIcon")), - UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object for ListItemIcon")), - UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined for ListItemIcon")) + UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number")), + UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool")), + UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object")), + UiPropertyValue::Array(_) => Err(anyhow!("unexpected type undefined")), + UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined")) } } } @@ -2011,10 +2149,11 @@ impl UiPropertyValueToEnum for EmptyViewImage { match value { UiPropertyValue::String(value) => Ok(EmptyViewImage::_1(Icons::from_str(value)?)), UiPropertyValue::Bytes(value) => Ok(EmptyViewImage::_0(value.clone())), - UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number for ListItemIcon")), - UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool for ListItemIcon")), - UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object for ListItemIcon")), - UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined for ListItemIcon")) + UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number")), + UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool")), + UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object")), + UiPropertyValue::Array(_) => Err(anyhow!("unexpected type undefined")), + UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined")) } } } @@ -2024,10 +2163,39 @@ impl UiPropertyValueToEnum for ImageSource { match value { UiPropertyValue::String(value) => Ok(ImageSource::_1(Icons::from_str(value)?)), UiPropertyValue::Bytes(value) => Ok(ImageSource::_0(value.clone())), - UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number for ListItemIcon")), - UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool for ListItemIcon")), - UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object for ListItemIcon")), - UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined for ListItemIcon")) + UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number")), + UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool")), + UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object")), + UiPropertyValue::Array(_) => Err(anyhow!("unexpected type undefined")), + UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined")) + } + } +} + +impl UiPropertyValueToEnum for IconAccessoryIcon { + fn convert(value: &UiPropertyValue) -> anyhow::Result { + match value { + UiPropertyValue::String(value) => Ok(IconAccessoryIcon::_1(Icons::from_str(value)?)), + UiPropertyValue::Bytes(value) => Ok(IconAccessoryIcon::_0(value.clone())), + UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number")), + UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool")), + UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object")), + UiPropertyValue::Array(_) => Err(anyhow!("unexpected type undefined")), + UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined")), + } + } +} + +impl UiPropertyValueToEnum for TextAccessoryIcon { + fn convert(value: &UiPropertyValue) -> anyhow::Result { + match value { + UiPropertyValue::String(value) => Ok(TextAccessoryIcon::_1(Icons::from_str(value)?)), + UiPropertyValue::Bytes(value) => Ok(TextAccessoryIcon::_0(value.clone())), + UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number")), + UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool")), + UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object")), + UiPropertyValue::Array(_) => Err(anyhow!("unexpected type undefined")), + UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined")) } } } diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index ccb89fc..97ab05e 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -212,6 +212,7 @@ pub enum UiPropertyValue { Number(f64), Bool(bool), Bytes(bytes::Bytes), + Array(Vec), Object(HashMap), Undefined, } @@ -245,6 +246,11 @@ impl UiPropertyValue { None } } + pub fn as_array(&self) -> Option> { + // currently array is only used for component refs which are not properties + // implement if needed + unimplemented!() + } pub fn as_object(&self) -> Option { if let UiPropertyValue::Object(val) = self { Some(UiPropertyValueToStruct::convert(val).expect("invalid object")) diff --git a/rust/common/src/rpc/grpc_convert.rs b/rust/common/src/rpc/grpc_convert.rs index ca5f46a..f80d905 100644 --- a/rust/common/src/rpc/grpc_convert.rs +++ b/rust/common/src/rpc/grpc_convert.rs @@ -1,94 +1,6 @@ -use std::collections::HashMap; - -use anyhow::anyhow; - -use crate::model::{EntrypointId, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SearchResultEntrypointType, UiPropertyValue, UiWidget}; +use crate::model::{PluginPreference, PluginPreferenceUserData, PreferenceEnumValue}; use crate::rpc::grpc::rpc_ui_property_value::Value; -use crate::rpc::grpc::{RpcEntrypointTypeSearchResult, RpcEnumValue, RpcPluginPreference, RpcPluginPreferenceUserData, RpcPluginPreferenceValueType, RpcSearchResult, RpcUiPropertyValue, RpcUiPropertyValueObject, RpcUiWidget, RpcUiWidgetId}; - -pub(crate) fn ui_widget_to_rpc(value: UiWidget) -> RpcUiWidget { - let children = value.widget_children.into_iter() - .map(|child| ui_widget_to_rpc(child)) - .collect::>(); - - let widget_id = RpcUiWidgetId { - value: value.widget_id - }; - - RpcUiWidget { - widget_id: Some(widget_id), - widget_type: value.widget_type, - widget_properties: ui_property_values_to_rpc(value.widget_properties), - widget_children: children - } -} - -pub(crate) fn ui_widget_from_rpc(value: RpcUiWidget) -> anyhow::Result { - let children = value.widget_children.into_iter() - .map(|child| ui_widget_from_rpc(child)) - .collect::>>()?; - - let widget_id = value.widget_id - .ok_or(anyhow!("invalid value widget_id"))? - .value; - - Ok(UiWidget { - widget_id, - widget_type: value.widget_type, - widget_properties: ui_property_values_from_rpc(value.widget_properties)?, - widget_children: children, - }) -} - -fn ui_property_values_to_rpc(value: HashMap) -> HashMap { - value.into_iter() - .map(|(key, value)| (key, ui_property_value_to_rpc(value))) - .collect() -} - -pub(crate) fn ui_property_value_to_rpc(value: UiPropertyValue) -> RpcUiPropertyValue { - match value { - UiPropertyValue::String(value) => RpcUiPropertyValue { value: Some(Value::String(value)) }, - UiPropertyValue::Number(value) => RpcUiPropertyValue { value: Some(Value::Number(value)) }, - UiPropertyValue::Bool(value) => RpcUiPropertyValue { value: Some(Value::Bool(value)) }, - UiPropertyValue::Bytes(value) => RpcUiPropertyValue { value: Some(Value::Bytes(value.to_vec())) }, - UiPropertyValue::Object(value) => { - let value: HashMap = value.into_iter() - .map(|(name, value)| (name, ui_property_value_to_rpc(value))) - .collect(); - - RpcUiPropertyValue { value: Some(Value::Object(RpcUiPropertyValueObject { value })) } - } - UiPropertyValue::Undefined => RpcUiPropertyValue { value: Some(Value::Undefined(0)) }, - } -} - -fn ui_property_values_from_rpc(value: HashMap) -> anyhow::Result> { - let result = value - .into_iter() - .map(|(key, value)| Ok((key, ui_property_value_from_rpc(value)?))) - - .collect::>>()? - .into_iter() - .collect::>(); - - Ok(result) -} - -pub fn ui_property_value_from_rpc(value: RpcUiPropertyValue) -> anyhow::Result { - let value = value.value.ok_or(anyhow!("invalid property value"))?; - - let value = match value { - Value::Undefined(_) => UiPropertyValue::Undefined, - Value::String(value) => UiPropertyValue::String(value), - Value::Number(value) => UiPropertyValue::Number(value), - Value::Bool(value) => UiPropertyValue::Bool(value), - Value::Bytes(value) => UiPropertyValue::Bytes(bytes::Bytes::from(value)), - Value::Object(value) => UiPropertyValue::Object(ui_property_values_from_rpc(value.value)?) - }; - - Ok(value) -} +use crate::rpc::grpc::{RpcEnumValue, RpcPluginPreference, RpcPluginPreferenceUserData, RpcPluginPreferenceValueType, RpcUiPropertyValue}; pub fn plugin_preference_user_data_from_rpc(value: RpcPluginPreferenceUserData) -> PluginPreferenceUserData { let value_type: RpcPluginPreferenceValueType = value.r#type.try_into().unwrap(); @@ -492,43 +404,3 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc } } -pub fn ui_search_result_from_rpc(search_result: RpcSearchResult) -> SearchResult { - let entrypoint_type = search_result.entrypoint_type - .try_into() - .unwrap(); - - let entrypoint_type = match entrypoint_type { - RpcEntrypointTypeSearchResult::SrCommand => SearchResultEntrypointType::Command, - RpcEntrypointTypeSearchResult::SrView => SearchResultEntrypointType::View, - RpcEntrypointTypeSearchResult::SrGeneratedCommand => SearchResultEntrypointType::GeneratedCommand, - }; - - let icon_path = Some(search_result.entrypoint_icon_path) - .filter(|path| path != ""); - - SearchResult { - plugin_id: PluginId::from_string(search_result.plugin_id), - plugin_name: search_result.plugin_name, - entrypoint_id: EntrypointId::from_string(search_result.entrypoint_id), - entrypoint_name: search_result.entrypoint_name, - entrypoint_icon: icon_path, - entrypoint_type, - } -} - -pub fn ui_search_result_to_rpc(item: SearchResult) -> RpcSearchResult { - let entrypoint_type = match item.entrypoint_type { - SearchResultEntrypointType::Command => RpcEntrypointTypeSearchResult::SrCommand, - SearchResultEntrypointType::View => RpcEntrypointTypeSearchResult::SrView, - SearchResultEntrypointType::GeneratedCommand => RpcEntrypointTypeSearchResult::SrGeneratedCommand, - }; - - RpcSearchResult { - entrypoint_type: entrypoint_type.into(), - entrypoint_name: item.entrypoint_name, - entrypoint_id: item.entrypoint_id.to_string(), - entrypoint_icon_path: item.entrypoint_icon.unwrap_or_default(), - plugin_name: item.plugin_name, - plugin_id: item.plugin_id.to_string(), - } -} diff --git a/rust/common/src/scenario_convert.rs b/rust/common/src/scenario_convert.rs index b9e9390..32450f5 100644 --- a/rust/common/src/scenario_convert.rs +++ b/rust/common/src/scenario_convert.rs @@ -41,6 +41,13 @@ fn ui_property_value_to_scenario(value: UiPropertyValue) -> ScenarioUiPropertyVa ScenarioUiPropertyValue::Object(value) } + UiPropertyValue::Array(values) => { + let value: Vec<_> = values.into_iter() + .map(|value| ui_property_value_to_scenario(value)) + .collect(); + + ScenarioUiPropertyValue::Array(value) + } UiPropertyValue::Undefined => ScenarioUiPropertyValue::Undefined, } } @@ -77,6 +84,13 @@ fn ui_property_value_from_scenario(value: ScenarioUiPropertyValue) -> UiProperty UiPropertyValue::Object(value) } + ScenarioUiPropertyValue::Array(values) => { + let value: Vec<_> = values.into_iter() + .map(|value| ui_property_value_from_scenario(value)) + .collect(); + + UiPropertyValue::Array(value) + } ScenarioUiPropertyValue::Undefined => UiPropertyValue::Undefined, } } diff --git a/rust/common/src/scenario_model.rs b/rust/common/src/scenario_model.rs index 964571a..a5adcf0 100644 --- a/rust/common/src/scenario_model.rs +++ b/rust/common/src/scenario_model.rs @@ -24,6 +24,7 @@ pub enum ScenarioUiPropertyValue { #[serde(with="base64")] Bytes(Vec), Object(HashMap), + Array(Vec), Undefined, } diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index bb3c194..f4c1057 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -92,12 +92,78 @@ pub enum PropertyType { Union { items: Vec }, + #[serde(rename = "array")] + Array { + item: Box + }, #[serde(rename = "object")] Object { name: String, }, } +impl PropertyType { + pub fn is_in_children(&self) -> bool { + match self { + PropertyType::String => false, + PropertyType::Number => false, + PropertyType::Boolean => false, + PropertyType::Component { .. } => true, + PropertyType::Function { .. } => false, + PropertyType::ImageSource => false, + PropertyType::Enum { .. } => false, + PropertyType::Union { items } => { + let all_in = items.iter().all(|prop_type| prop_type.is_in_children()); + let any_in = items.iter().any(|prop_type| prop_type.is_in_children()); + + if all_in && any_in { + true + } else { + let all_not_in = items.iter().all(|prop_type| !prop_type.is_in_children()); + let any_not_in = items.iter().any(|prop_type| !prop_type.is_in_children()); + if all_not_in && any_not_in { + false + } else { + panic!("all items in union should be of the same kind") + } + } + } + PropertyType::Array { item } => item.is_in_children(), + PropertyType::Object { .. } => false, + } + } + + pub fn is_in_property(&self) -> bool { + match self { + PropertyType::String => true, + PropertyType::Number => true, + PropertyType::Boolean => true, + PropertyType::Component { .. } => false, + PropertyType::Function { .. } => false, + PropertyType::ImageSource => true, + PropertyType::Enum { .. } => true, + PropertyType::Union { items } => { + let all_in = items.iter().all(|prop_type| prop_type.is_in_property()); + let any_in = items.iter().any(|prop_type| prop_type.is_in_property()); + + if all_in && any_in { + true + } else { + let all_not_in = items.iter().all(|prop_type| !prop_type.is_in_property()); + let any_not_in = items.iter().any(|prop_type| !prop_type.is_in_property()); + if all_not_in && any_not_in { + false + } else { + panic!("all items in union should be of the same kind") + } + } + } + PropertyType::Array { item } => item.is_in_property(), + PropertyType::Object { .. } => true, + } + } +} + #[derive(Debug, Clone, Serialize)] #[serde(tag = "type")] pub enum SharedType { @@ -920,6 +986,29 @@ pub fn create_component_model() -> Vec { children_none(), ); + let accessory_text_component = component( + "accessory_text", + mark_doc!("/accessory_text/description.md"), + "TextAccessory", + [ + property("text", mark_doc!("/accessory_text/props/text.md"),false, PropertyType::String), + property("icon", mark_doc!("/accessory_text/props/icon.md"),true, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] }), + property("tooltip", mark_doc!("/accessory_text/props/tooltip.md"),true, PropertyType::String), + ], + children_none(), + ); + + let accessory_icon_component = component( + "accessory_icon", + mark_doc!("/accessory_icon/description.md"), + "IconAccessory", + [ + property("icon", mark_doc!("/accessory_icon/props/icon.md"),false, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] }), + property("tooltip", mark_doc!("/accessory_icon/props/tooltip.md"),true, PropertyType::String), + ], + children_none(), + ); + let list_item_component = component( "list_item", mark_doc!("/list_item/description.md"), @@ -928,7 +1017,7 @@ pub fn create_component_model() -> Vec { property("title", mark_doc!("/list_item/props/title.md"),false, PropertyType::String), property("subtitle", mark_doc!("/list_item/props/subtitle.md"),true, PropertyType::String), property("icon", mark_doc!("/list_item/props/icon.md"),true, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] }), - // accessories + property("accessories", mark_doc!("/list_item/props/accessories.md"),true, PropertyType::Array { item: Box::new(PropertyType::Union { items: vec![component_ref(&accessory_text_component), component_ref(&accessory_icon_component)]}) }), event("onClick", mark_doc!("/list_item/props/onClick.md"), true, []) ], children_none(), @@ -970,7 +1059,7 @@ pub fn create_component_model() -> Vec { [ property("title", mark_doc!("/grid_item/props/title.md"), true, PropertyType::String), property("subtitle", mark_doc!("/grid_item/props/subtitle.md"), true, PropertyType::String), - // accessories + property("accessory", mark_doc!("/grid_item/props/accessory.md"),true, component_ref(&accessory_icon_component)), event("onClick", mark_doc!("/grid_item/props/onClick.md"), true, []) ], children_members([ @@ -1129,6 +1218,10 @@ pub fn create_component_model() -> Vec { inline_component, empty_view_component, + + accessory_icon_component, + accessory_text_component, + list_item_component, list_section_component, list_component, diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 5758648..1a0089a 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -641,7 +641,7 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { UiPropertyValue::Number(value) => JsUiPropertyValue::Number { value }, UiPropertyValue::Bool(value) => JsUiPropertyValue::Bool { value }, UiPropertyValue::Undefined => JsUiPropertyValue::Undefined, - UiPropertyValue::Bytes(_) | UiPropertyValue::Object(_) => { + UiPropertyValue::Array(_) | UiPropertyValue::Bytes(_) | UiPropertyValue::Object(_) => { todo!() } }) diff --git a/rust/server/src/plugins/js/ui.rs b/rust/server/src/plugins/js/ui.rs index 1003bd2..dd5fba3 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/server/src/plugins/js/ui.rs @@ -197,6 +197,10 @@ fn from_js_to_intermediate_properties( return Err(anyhow!("unknown property encountered {:?}", name)) }; + if !property_type.is_in_property() { + return Err(anyhow!("unknown property encountered {:?}", name)) + } + convert(state.clone(), scope, property_type, name, val, shared_types) }) .collect::>>()?; @@ -295,6 +299,9 @@ fn convert( } } } + PropertyType::Array { .. } => { + unimplemented!() + } } } @@ -372,6 +379,9 @@ fn expected_type(prop_type: &PropertyType) -> String { .join(", ") }, PropertyType::Object { .. } => "object".to_owned(), + PropertyType::Array { .. } => { + unimplemented!() + } } } From 2bb6355f0ce553b4df7cf7aba60ce32ad236032b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:55:10 +0200 Subject: [PATCH 079/540] Fix panic when stopping disabled plugin --- rust/server/src/plugins/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 7035a4c..6b715e4 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -332,7 +332,10 @@ impl ApplicationManager { pub async fn remove_plugin(&self, plugin_id: PluginId) -> anyhow::Result<()> { tracing::info!(target = "plugin", "Removing plugin with id: {:?}", plugin_id); - self.stop_plugin(plugin_id.clone()).await; + let running = self.run_status_holder.is_plugin_running(&plugin_id); + if running { + self.stop_plugin(plugin_id.clone()).await; + } self.db_repository.remove_plugin(&plugin_id.to_string()).await?; self.search_index.remove_for_plugin(plugin_id)?; Ok(()) From 34c3cf0da6f59eb97f228941f2f4dfba750d48b3 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:24:37 +0200 Subject: [PATCH 080/540] Add Contribution section to README.md --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dd54426..9fd9b0e 100644 --- a/README.md +++ b/README.md @@ -399,9 +399,10 @@ Wayland support requires LayerShell protocol `zwlr_layer_shell_v1`. ## Building Gauntlet You will need: -- NodeJS v18 +- NodeJS - Rust - Protobuf Compiler +- CMake (not used by the project itself, but is required by a dependency) - On Linux: `libxkbcommon-dev` (note: name may differ depending on used distribution) To build dev run: @@ -423,6 +424,21 @@ cargo build --release --features release ``` But the new version release needs to be done via GitHub Actions +## Contributing + +If you'd like to help build Gauntlet you can do it in more ways than just contributing code: +- Reporting a bug or UI/UX problem +- Creating a plugin +- Creating and contributing a theme - see [#16](https://github.com/project-gauntlet/gauntlet/issues/16) + +If you are looking for things to do see pinned [issues](https://github.com/project-gauntlet/gauntlet/issues). + +For simple problems feel free to open an issue or PR and tackle it yourself! + +For more significant changes please contact creators on Discord (invite link on top of README) and discuss first. + +All and any contributions are welcome. + ## Versioning ### Application From 2ce02919a0937bca89f5035765105b9b3b0f0df4 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 23 Sep 2024 20:59:04 +0200 Subject: [PATCH 081/540] Fix spacing on section subtitle. Tweak text colors --- dev_plugin/src/detail-view.tsx | 2 +- rust/client/src/ui/theme/mod.rs | 207 ++++++++++++++++--------------- rust/client/src/ui/theme/row.rs | 2 + rust/client/src/ui/theme/text.rs | 8 ++ rust/client/src/ui/widget.rs | 12 +- 5 files changed, 126 insertions(+), 105 deletions(-) diff --git a/dev_plugin/src/detail-view.tsx b/dev_plugin/src/detail-view.tsx index 3e4ed73..fc66b0b 100644 --- a/dev_plugin/src/detail-view.tsx +++ b/dev_plugin/src/detail-view.tsx @@ -82,7 +82,7 @@ export default function DetailView(): ReactElement { return ( + { diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 3e2ff3a..3a53cf8 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -24,20 +24,20 @@ mod loading_bar; pub type Element<'a, Message> = iced::Element<'a, Message, GauntletTheme>; -const CURRENT_COLOR_THEME_VERSION: u64 = 2; +const CURRENT_COLOR_THEME_VERSION: u64 = 3; const CURRENT_THEME_VERSION: u64 = 3; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GauntletColorTheme { version: u64, - background_color: ThemeColor, - background_overlay_color: ThemeColor, - background_light_color: ThemeColor, + background_darkest_color: ThemeColor, + background_darker_color: ThemeColor, background_lighter_color: ThemeColor, - text_color: ThemeColor, - text_hovered_color: ThemeColor, + background_lightest_color: ThemeColor, + text_lightest_color: ThemeColor, + text_lighter_color: ThemeColor, text_darker_color: ThemeColor, - text_dark_color: ThemeColor, + text_darkest_color: ThemeColor, primary_color: ThemeColor, primary_hovered_color: ThemeColor, date_picker_text_darker: ThemeColor @@ -79,13 +79,15 @@ pub struct GauntletTheme { grid_item: ThemeButton, grid_item_title: ThemePaddingTextColor, grid_item_subtitle: ThemeTextColor, - grid_section_title: ThemePaddingTextColor, + grid_section_title: ThemePaddingTextColorSpacing, + grid_section_subtitle: ThemeTextColor, inline: ThemePaddingOnly, list_item: ThemeButton, list_item_subtitle: ThemePaddingTextColor, list_item_title: ThemePaddingOnly, list_item_icon: ThemePaddingOnly, - list_section_title: ThemePaddingTextColor, + list_section_title: ThemePaddingTextColorSpacing, + list_section_subtitle: ThemeTextColor, main_list: ThemePaddingOnly, main_list_inner: ThemePaddingOnly, main_list_item: ThemeButton, @@ -115,7 +117,7 @@ pub struct GauntletTheme { scrollbar: ThemeScrollbar, tooltip: ThemeTooltip, loading_bar: ThemeLoadingBar, - text_accessory: ThemeTextAccessory, + text_accessory: ThemePaddingTextColorSpacing, icon_accessory: ThemeIconAccessory, } @@ -204,14 +206,14 @@ impl GauntletTheme { pub fn default_color_theme() -> GauntletColorTheme { GauntletColorTheme { version: CURRENT_COLOR_THEME_VERSION, - background_color: BACKGROUND, - background_overlay_color: BACKGROUND_OVERLAY, - background_light_color: BACKGROUND_LIGHT, + background_lightest_color: BACKGROUND_LIGHTEST, background_lighter_color: BACKGROUND_LIGHTER, - text_color: TEXT, - text_hovered_color: TEXT_HOVERED, + background_darker_color: BACKGROUND_DARKER, + background_darkest_color: BACKGROUND_DARKEST, + text_lightest_color: TEXT_LIGHTEST, + text_lighter_color: TEXT_LIGHTER, text_darker_color: TEXT_DARKER, - text_dark_color: TEXT_DARK, + text_darkest_color: TEXT_DARKEST, primary_color: PRIMARY, primary_hovered_color: PRIMARY_HOVERED, date_picker_text_darker: DATE_PICKER_TEXT_DARKER @@ -221,14 +223,15 @@ impl GauntletTheme { pub fn default_theme(color_theme: GauntletColorTheme) -> GauntletTheme { let GauntletColorTheme { version: _, - background_color, - background_overlay_color, - background_light_color, + // background_darkest_2_color, + background_darkest_color, + background_darker_color, background_lighter_color, - text_color, - text_hovered_color, + background_lightest_color, + text_lightest_color, + text_lighter_color, text_darker_color, - text_dark_color, + text_darkest_color, primary_color, primary_hovered_color, date_picker_text_darker @@ -236,16 +239,16 @@ impl GauntletTheme { GauntletTheme { version: CURRENT_THEME_VERSION, - text: text_color, + text: text_lightest_color, root: ThemeRoot { - background_color, + background_color: background_darkest_color, border_radius: 10.0, border_width: 1.0, - border_color: background_light_color, + border_color: background_lighter_color, }, action_panel: ThemePaddingBackgroundColor { padding: padding_all(8.0), - background_color: background_overlay_color, + background_color: background_darker_color, }, action_panel_title: ThemePaddingOnly { padding: padding(2.0, 8.0, 4.0, 8.0), @@ -253,9 +256,9 @@ impl GauntletTheme { action: ThemeButton { padding: padding_all(8.0), background_color: TRANSPARENT, - background_color_hovered: background_light_color, - text_color, - text_color_hovered: text_color, + background_color_hovered: background_lighter_color, + text_color: text_lightest_color, + text_color_hovered: text_lightest_color, border_radius: BUTTON_BORDER_RADIUS, border_width: 0.0, border_color: TRANSPARENT, @@ -266,7 +269,7 @@ impl GauntletTheme { action_shortcut_modifier: ThemeActionShortcutModifier { padding: padding_axis(0.0, 8.0), spacing: 8.0, - background_color: background_lighter_color, + background_color: background_lightest_color, border_radius: 4.0, border_width: 0.0, border_color: TRANSPARENT, @@ -281,8 +284,8 @@ impl GauntletTheme { padding: padding_axis(2.0, 8.0), background_color: primary_color, background_color_hovered: primary_hovered_color, - text_color: text_dark_color, - text_color_hovered: text_dark_color, + text_color: text_darkest_color, + text_color_hovered: text_darkest_color, border_radius: BUTTON_BORDER_RADIUS, border_width: 0.0, border_color: TRANSPARENT, @@ -300,7 +303,7 @@ impl GauntletTheme { }, list_item_subtitle: ThemePaddingTextColor { padding: padding_all(4.0), - text_color: text_darker_color, + text_color: text_lighter_color, }, list_item_title: ThemePaddingOnly { padding: padding_all(4.0), @@ -327,27 +330,27 @@ impl GauntletTheme { }, grid_item: ThemeButton { padding: padding_all(8.0), - background_color: background_light_color, - background_color_hovered: background_lighter_color, - text_color, - text_color_hovered: text_color, + background_color: background_lighter_color, + background_color_hovered: background_lightest_color, + text_color: text_lightest_color, + text_color_hovered: text_lightest_color, border_radius: BUTTON_BORDER_RADIUS, border_width: 0.0, border_color: TRANSPARENT, }, grid_item_title: ThemePaddingTextColor { padding: padding_axis(4.0, 0.0), - text_color, + text_color: text_lightest_color, }, grid_item_subtitle: ThemeTextColor { - text_color: text_darker_color, + text_color: text_lighter_color, }, content_horizontal_break: ThemePaddingOnly { padding: padding_axis(8.0, 0.0), }, content_code_block_text: ThemeCode { padding: padding_axis(4.0, 8.0), - background_color: background_light_color, + background_color: background_lighter_color, border_radius: 4.0, border_width: 0.0, border_color: TRANSPARENT, @@ -360,24 +363,24 @@ impl GauntletTheme { }, root_top_panel_button: ThemeButton { padding: padding_axis(3.0, 5.0), - background_color: background_light_color, - background_color_hovered: background_lighter_color, - text_color, - text_color_hovered: text_color, + background_color: background_lighter_color, + background_color_hovered: background_lightest_color, + text_color: text_lightest_color, + text_color_hovered: text_lightest_color, border_radius: 6.0, border_width: 0.0, border_color: TRANSPARENT, }, root_bottom_panel: ThemePaddingBackgroundColor { padding: padding_axis(6.0, 8.0), - background_color: background_overlay_color, + background_color: background_darker_color, }, root_bottom_panel_action_button: ThemeButton { padding: padding_axis(3.0, 5.0), background_color: TRANSPARENT, - background_color_hovered: background_light_color, - text_color, - text_color_hovered: text_color, + background_color_hovered: background_lighter_color, + text_color: text_lightest_color, + text_color_hovered: text_lightest_color, border_radius: 6.0, border_width: 0.0, border_color: TRANSPARENT, @@ -388,9 +391,9 @@ impl GauntletTheme { list_item: ThemeButton { padding: padding_all(5.0), background_color: TRANSPARENT, - background_color_hovered: background_light_color, - text_color, - text_color_hovered: text_color, + background_color_hovered: background_lighter_color, + text_color: text_lightest_color, + text_color_hovered: text_lightest_color, border_radius: BUTTON_BORDER_RADIUS, border_width: 0.0, border_color: TRANSPARENT, @@ -435,20 +438,28 @@ impl GauntletTheme { form_input_label: ThemePaddingOnly { padding: padding_axis(4.0, 12.0), }, - list_section_title: ThemePaddingTextColor { + list_section_title: ThemePaddingTextColorSpacing { padding: padding(12.0, 8.0, 4.0, 8.0), - text_color: text_darker_color, + text_color: text_lighter_color, + spacing: 8.0, }, - grid_section_title: ThemePaddingTextColor { + list_section_subtitle: ThemeTextColor { + text_color: text_darker_color + }, + grid_section_title: ThemePaddingTextColorSpacing { padding: padding(12.0, 0.0, 4.0, 0.0), - text_color: text_darker_color, + text_color: text_lighter_color, + spacing: 8.0, + }, + grid_section_subtitle: ThemeTextColor { + text_color: text_darker_color }, main_list_item: ThemeButton { padding: padding_all(5.0), background_color: TRANSPARENT, - background_color_hovered: background_light_color, - text_color, - text_color_hovered: text_color, + background_color_hovered: background_lighter_color, + text_color: text_lightest_color, + text_color_hovered: text_lightest_color, border_radius: BUTTON_BORDER_RADIUS, border_width: 0.0, border_color: TRANSPARENT, @@ -482,76 +493,76 @@ impl GauntletTheme { padding: padding_all(12.0), }, metadata_link: ThemeLink { - text_color, - text_color_hovered: text_hovered_color, + text_color: text_lightest_color, + text_color_hovered: text_lighter_color, }, empty_view_subtitle: ThemeTextColor { text_color: text_darker_color, }, form_input_date_picker: ThemeDatePicker { - background_color, + background_color: background_darkest_color, border_radius: 10.0, border_width: 1.0, - border_color: background_light_color, - text_color, + border_color: background_lighter_color, + text_color: text_lightest_color, text_color_selected: text_darker_color, text_color_hovered: text_darker_color, text_attenuated_color: date_picker_text_darker, - day_background_color: background_light_color, - day_background_color_selected: background_light_color, - day_background_color_hovered: background_light_color, + day_background_color: background_lighter_color, + day_background_color_selected: background_lighter_color, + day_background_color_hovered: background_lighter_color, }, form_input_date_picker_buttons: ThemeButton { padding: padding_all(8.0), background_color: primary_color, background_color_hovered: primary_hovered_color, - text_color: text_dark_color, - text_color_hovered: text_dark_color, + text_color: text_darkest_color, + text_color_hovered: text_darkest_color, border_radius: BUTTON_BORDER_RADIUS, border_width: 0.0, border_color: TRANSPARENT, }, form_input_checkbox: ThemeCheckbox { background_color_checked: primary_color, - background_color_unchecked: background_color, + background_color_unchecked: background_darkest_color, background_color_checked_hovered: primary_hovered_color, - background_color_unchecked_hovered: background_light_color, + background_color_unchecked_hovered: background_lighter_color, border_radius: 4.0, border_width: 1.0, border_color: primary_color, - icon_color: background_color, + icon_color: background_darkest_color, }, form_input_select: ThemeSelect { background_color: primary_color, background_color_hovered: primary_hovered_color, - text_color: text_dark_color, - text_color_hovered: text_dark_color, + text_color: text_darkest_color, + text_color_hovered: text_darkest_color, border_radius: BUTTON_BORDER_RADIUS, border_width: 1.0, - border_color: background_light_color, + border_color: background_lighter_color, }, form_input_select_menu: ThemeSelectMenu { - background_color, - background_color_selected: background_light_color, - text_color, - text_color_selected: text_color, + background_color: background_darkest_color, + background_color_selected: background_lighter_color, + text_color: text_lightest_color, + text_color_selected: text_lightest_color, border_radius: BUTTON_BORDER_RADIUS, border_width: 1.0, - border_color: background_light_color, + border_color: background_lighter_color, }, form_input_text_field: ThemeTextField { background_color: TRANSPARENT, - background_color_hovered: background_light_color, - text_color, + background_color_hovered: background_lighter_color, + text_color: text_lightest_color, text_color_placeholder: text_darker_color, - selection_color: background_light_color, + selection_color: background_lighter_color, border_radius: 4.0, border_width: 1.0, - border_color: background_light_color, - border_color_hovered: background_light_color, + border_color: background_lighter_color, + border_color_hovered: background_lighter_color, }, separator: ThemeSeparator { - color: background_light_color + color: background_lighter_color }, scrollbar: ThemeScrollbar { color: primary_color, @@ -561,20 +572,20 @@ impl GauntletTheme { }, tooltip: ThemeTooltip { padding: 8.0, - background_color: background_overlay_color, + background_color: background_darker_color, }, loading_bar: ThemeLoadingBar { loading_bar_color: primary_color, - background_color: background_light_color, + background_color: background_lighter_color, }, - text_accessory: ThemeTextAccessory { + text_accessory: ThemePaddingTextColorSpacing { padding: padding(4.0, 4.0, 4.0, 16.0), - text_color: text_darker_color, + text_color: text_lighter_color, spacing: 8.0, }, icon_accessory: ThemeIconAccessory { padding: padding(4.0, 4.0, 4.0, 16.0), - icon_color: text_darker_color, + icon_color: text_lighter_color, }, } } @@ -594,14 +605,14 @@ const NOT_INTENDED_TO_BE_USED: ThemeColor = ThemeColor::new(0xAF5BFF, 1.0); // keep colors more or less in sync with settings ui const TRANSPARENT: ThemeColor = ThemeColor::new(0x000000, 0.0); -const BACKGROUND: ThemeColor = ThemeColor::new(0x2C323A, 1.0); -const BACKGROUND_OVERLAY: ThemeColor = ThemeColor::new(0x333a42, 1.0); -const BACKGROUND_LIGHT: ThemeColor = ThemeColor::new(0x48505B, 0.5); -const BACKGROUND_LIGHTER: ThemeColor = ThemeColor::new(0x626974, 0.3); -const TEXT: ThemeColor = ThemeColor::new(0xBFC5CB, 1.0); -const TEXT_HOVERED: ThemeColor = ThemeColor::new(0xDDDFE1, 1.0); +const BACKGROUND_LIGHTEST: ThemeColor = ThemeColor::new(0x626974, 0.3); +const BACKGROUND_LIGHTER: ThemeColor = ThemeColor::new(0x48505B, 0.5); +const BACKGROUND_DARKER: ThemeColor = ThemeColor::new(0x333a42, 1.0); +const BACKGROUND_DARKEST: ThemeColor = ThemeColor::new(0x2C323A, 1.0); +const TEXT_LIGHTEST: ThemeColor = ThemeColor::new(0xDDDFE1, 1.0); +const TEXT_LIGHTER: ThemeColor = ThemeColor::new(0x9AA0A6, 1.0); const TEXT_DARKER: ThemeColor = ThemeColor::new(0x6B7785, 1.0); -const TEXT_DARK: ThemeColor = ThemeColor::new(0x1D242C, 1.0); +const TEXT_DARKEST: ThemeColor = ThemeColor::new(0x1D242C, 1.0); const PRIMARY: ThemeColor = ThemeColor::new(0xC79F60, 1.0); const PRIMARY_HOVERED: ThemeColor = ThemeColor::new(0xD7B37A, 1.0); const DATE_PICKER_TEXT_DARKER: ThemeColor = ThemeColor::new(0xCAC2B6, 0.3); @@ -831,7 +842,7 @@ pub struct ThemeIconAccessory { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ThemeTextAccessory { +pub struct ThemePaddingTextColorSpacing { padding: ThemePadding, text_color: ThemeColor, spacing: f32, diff --git a/rust/client/src/ui/theme/row.rs b/rust/client/src/ui/theme/row.rs index c43cc1d..b2f50aa 100644 --- a/rust/client/src/ui/theme/row.rs +++ b/rust/client/src/ui/theme/row.rs @@ -25,9 +25,11 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletT } RowStyle::ListSectionTitle => { self.padding(theme.list_section_title.padding.to_iced()) + .spacing(theme.list_section_title.spacing) } RowStyle::GridSectionTitle => { self.padding(theme.grid_section_title.padding.to_iced()) + .spacing(theme.grid_section_title.spacing) } RowStyle::GridItemTitle => { self.padding(theme.grid_item_title.padding.to_iced()) diff --git a/rust/client/src/ui/theme/text.rs b/rust/client/src/ui/theme/text.rs index d9c05ba..71f6cfd 100644 --- a/rust/client/src/ui/theme/text.rs +++ b/rust/client/src/ui/theme/text.rs @@ -12,7 +12,9 @@ pub enum TextStyle { EmptyViewSubtitle, ListItemSubtitle, ListSectionTitle, + ListSectionSubtitle, GridSectionTitle, + GridSectionSubtitle, MainListItemSubtext, MetadataItemLabel, TextAccessory, @@ -56,9 +58,15 @@ impl text::StyleSheet for GauntletTheme { TextStyle::ListSectionTitle => Appearance { color: Some(self.list_section_title.text_color.to_iced()), }, + TextStyle::ListSectionSubtitle => Appearance { + color: Some(self.list_section_subtitle.text_color.to_iced()), + }, TextStyle::GridSectionTitle => Appearance { color: Some(self.grid_section_title.text_color.to_iced()), }, + TextStyle::GridSectionSubtitle => Appearance{ + color: Some(self.grid_section_subtitle.text_color.to_iced()), + }, TextStyle::MainListItemSubtext => Appearance { color: Some(self.main_list_item_sub_text.text_color.to_iced()), }, diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 53569a1..96de5f3 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1031,7 +1031,7 @@ impl ComponentWidgetWrapper { let content = column(content) .into(); - render_section(content, Some(title), subtitle, RowStyle::ListSectionTitle, TextStyle::ListSectionTitle) + render_section(content, Some(title), subtitle, RowStyle::ListSectionTitle, TextStyle::ListSectionTitle, TextStyle::ListSectionSubtitle) } ComponentWidget::List { children, isLoading: is_loading } => { let ComponentWidgetState::List { show_action_panel } = *state else { @@ -1179,7 +1179,7 @@ impl ComponentWidgetWrapper { ComponentWidget::GridSection { children, title, subtitle, columns } => { let content = render_grid(children, columns, context); - render_section(content, Some(title), subtitle, RowStyle::GridSectionTitle, TextStyle::GridSectionTitle) + render_section(content, Some(title), subtitle, RowStyle::GridSectionTitle, TextStyle::GridSectionTitle, TextStyle::GridSectionSubtitle) } ComponentWidget::Grid { children, columns, isLoading: is_loading } => { let ComponentWidgetState::Grid { show_action_panel } = *state else { @@ -1331,13 +1331,13 @@ fn render_grid<'a>(children: &[ComponentWidgetWrapper], /*aspect_ratio: Option<& grid } -fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option<&str>, subtitle: &Option, theme_kind_title: RowStyle, theme_kind_title_text: TextStyle) -> Element<'a, ComponentWidgetEvent> { +fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option<&str>, subtitle: &Option, theme_kind_title: RowStyle, theme_kind_title_text: TextStyle, theme_kind_subtitle_text: TextStyle) -> Element<'a, ComponentWidgetEvent> { let mut title_content = vec![]; if let Some(title) = title { let title: Element<_> = text(title) .size(15) - .themed(theme_kind_title_text.clone()); + .themed(theme_kind_title_text); title_content.push(title) } @@ -1345,7 +1345,7 @@ fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option< if let Some(subtitle) = subtitle { // TODO remove ? let subtitle: Element<_> = text(subtitle) .size(15) - .themed(theme_kind_title_text); + .themed(theme_kind_subtitle_text); title_content.push(subtitle) } @@ -1454,7 +1454,7 @@ fn render_root<'a>( }; floating_element(content, action_panel_element) - .offset(Offset::from([8.0, 40.0])) // TODO calculate based on theme + .offset(Offset::from([8.0, 48.0])) // TODO calculate based on theme .hide(hide_action_panel) .into() } From 2bca56a1d1590046ca4f249ac306fede7f6584e3 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 24 Sep 2024 20:26:12 +0200 Subject: [PATCH 082/540] Add plugin name and entrypoint name over inline view. Refine inline view styling. --- bundled_plugins/calculator/src/default.tsx | 8 ++-- rust/client/src/ui/client_context.rs | 6 +-- rust/client/src/ui/inline_view_container.rs | 6 ++- rust/client/src/ui/mod.rs | 23 +++++++++-- rust/client/src/ui/theme/container.rs | 29 ++++++++++++-- rust/client/src/ui/theme/mod.rs | 29 +++++++++++++- rust/client/src/ui/theme/text.rs | 13 +++++++ rust/client/src/ui/widget.rs | 39 ++++++++++++------- rust/client/src/ui/widget_container.rs | 18 ++++++++- rust/common/src/model.rs | 2 + rust/common/src/rpc/frontend_api.rs | 4 ++ rust/scenario_runner/src/frontend_mock.rs | 2 +- rust/server/src/model.rs | 1 + rust/server/src/plugins/js/mod.rs | 42 +++++++++++++++++---- rust/server/src/plugins/js/ui.rs | 11 +++++- rust/server/src/plugins/mod.rs | 8 ++++ 16 files changed, 199 insertions(+), 42 deletions(-) diff --git a/bundled_plugins/calculator/src/default.tsx b/bundled_plugins/calculator/src/default.tsx index e0cb726..c93ad89 100644 --- a/bundled_plugins/calculator/src/default.tsx +++ b/bundled_plugins/calculator/src/default.tsx @@ -26,15 +26,15 @@ export default function Default(props: { text: string }): ReactNode | undefined return ( - + {left} - + - + {right} - + ) diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index b3b19d7..fe37440 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -55,10 +55,10 @@ impl ClientContext { self.view.get_entrypoint_id() } - pub fn replace_view(&mut self, render_location: UiRenderLocation, container: UiWidget, plugin_id: &PluginId, entrypoint_id: &EntrypointId) { + pub fn replace_view(&mut self, render_location: UiRenderLocation, container: UiWidget, plugin_id: &PluginId, plugin_name: &str, entrypoint_id: &EntrypointId, entrypoint_name: &str) { match render_location { - UiRenderLocation::InlineView => self.get_mut_inline_view_container(plugin_id).replace_view(container, plugin_id, entrypoint_id), - UiRenderLocation::View => self.get_mut_view_container().replace_view(container, plugin_id, entrypoint_id) + UiRenderLocation::InlineView => self.get_mut_inline_view_container(plugin_id).replace_view(container, plugin_id, plugin_name, entrypoint_id, entrypoint_name), + UiRenderLocation::View => self.get_mut_view_container().replace_view(container, plugin_id, plugin_name, entrypoint_id, entrypoint_name) } } diff --git a/rust/client/src/ui/inline_view_container.rs b/rust/client/src/ui/inline_view_container.rs index a81a1a3..275bfd4 100644 --- a/rust/client/src/ui/inline_view_container.rs +++ b/rust/client/src/ui/inline_view_container.rs @@ -58,8 +58,10 @@ impl Component for InlineViewContainer { let containers = client_context.get_all_inline_view_containers(); if let Some((_, container)) = &containers.get(state.current_plugin) { - container.render_widget(ComponentRenderContext::None) - .map(InlineViewContainerEvent::WidgetEvent) + container.render_widget(ComponentRenderContext::InlineRoot { + plugin_name: container.get_plugin_name(), + entrypoint_name: container.get_entrypoint_name(), + }).map(InlineViewContainerEvent::WidgetEvent) } else { horizontal_space() .into() diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 8b66a24..ce0f513 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -322,7 +322,9 @@ impl Application for AppModel { ui_render_location_from_scenario(render_location), ui_widget_from_scenario(container), &plugin_id, - &entrypoint_id + "Screenshot Plugin", + &entrypoint_id, + "Screenshot Entrypoint", ); commands.push(Command::perform(async move { top_level_view }, |top_level_view| AppMsg::ReplaceView { top_level_view })); @@ -1299,8 +1301,23 @@ async fn request_loop( let mut client_context = client_context.write().expect("lock is poisoned"); match request_data { - UiRequestData::ReplaceView { plugin_id, entrypoint_id, render_location, top_level_view, container } => { - client_context.replace_view(render_location, container, &plugin_id, &entrypoint_id); + UiRequestData::ReplaceView { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + render_location, + top_level_view, + container + } => { + client_context.replace_view( + render_location, + container, + &plugin_id, + &plugin_name, + &entrypoint_id, + &entrypoint_name + ); responder.respond(UiResponseData::Nothing); diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 219eb35..b051070 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -54,6 +54,8 @@ pub enum ContainerStyle { TextAccessory, TextAccessoryIcon, IconAccessory, + InlineInner, + InlineName, } #[derive(Default)] @@ -70,6 +72,7 @@ pub enum ContainerStyleInner { Root, ContentImage, RootBottomPanel, + InlineInner, } @@ -202,6 +205,19 @@ impl container::StyleSheet for GauntletTheme { ..Appearance::default() } } + ContainerStyleInner::InlineInner => { + let theme = &self.inline_inner; + + Appearance { + background: Some(theme.background_color.to_iced().into()), + border: Border { + radius: theme.border_radius.into(), + width: theme.border_width, + color: theme.border_color.to_iced(), + }, + ..Appearance::default() + } + } } } } @@ -297,10 +313,17 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau self.padding(theme.form_input_label.padding.to_iced()) } ContainerStyle::Inline => { + self.padding(theme.inline.padding.to_iced()) + } + ContainerStyle::InlineInner => { self - .padding(theme.inline.padding.to_iced()) - .height(100) - .max_height(100) + .height(120) + .max_height(120) + .padding(theme.inline_inner.padding.to_iced()) + .style(ContainerStyleInner::InlineInner) + } + ContainerStyle::InlineName => { + self.padding(theme.inline_name.padding.to_iced()) } ContainerStyle::EmptyViewImage => { self.padding(theme.empty_view_image.padding.to_iced()) diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 3a53cf8..7dce43b 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -82,6 +82,9 @@ pub struct GauntletTheme { grid_section_title: ThemePaddingTextColorSpacing, grid_section_subtitle: ThemeTextColor, inline: ThemePaddingOnly, + inline_inner: ThemeInline, + inline_name: ThemePaddingTextColor, + inline_separator: ThemeTextColor, list_item: ThemeButton, list_item_subtitle: ThemePaddingTextColor, list_item_title: ThemePaddingOnly, @@ -223,7 +226,6 @@ impl GauntletTheme { pub fn default_theme(color_theme: GauntletColorTheme) -> GauntletTheme { let GauntletColorTheme { version: _, - // background_darkest_2_color, background_darkest_color, background_darker_color, background_lighter_color, @@ -319,7 +321,21 @@ impl GauntletTheme { border_radius: 6.0, }, inline: ThemePaddingOnly { - padding: padding_all(8.0) + padding: padding_axis(0.0, 8.0), + }, + inline_name: ThemePaddingTextColor { + padding: padding_all(8.0), + text_color: text_lighter_color, + }, + inline_separator: ThemeTextColor { + text_color: text_lighter_color, + }, + inline_inner: ThemeInline { + padding: padding_all(8.0), + background_color: background_lighter_color, + border_radius: BUTTON_BORDER_RADIUS, + border_width: 0.0, + border_color: TRANSPARENT, }, empty_view_image: ThemePaddingSize { padding: padding_all(8.0), @@ -762,6 +778,15 @@ pub struct ThemeCode { border_color: ThemeColor, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThemeInline { + padding: ThemePadding, + background_color: ThemeColor, + border_radius: f32, + border_width: f32, + border_color: ThemeColor, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ThemeDatePicker { background_color: ThemeColor, diff --git a/rust/client/src/ui/theme/text.rs b/rust/client/src/ui/theme/text.rs index 71f6cfd..c404b55 100644 --- a/rust/client/src/ui/theme/text.rs +++ b/rust/client/src/ui/theme/text.rs @@ -21,6 +21,8 @@ pub enum TextStyle { IconAccessory, GridItemTitle, GridItemSubTitle, + InlineName, + InlineSeparator, } impl<'a, Message: 'a> ThemableWidget<'a, Message> for Text<'a, GauntletTheme, Renderer> { @@ -35,6 +37,11 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Text<'a, GauntletTheme, Re .size(theme.metadata_item_label.text_size) .into() } + TextStyle::InlineName => { + self.size(15) + .style(kind) + .into() + } _ => { self.style(kind) .into() @@ -84,6 +91,12 @@ impl text::StyleSheet for GauntletTheme { }, TextStyle::GridItemSubTitle => Appearance { color: Some(self.grid_item_subtitle.text_color.to_iced()), + }, + TextStyle::InlineName => Appearance { + color: Some(self.inline_name.text_color.to_iced()), + }, + TextStyle::InlineSeparator => Appearance { + color: Some(self.inline_separator.text_color.to_iced()), } } } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 96de5f3..ec8c181 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -4,6 +4,8 @@ use std::str::FromStr; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; use anyhow::anyhow; +use common::model::{PhysicalKey, PhysicalShortcut, PluginId, UiPropertyValue, UiPropertyValueToEnum, UiPropertyValueToStruct, UiWidgetId}; +use common_ui::shortcut_to_text; use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; use iced::widget::image::Handle; @@ -17,9 +19,6 @@ use iced_aw::helpers::{date_picker, grid, grid_row, wrap_horizontal}; use iced_aw::{floating_element, GridRow}; use itertools::Itertools; -use common::model::{PhysicalKey, PhysicalShortcut, PluginId, UiPropertyValue, UiPropertyValueToEnum, UiPropertyValueToStruct, UiWidgetId}; -use common_ui::shortcut_to_text; - use crate::model::UiViewEvent; use crate::ui::custom_widgets::loading_bar::LoadingBar; use crate::ui::theme::button::ButtonStyle; @@ -130,6 +129,10 @@ pub enum ComponentRenderContext { H4, H5, H6, + InlineRoot { + plugin_name: String, + entrypoint_name: String, + }, Inline, GridItem, List { @@ -781,8 +784,8 @@ impl ComponentWidgetWrapper { let icon = text(icon_to_bootstrap(icon)) .font(icons::BOOTSTRAP_FONT) - .size(30) - .into(); + .size(45) + .themed(TextStyle::InlineSeparator); let bot_rule: Element<_> = vertical_rule(1) .into(); @@ -798,6 +801,16 @@ impl ComponentWidgetWrapper { } } ComponentWidget::Inline { children } => { + let ComponentRenderContext::InlineRoot { plugin_name, entrypoint_name } = context else { + panic!("not supposed to be passed to root item: {:?}", context) + }; + + let name: Element<_> = text(format!("{} - {}", plugin_name, entrypoint_name)) + .themed(TextStyle::InlineName); + + let name: Element<_> = container(name) + .themed(ContainerStyle::InlineName); + let content: Vec> = children .into_iter() .map(|child| { @@ -823,17 +836,16 @@ impl ComponentWidgetWrapper { .into(); let content: Element<_> = container(content) - .center_x() - .center_y() - .themed(ContainerStyle::Inline); + .themed(ContainerStyle::InlineInner); - let rule: Element<_> = horizontal_rule(1) - .into(); - - let content: Element<_> = column([content, rule]) + let content: Element<_> = column(vec![name, content]) .width(Length::Fill) .into(); + let content: Element<_> = container(content) + .width(Length::Fill) + .themed(ContainerStyle::Inline); + content } ComponentWidget::EmptyView { title, description, image: empty_view_image } => { @@ -1474,7 +1486,8 @@ fn render_text_part<'a>(value: &str, context: ComponentRenderContext) -> Element ComponentRenderContext::ActionPanel { .. } => panic!("not supposed to be passed to text part"), ComponentRenderContext::Action { .. } => panic!("not supposed to be passed to text part"), ComponentRenderContext::Inline => panic!("not supposed to be passed to text part"), - ComponentRenderContext::GridItem => panic!("not supposed to be passed to text part") + ComponentRenderContext::GridItem => panic!("not supposed to be passed to text part"), + ComponentRenderContext::InlineRoot { .. } => panic!("not supposed to be passed to text part") }; let mut text = text(value); diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 80c98e0..4363141 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -7,7 +7,9 @@ use crate::ui::widget::{ComponentRenderContext, ComponentWidgetEvent, ComponentW pub struct PluginWidgetContainer { root_widget: ComponentWidgetWrapper, plugin_id: Option, - entrypoint_id: Option + plugin_name: Option, + entrypoint_id: Option, + entrypoint_name: Option } impl PluginWidgetContainer { @@ -15,7 +17,9 @@ impl PluginWidgetContainer { Self { root_widget: ComponentWidgetWrapper::root(0), plugin_id: None, + plugin_name: None, entrypoint_id: None, + entrypoint_name: None, } } @@ -23,10 +27,18 @@ impl PluginWidgetContainer { self.plugin_id.clone().expect("plugin id should always exist after render") } + pub fn get_plugin_name(&self) -> String { + self.plugin_name.clone().expect("plugin id should always exist after render") + } + pub fn get_entrypoint_id(&self) -> EntrypointId { self.entrypoint_id.clone().expect("entrypoint id should always exist after render") } + pub fn get_entrypoint_name(&self) -> String { + self.entrypoint_name.clone().expect("entrypoint id should always exist after render") + } + pub fn render_widget<'a>(&self, context: ComponentRenderContext) -> Element<'a, ComponentWidgetEvent> { self.root_widget.render_widget(context) } @@ -41,11 +53,13 @@ impl PluginWidgetContainer { .expect("unable to create widget") } - pub fn replace_view(&mut self, container: UiWidget, plugin_id: &PluginId, entrypoint_id: &EntrypointId) { + pub fn replace_view(&mut self, container: UiWidget, plugin_id: &PluginId, plugin_name: &str, entrypoint_id: &EntrypointId, entrypoint_name: &str) { tracing::trace!("replace_view is called. container: {:?}", container); self.plugin_id = Some(plugin_id.clone()); + self.plugin_name = Some(plugin_name.to_string()); self.entrypoint_id = Some(entrypoint_id.clone()); + self.entrypoint_name = Some(entrypoint_name.to_string()); let children = container.widget_children.into_iter() .map(|child| self.create_component_widget(child)) diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 97ab05e..d78baed 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -121,7 +121,9 @@ pub enum UiRequestData { }, ReplaceView { plugin_id: PluginId, + plugin_name: String, entrypoint_id: EntrypointId, + entrypoint_name: String, render_location: UiRenderLocation, top_level_view: bool, container: UiWidget, diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index 70873d2..2508a28 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -38,14 +38,18 @@ impl FrontendApi { pub async fn replace_view( &mut self, plugin_id: PluginId, + plugin_name: String, entrypoint_id: EntrypointId, + entrypoint_name: String, render_location: UiRenderLocation, top_level_view: bool, container: UiWidget, ) -> Result<(), FrontendApiError> { let request = UiRequestData::ReplaceView { plugin_id, + plugin_name, entrypoint_id, + entrypoint_name, render_location, top_level_view, container, diff --git a/rust/scenario_runner/src/frontend_mock.rs b/rust/scenario_runner/src/frontend_mock.rs index e52fc76..228f6a6 100644 --- a/rust/scenario_runner/src/frontend_mock.rs +++ b/rust/scenario_runner/src/frontend_mock.rs @@ -158,7 +158,7 @@ async fn request_loop(mut request_receiver: RequestReceiver { // noop } - UiRequestData::ReplaceView { plugin_id: _, entrypoint_id, render_location, top_level_view, container } => { + UiRequestData::ReplaceView { plugin_id: _, plugin_name: _, entrypoint_id, entrypoint_name: _, render_location, top_level_view, container } => { let event = ScenarioFrontendEvent::ReplaceView { entrypoint_id: entrypoint_id.to_string(), render_location: ui_render_location_to_scenario(render_location), diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 8bf7af6..46d7596 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -14,6 +14,7 @@ pub enum JsUiResponseData { pub enum JsUiRequestData { ReplaceView { entrypoint_id: EntrypointId, + entrypoint_name: String, render_location: JsUiRenderLocation, top_level_view: bool, container: UiWidget, diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 1a0089a..809619c 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -63,6 +63,8 @@ pub mod permissions; pub struct PluginRuntimeData { pub id: PluginId, pub uuid: String, + pub name: String, + pub entrypoint_names: HashMap, pub code: PluginCode, pub inline_view_entrypoint_id: Option, pub permissions: PluginPermissions, @@ -235,6 +237,8 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run start_js_runtime( data.id.clone(), data.uuid, + data.name, + data.entrypoint_names, data.code, data.permissions, data.inline_view_entrypoint_id, @@ -274,6 +278,8 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run async fn start_js_runtime( plugin_id: PluginId, plugin_uuid: String, + plugin_name: String, + entrypoint_names: HashMap, code: PluginCode, permissions: PluginPermissions, inline_view_entrypoint_id: Option, @@ -337,7 +343,7 @@ async fn start_js_runtime( module_loader: Rc::new(CustomModuleLoader::new(code, dev_plugin)), extensions: vec![plugin_ext::init_ops_and_esm( EventReceiver::new(event_stream), - PluginData::new(plugin_id, plugin_uuid, inline_view_entrypoint_id, runtime_permissions), + PluginData::new(plugin_id, plugin_uuid, plugin_name, entrypoint_names, inline_view_entrypoint_id, runtime_permissions), frontend_api, ComponentModel::new(component_model), repository, @@ -565,7 +571,7 @@ async fn op_plugin_get_pending_event(state: Rc>) -> anyhow::Res } fn make_request(state: &Rc>, data: JsUiRequestData) -> anyhow::Result { - let (plugin_id, mut frontend_api) = { + let (plugin_id, plugin_name, mut frontend_api) = { let state = state.borrow(); let plugin_id = state @@ -573,27 +579,32 @@ fn make_request(state: &Rc>, data: JsUiRequestData) -> anyhow:: .plugin_id() .clone(); + let plugin_name = state + .borrow::() + .plugin_name() + .to_string(); + let frontend_api = state .borrow::() .clone(); - (plugin_id, frontend_api) + (plugin_id, plugin_name, frontend_api) }; block_on(async { - make_request_async(plugin_id, &mut frontend_api, data).await + make_request_async(plugin_id, plugin_name, &mut frontend_api, data).await }) } -async fn make_request_async(plugin_id: PluginId, frontend_api: &mut FrontendApi, data: JsUiRequestData) -> anyhow::Result { +async fn make_request_async(plugin_id: PluginId, plugin_name: String, frontend_api: &mut FrontendApi, data: JsUiRequestData) -> anyhow::Result { match data { - JsUiRequestData::ReplaceView { render_location, top_level_view, container, entrypoint_id } => { + JsUiRequestData::ReplaceView { render_location, top_level_view, container, entrypoint_id, entrypoint_name } => { let render_location = match render_location { // TODO into? JsUiRenderLocation::InlineView => UiRenderLocation::InlineView, JsUiRenderLocation::View => UiRenderLocation::View, }; - frontend_api.replace_view(plugin_id, entrypoint_id, render_location, top_level_view, container).await?; + frontend_api.replace_view(plugin_id, plugin_name, entrypoint_id, entrypoint_name, render_location, top_level_view, container).await?; Ok(JsUiResponseData::Nothing) } @@ -672,15 +683,26 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { pub struct PluginData { plugin_id: PluginId, plugin_uuid: String, + plugin_name: String, + entrypoint_names: HashMap, inline_view_entrypoint_id: Option, permissions: PluginRuntimePermissions } impl PluginData { - fn new(plugin_id: PluginId, plugin_uuid: String, inline_view_entrypoint_id: Option, permissions: PluginRuntimePermissions) -> Self { + fn new( + plugin_id: PluginId, + plugin_uuid: String, + plugin_name: String, + entrypoint_names: HashMap, + inline_view_entrypoint_id: Option, + permissions: PluginRuntimePermissions + ) -> Self { Self { plugin_id, plugin_uuid, + plugin_name, + entrypoint_names, inline_view_entrypoint_id, permissions } @@ -694,6 +716,10 @@ impl PluginData { &self.plugin_uuid } + fn plugin_name(&self) -> &str { + &self.plugin_name + } + fn inline_view_entrypoint_id(&self) -> Option { self.inline_view_entrypoint_id.clone() } diff --git a/rust/server/src/plugins/js/ui.rs b/rust/server/src/plugins/js/ui.rs index dd5fba3..bd89a76 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/server/src/plugins/js/ui.rs @@ -82,13 +82,22 @@ fn op_react_replace_view( let comp_state = state.borrow(); let component_model = comp_state.borrow::(); + let entrypoint_names = comp_state.borrow::(); + + let entrypoint_id = EntrypointId::from_string(entrypoint_id); + + let entrypoint_name = entrypoint_names.entrypoint_names + .get(&entrypoint_id) + .expect("entrypoint name for id should always exist") + .to_string(); let Component::Root { shared_types, .. } = component_model.components.get("gauntlet:root").unwrap() else { unreachable!() }; let data = JsUiRequestData::ReplaceView { - entrypoint_id: EntrypointId::from_string(entrypoint_id), + entrypoint_id, + entrypoint_name, render_location, top_level_view, container: from_js_to_intermediate_widget(state.clone(), scope, container, component_model, shared_types)?, diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 6b715e4..7a6d91a 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -559,6 +559,12 @@ impl ApplicationManager { let plugin = self.db_repository.get_plugin_by_id(&plugin_id_str) .await?; + let entrypoint_names = self.db_repository.get_entrypoints_by_plugin_id(&plugin_id_str) + .await? + .into_iter() + .map(|entrypoint| (EntrypointId::from_string(entrypoint.id), entrypoint.name)) + .collect::>(); + let inline_view_entrypoint_id = self.db_repository.get_inline_view_entrypoint_id_for_plugin(&plugin_id_str) .await?; @@ -585,6 +591,8 @@ impl ApplicationManager { let data = PluginRuntimeData { id: plugin_id, uuid: plugin.uuid, + name: plugin.name, + entrypoint_names, code: PluginCode { js: plugin.code.js }, inline_view_entrypoint_id, permissions: PluginPermissions { From cdf46205d3fb65e66180ba66f42159b0e4371845 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 26 Sep 2024 20:26:43 +0200 Subject: [PATCH 083/540] Use same colors for settings ui --- rust/management_client/src/theme.rs | 19 +++++---- rust/management_client/src/theme/button.rs | 40 +++++++++---------- rust/management_client/src/theme/checkbox.rs | 14 +++---- rust/management_client/src/theme/container.rs | 10 ++--- .../src/theme/number_input.rs | 4 +- rust/management_client/src/theme/pick_list.rs | 20 +++++----- rust/management_client/src/theme/rule.rs | 4 +- .../src/theme/shortcut_selector.rs | 6 +-- rust/management_client/src/theme/table.rs | 12 +++--- .../management_client/src/theme/text_input.rs | 18 ++++----- 10 files changed, 73 insertions(+), 74 deletions(-) diff --git a/rust/management_client/src/theme.rs b/rust/management_client/src/theme.rs index 7dd7862..08c83ce 100644 --- a/rust/management_client/src/theme.rs +++ b/rust/management_client/src/theme.rs @@ -23,8 +23,8 @@ impl StyleSheet for GauntletSettingsTheme { fn appearance(&self, _: &Self::Style) -> Appearance { Appearance { - background_color: BACKGROUND.to_iced(), - text_color: TEXT.to_iced(), + background_color: BACKGROUND_DARKEST.to_iced(), + text_color: TEXT_LIGHTEST.to_iced(), } } } @@ -33,15 +33,14 @@ impl StyleSheet for GauntletSettingsTheme { pub const NOT_INTENDED_TO_BE_USED: ThemeColor = ThemeColor::new(0xAF5BFF, 1.0); pub const TRANSPARENT: ThemeColor = ThemeColor::new(0x000000, 0.0); -pub const BACKGROUND: ThemeColor = ThemeColor::new(0x2C323A, 1.0); -pub const BACKGROUND_OVERLAY: ThemeColor = ThemeColor::new(0x333a42, 1.0); -pub const BACKGROUND_LIGHT: ThemeColor = ThemeColor::new(0x48505B, 0.5); -pub const BACKGROUND_LIGHTER: ThemeColor = ThemeColor::new(0x626974, 0.3); -pub const BACKGROUND_LIGHTEST: ThemeColor = ThemeColor::new(0x626974, 0.1); -pub const TEXT: ThemeColor = ThemeColor::new(0xBFC5CB, 1.0); -pub const TEXT_HOVERED: ThemeColor = ThemeColor::new(0xDDDFE1, 1.0); +pub const BACKGROUND_LIGHTEST: ThemeColor = ThemeColor::new(0x626974, 0.3); +pub const BACKGROUND_LIGHTER: ThemeColor = ThemeColor::new(0x48505B, 0.5); +pub const BACKGROUND_DARKER: ThemeColor = ThemeColor::new(0x333a42, 1.0); +pub const BACKGROUND_DARKEST: ThemeColor = ThemeColor::new(0x2C323A, 1.0); +pub const TEXT_LIGHTEST: ThemeColor = ThemeColor::new(0xDDDFE1, 1.0); +pub const TEXT_LIGHTER: ThemeColor = ThemeColor::new(0x9AA0A6, 1.0); pub const TEXT_DARKER: ThemeColor = ThemeColor::new(0x6B7785, 1.0); -pub const TEXT_DARK: ThemeColor = ThemeColor::new(0x1D242C, 1.0); +pub const TEXT_DARKEST: ThemeColor = ThemeColor::new(0x1D242C, 1.0); pub const PRIMARY: ThemeColor = ThemeColor::new(0xC79F60, 1.0); pub const PRIMARY_HOVERED: ThemeColor = ThemeColor::new(0xD7B37A, 1.0); diff --git a/rust/management_client/src/theme/button.rs b/rust/management_client/src/theme/button.rs index c1dc48f..eb25ee0 100644 --- a/rust/management_client/src/theme/button.rs +++ b/rust/management_client/src/theme/button.rs @@ -1,8 +1,8 @@ -use iced::Border; use iced::widget::button; use iced::widget::button::Appearance; +use iced::Border; -use crate::theme::{BACKGROUND_LIGHT, BACKGROUND_LIGHTER, BUTTON_BORDER_RADIUS, DANGER, GauntletSettingsTheme, PRIMARY, PRIMARY_HOVERED, SUCCESS, TEXT, TEXT_DARK}; +use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, BACKGROUND_LIGHTER, BUTTON_BORDER_RADIUS, DANGER, PRIMARY, PRIMARY_HOVERED, SUCCESS, TEXT_DARKEST, TEXT_LIGHTEST}; #[derive(Default)] pub enum ButtonStyle { @@ -22,20 +22,20 @@ impl button::StyleSheet for GauntletSettingsTheme { fn active(&self, style: &Self::Style) -> Appearance { let (background_color, text_color) = match style { - ButtonStyle::Primary => (PRIMARY.to_iced(), TEXT_DARK.to_iced()), - ButtonStyle::Positive => (SUCCESS.to_iced(), TEXT_DARK.to_iced()), - ButtonStyle::Destructive => (DANGER.to_iced(), TEXT.to_iced()), + ButtonStyle::Primary => (PRIMARY.to_iced(), TEXT_DARKEST.to_iced()), + ButtonStyle::Positive => (SUCCESS.to_iced(), TEXT_DARKEST.to_iced()), + ButtonStyle::Destructive => (DANGER.to_iced(), TEXT_LIGHTEST.to_iced()), ButtonStyle::TableRow => { return Appearance { background: None, - text_color: TEXT.to_iced(), + text_color: TEXT_LIGHTEST.to_iced(), ..Default::default() } } ButtonStyle::ViewSwitcher => { return Appearance { background: None, - text_color: TEXT.to_iced(), + text_color: TEXT_LIGHTEST.to_iced(), border: Border { radius: BUTTON_BORDER_RADIUS.into(), ..Default::default() @@ -45,8 +45,8 @@ impl button::StyleSheet for GauntletSettingsTheme { } ButtonStyle::ViewSwitcherSelected => { return Appearance { - background: Some(BACKGROUND_LIGHT.to_iced().into()), - text_color: TEXT.to_iced(), + background: Some(BACKGROUND_DARKER.to_iced().into()), + text_color: TEXT_LIGHTEST.to_iced(), border: Border { radius: BUTTON_BORDER_RADIUS.into(), ..Default::default() @@ -57,7 +57,7 @@ impl button::StyleSheet for GauntletSettingsTheme { ButtonStyle::DownloadInfo => { return Appearance { background: None, - text_color: TEXT.to_iced(), + text_color: TEXT_LIGHTEST.to_iced(), border: Border { radius: BUTTON_BORDER_RADIUS.into(), ..Default::default() @@ -80,20 +80,20 @@ impl button::StyleSheet for GauntletSettingsTheme { fn hovered(&self, style: &Self::Style) -> Appearance { let (background_color, text_color) = match style { - ButtonStyle::Primary => (PRIMARY_HOVERED.to_iced(), TEXT_DARK.to_iced()), - ButtonStyle::Positive => (SUCCESS.to_iced(), TEXT_DARK.to_iced()), // TODO - ButtonStyle::Destructive => (DANGER.to_iced(), TEXT.to_iced()), // TODO + ButtonStyle::Primary => (PRIMARY_HOVERED.to_iced(), TEXT_DARKEST.to_iced()), + ButtonStyle::Positive => (SUCCESS.to_iced(), TEXT_DARKEST.to_iced()), // TODO + ButtonStyle::Destructive => (DANGER.to_iced(), TEXT_LIGHTEST.to_iced()), // TODO ButtonStyle::TableRow => { return Appearance { background: None, - text_color: TEXT.to_iced(), // TODO + text_color: TEXT_LIGHTEST.to_iced(), // TODO ..Default::default() } } ButtonStyle::ViewSwitcher => { return Appearance { background: Some(BACKGROUND_LIGHTER.to_iced().into()), - text_color: TEXT.to_iced(), + text_color: TEXT_LIGHTEST.to_iced(), border: Border { radius: BUTTON_BORDER_RADIUS.into(), ..Default::default() @@ -104,7 +104,7 @@ impl button::StyleSheet for GauntletSettingsTheme { ButtonStyle::ViewSwitcherSelected => { return Appearance { background: Some(BACKGROUND_LIGHTER.to_iced().into()), - text_color: TEXT.to_iced(), + text_color: TEXT_LIGHTEST.to_iced(), border: Border { radius: BUTTON_BORDER_RADIUS.into(), ..Default::default() @@ -115,7 +115,7 @@ impl button::StyleSheet for GauntletSettingsTheme { ButtonStyle::DownloadInfo => { return Appearance { background: Some(BACKGROUND_LIGHTER.to_iced().into()), - text_color: TEXT.to_iced(), + text_color: TEXT_LIGHTEST.to_iced(), border: Border { radius: BUTTON_BORDER_RADIUS.into(), ..Default::default() @@ -139,9 +139,9 @@ impl button::StyleSheet for GauntletSettingsTheme { fn pressed(&self, style: &Self::Style) -> Appearance { match style { ButtonStyle::ViewSwitcher | ButtonStyle::ViewSwitcherSelected => { - return Appearance { - background: Some(BACKGROUND_LIGHT.to_iced().into()), - text_color: TEXT.to_iced(), + Appearance { + background: Some(BACKGROUND_DARKER.to_iced().into()), + text_color: TEXT_LIGHTEST.to_iced(), border: Border { radius: BUTTON_BORDER_RADIUS.into(), ..Default::default() diff --git a/rust/management_client/src/theme/checkbox.rs b/rust/management_client/src/theme/checkbox.rs index 6c30788..51ed81f 100644 --- a/rust/management_client/src/theme/checkbox.rs +++ b/rust/management_client/src/theme/checkbox.rs @@ -2,7 +2,7 @@ use iced::Border; use iced::widget::checkbox; use iced::widget::checkbox::Appearance; -use crate::theme::{BACKGROUND, BACKGROUND_LIGHT, BACKGROUND_LIGHTER, GauntletSettingsTheme, PRIMARY, PRIMARY_HOVERED}; +use crate::theme::{BACKGROUND_LIGHTER, GauntletSettingsTheme, PRIMARY, PRIMARY_HOVERED, BACKGROUND_DARKER, BACKGROUND_DARKEST}; #[derive(Default)] pub enum CheckboxStyle { @@ -17,12 +17,12 @@ impl checkbox::StyleSheet for GauntletSettingsTheme { let background = if is_checked { PRIMARY.to_iced().into() } else { - BACKGROUND.to_iced().into() + BACKGROUND_DARKEST.to_iced().into() }; Appearance { background, - icon_color: BACKGROUND.to_iced(), + icon_color: BACKGROUND_DARKEST.to_iced(), border: Border { radius: 4.0.into(), width: 1.0, @@ -36,12 +36,12 @@ impl checkbox::StyleSheet for GauntletSettingsTheme { let background = if is_checked { PRIMARY_HOVERED.to_iced().into() } else { - BACKGROUND_LIGHT.to_iced().into() + BACKGROUND_DARKER.to_iced().into() }; Appearance { background, - icon_color: BACKGROUND.to_iced(), + icon_color: BACKGROUND_DARKEST.to_iced(), border: Border { radius: 4.0.into(), width: 1.0, @@ -55,12 +55,12 @@ impl checkbox::StyleSheet for GauntletSettingsTheme { let background = if is_checked { BACKGROUND_LIGHTER.to_iced().into() } else { - BACKGROUND_LIGHT.to_iced().into() + BACKGROUND_DARKER.to_iced().into() }; Appearance { background, - icon_color: BACKGROUND.to_iced(), + icon_color: BACKGROUND_DARKEST.to_iced(), border: Default::default(), text_color: None, } diff --git a/rust/management_client/src/theme/container.rs b/rust/management_client/src/theme/container.rs index e8d9454..b748a51 100644 --- a/rust/management_client/src/theme/container.rs +++ b/rust/management_client/src/theme/container.rs @@ -1,7 +1,7 @@ use iced::Border; use iced::widget::container; -use crate::theme::{BACKGROUND_LIGHT, BACKGROUND_OVERLAY, GauntletSettingsTheme}; +use crate::theme::{GauntletSettingsTheme, BACKGROUND_LIGHTEST, BACKGROUND_DARKER}; #[derive(Default)] pub enum ContainerStyle { @@ -19,9 +19,9 @@ impl container::StyleSheet for GauntletSettingsTheme { ContainerStyle::Transparent => Default::default(), ContainerStyle::Box => { container::Appearance { - background: Some(BACKGROUND_OVERLAY.to_iced().into()), + background: Some(BACKGROUND_LIGHTEST.to_iced().into()), border: Border { - color: BACKGROUND_LIGHT.to_iced(), + color: BACKGROUND_DARKER.to_iced(), radius: 10.0.into(), width: 1.0, }, @@ -30,11 +30,11 @@ impl container::StyleSheet for GauntletSettingsTheme { } ContainerStyle::TextInputLike => { container::Appearance { - background: Some(BACKGROUND_LIGHT.to_iced().into()), + background: Some(BACKGROUND_LIGHTEST.to_iced().into()), border: Border { radius: 4.0.into(), width: 1.0, - color: BACKGROUND_LIGHT.to_iced().into(), + color: BACKGROUND_LIGHTEST.to_iced().into(), }, ..Default::default() } diff --git a/rust/management_client/src/theme/number_input.rs b/rust/management_client/src/theme/number_input.rs index 7eb2bef..569e482 100644 --- a/rust/management_client/src/theme/number_input.rs +++ b/rust/management_client/src/theme/number_input.rs @@ -1,6 +1,6 @@ use iced_aw::number_input; -use crate::theme::{GauntletSettingsTheme, PRIMARY, PRIMARY_HOVERED, TEXT, TEXT_DARKER}; +use crate::theme::{GauntletSettingsTheme, PRIMARY, PRIMARY_HOVERED, TEXT_DARKER, TEXT_LIGHTEST}; #[derive(Default)] pub enum NumberInputStyle { @@ -28,7 +28,7 @@ impl number_input::StyleSheet for GauntletSettingsTheme { fn disabled(&self, _: &Self::Style) -> number_input::Appearance { number_input::Appearance { button_background: None, - icon_color: TEXT.to_iced(), + icon_color: TEXT_LIGHTEST.to_iced(), } } } \ No newline at end of file diff --git a/rust/management_client/src/theme/pick_list.rs b/rust/management_client/src/theme/pick_list.rs index 44d5866..27db849 100644 --- a/rust/management_client/src/theme/pick_list.rs +++ b/rust/management_client/src/theme/pick_list.rs @@ -1,7 +1,7 @@ use iced::{Border, overlay}; use iced::widget::pick_list; -use crate::theme::{BACKGROUND, BACKGROUND_LIGHT, BUTTON_BORDER_RADIUS, GauntletSettingsTheme, PRIMARY, PRIMARY_HOVERED, TEXT, TEXT_DARK}; +use crate::theme::{BUTTON_BORDER_RADIUS, GauntletSettingsTheme, PRIMARY, PRIMARY_HOVERED, TEXT_DARKEST, BACKGROUND_DARKER, BACKGROUND_DARKEST, TEXT_LIGHTEST}; #[derive(Clone, Default)] pub enum PickListStyle { @@ -39,17 +39,17 @@ fn pick_list_appearance(state: PickListState) -> pick_list::Appearance { }; let text_color = match state { - PickListState::Active => TEXT_DARK.to_iced(), - PickListState::Hovered => TEXT_DARK.to_iced(), + PickListState::Active => TEXT_DARKEST.to_iced(), + PickListState::Hovered => TEXT_DARKEST.to_iced(), }; pick_list::Appearance { text_color, background: background_color.into(), - placeholder_color: BACKGROUND_LIGHT.to_iced(), + placeholder_color: BACKGROUND_DARKER.to_iced(), handle_color: text_color, border: Border { - color: BACKGROUND_LIGHT.to_iced(), + color: BACKGROUND_DARKER.to_iced(), width: 1.0, radius: BUTTON_BORDER_RADIUS.into(), }, @@ -61,15 +61,15 @@ impl overlay::menu::StyleSheet for GauntletSettingsTheme { fn appearance(&self, _: &Self::Style) -> overlay::menu::Appearance { overlay::menu::Appearance { - text_color: TEXT.to_iced(), - background: BACKGROUND.to_iced().into(), + text_color: TEXT_LIGHTEST.to_iced(), + background: BACKGROUND_DARKEST.to_iced().into(), border: Border { radius: BUTTON_BORDER_RADIUS.into(), width: 1.0, - color: BACKGROUND_LIGHT.to_iced().into(), + color: BACKGROUND_DARKER.to_iced().into(), }, - selected_text_color: TEXT.to_iced(), - selected_background: BACKGROUND_LIGHT.to_iced().into(), + selected_text_color: TEXT_LIGHTEST.to_iced(), + selected_background: BACKGROUND_DARKER.to_iced().into(), } } } diff --git a/rust/management_client/src/theme/rule.rs b/rust/management_client/src/theme/rule.rs index 9683553..bc98df8 100644 --- a/rust/management_client/src/theme/rule.rs +++ b/rust/management_client/src/theme/rule.rs @@ -1,5 +1,5 @@ use iced::widget::rule; -use crate::theme::{BACKGROUND_LIGHT, GauntletSettingsTheme}; +use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER}; #[derive(Default)] pub enum RuleStyle { @@ -12,7 +12,7 @@ impl rule::StyleSheet for GauntletSettingsTheme { fn appearance(&self, _: &Self::Style) -> rule::Appearance { rule::Appearance { - color: BACKGROUND_LIGHT.to_iced(), + color: BACKGROUND_DARKER.to_iced(), width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Full, diff --git a/rust/management_client/src/theme/shortcut_selector.rs b/rust/management_client/src/theme/shortcut_selector.rs index 61f2e96..54ff022 100644 --- a/rust/management_client/src/theme/shortcut_selector.rs +++ b/rust/management_client/src/theme/shortcut_selector.rs @@ -1,7 +1,7 @@ use iced::Border; use iced::widget::container::Appearance; use crate::components::shortcut_selector; -use crate::theme::{BACKGROUND_LIGHT, BUTTON_BORDER_RADIUS, GauntletSettingsTheme, PRIMARY}; +use crate::theme::{BUTTON_BORDER_RADIUS, GauntletSettingsTheme, PRIMARY, BACKGROUND_DARKER}; #[derive(Default)] pub enum ShortcutSelectorStyle { @@ -16,7 +16,7 @@ impl shortcut_selector::StyleSheet for GauntletSettingsTheme { match style { ShortcutSelectorStyle::Default => { Appearance { - background: Some(BACKGROUND_LIGHT.to_iced().into()), + background: Some(BACKGROUND_DARKER.to_iced().into()), border: Border { radius: BUTTON_BORDER_RADIUS.into(), ..Default::default() @@ -31,7 +31,7 @@ impl shortcut_selector::StyleSheet for GauntletSettingsTheme { match style { ShortcutSelectorStyle::Default => { Appearance { - background: Some(BACKGROUND_LIGHT.to_iced().into()), + background: Some(BACKGROUND_DARKER.to_iced().into()), border: Border { radius: BUTTON_BORDER_RADIUS.into(), width: 2.0, diff --git a/rust/management_client/src/theme/table.rs b/rust/management_client/src/theme/table.rs index f768736..9212db2 100644 --- a/rust/management_client/src/theme/table.rs +++ b/rust/management_client/src/theme/table.rs @@ -1,7 +1,7 @@ use iced::Border; use iced::widget::container; -use crate::theme::{BACKGROUND_LIGHT, BACKGROUND_LIGHTEST, GauntletSettingsTheme, TEXT}; +use crate::theme::{BACKGROUND_DARKER, BACKGROUND_LIGHTEST, GauntletSettingsTheme, TEXT_LIGHTEST, BACKGROUND_LIGHTER}; #[derive(Default, Clone)] pub enum TableStyle { @@ -14,8 +14,8 @@ impl iced_table::StyleSheet for GauntletSettingsTheme { fn header(&self, _: &Self::Style) -> container::Appearance { container::Appearance { - text_color: Some(TEXT.to_iced()), - background: Some(BACKGROUND_LIGHT.to_iced().into()), + text_color: Some(TEXT_LIGHTEST.to_iced()), + background: Some(BACKGROUND_DARKER.to_iced().into()), border: Border { radius: 4.0.into(), ..Default::default() @@ -26,8 +26,8 @@ impl iced_table::StyleSheet for GauntletSettingsTheme { fn footer(&self, _: &Self::Style) -> container::Appearance { container::Appearance { - text_color: Some(TEXT.to_iced()), - background: Some(BACKGROUND_LIGHT.to_iced().into()), + text_color: Some(TEXT_LIGHTEST.to_iced()), + background: Some(BACKGROUND_DARKER.to_iced().into()), border: Border { radius: 4.0.into(), ..Default::default() @@ -41,7 +41,7 @@ impl iced_table::StyleSheet for GauntletSettingsTheme { let background = if index % 2 == 0 { None } else { - Some(BACKGROUND_LIGHTEST.to_iced().into()) + Some(BACKGROUND_DARKER.to_iced().into()) }; container::Appearance { diff --git a/rust/management_client/src/theme/text_input.rs b/rust/management_client/src/theme/text_input.rs index a7d0a75..6140df5 100644 --- a/rust/management_client/src/theme/text_input.rs +++ b/rust/management_client/src/theme/text_input.rs @@ -1,7 +1,7 @@ -use iced::{Border, Color}; +use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, NOT_INTENDED_TO_BE_USED, TEXT_DARKER, TEXT_LIGHTEST, TRANSPARENT}; use iced::widget::text_input; use iced::widget::text_input::Appearance; -use crate::theme::{BACKGROUND_LIGHT, GauntletSettingsTheme, NOT_INTENDED_TO_BE_USED, TEXT, TEXT_DARKER, TRANSPARENT}; +use iced::{Border, Color}; #[derive(Default)] pub enum TextInputStyle { @@ -21,7 +21,7 @@ impl text_input::StyleSheet for GauntletSettingsTheme { border: Border { radius: 4.0.into(), width: 1.0, - color: BACKGROUND_LIGHT.to_iced().into(), + color: BACKGROUND_DARKER.to_iced().into(), }, icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), } @@ -33,11 +33,11 @@ impl text_input::StyleSheet for GauntletSettingsTheme { match style { TextInputStyle::FormInput => { Appearance { - background: BACKGROUND_LIGHT.to_iced().into(), + background: BACKGROUND_DARKER.to_iced().into(), border: Border { radius: 4.0.into(), width: 1.0, - color: BACKGROUND_LIGHT.to_iced().into(), + color: BACKGROUND_DARKER.to_iced().into(), }, icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), } @@ -47,11 +47,11 @@ impl text_input::StyleSheet for GauntletSettingsTheme { fn disabled(&self, _: &Self::Style) -> Appearance { Appearance { - background: BACKGROUND_LIGHT.to_iced().into(), + background: BACKGROUND_DARKER.to_iced().into(), border: Border { radius: 4.0.into(), width: 1.0, - color: BACKGROUND_LIGHT.to_iced().into(), + color: BACKGROUND_DARKER.to_iced().into(), }, icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), } @@ -62,7 +62,7 @@ impl text_input::StyleSheet for GauntletSettingsTheme { } fn value_color(&self, _: &Self::Style) -> Color { - TEXT.to_iced() + TEXT_LIGHTEST.to_iced() } fn disabled_color(&self, style: &Self::Style) -> Color { @@ -70,6 +70,6 @@ impl text_input::StyleSheet for GauntletSettingsTheme { } fn selection_color(&self, _: &Self::Style) -> Color { - BACKGROUND_LIGHT.to_iced() + BACKGROUND_DARKER.to_iced() } } \ No newline at end of file From 00a2888b085dcfc0e99d5f4f3acaa34ad6eb3598 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 27 Sep 2024 18:30:34 +0200 Subject: [PATCH 084/540] Add action bar to main search view --- rust/client/src/ui/mod.rs | 63 ++++- rust/client/src/ui/widget.rs | 429 +++++++++++++++++++++-------------- 2 files changed, 318 insertions(+), 174 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index ce0f513..c3ef523 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -24,7 +24,7 @@ use tokio::sync::RwLock as TokioRwLock; use tonic::transport::Server; use client_context::ClientContext; -use common::model::{BackendRequestData, BackendResponseData, EntrypointId, PhysicalKey, PhysicalShortcut, PluginId, SearchResult, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData}; +use common::model::{BackendRequestData, BackendResponseData, EntrypointId, PhysicalKey, PhysicalShortcut, PluginId, SearchResult, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; use common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFrontendApiError}; use common::scenario_convert::{ui_render_location_from_scenario, ui_widget_from_scenario}; use common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; @@ -38,7 +38,7 @@ use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::theme::container::ContainerStyle; use crate::ui::theme::text_input::TextInputStyle; use crate::ui::view_container::view_container; -use crate::ui::widget::ComponentWidgetEvent; +use crate::ui::widget::{render_root, ActionPanel, ActionPanelItems, ComponentWidgetEvent}; mod view_container; mod search_list; @@ -75,6 +75,7 @@ pub struct AppModel { plugin_view_data: Option, error_view: Option, search_results: Vec, + show_action_panel: bool, } struct PluginViewData { @@ -137,6 +138,8 @@ pub enum AppMsg { FontLoaded(Result<(), font::Error>), ShowWindow, HideWindow, + ToggleActionPanel, + RunAction(UiWidgetId), ShowPreferenceRequiredView { plugin_id: PluginId, entrypoint_id: EntrypointId, @@ -390,6 +393,7 @@ impl Application for AppModel { plugin_view_data, error_view, search_results: vec![], + show_action_panel: false, }, Command::batch(commands), ) @@ -728,6 +732,16 @@ impl Application for AppModel { BackendForFrontendApiError::TimeoutError => ErrorViewData::BackendTimeout, }); + Command::none() + } + AppMsg::ToggleActionPanel => { + self.show_action_panel = !self.show_action_panel; + + Command::none() + } + AppMsg::RunAction(widget_id) => { + println!("widget_id: {}", widget_id); + Command::none() } } @@ -980,19 +994,53 @@ impl Application for AppModel { let separator = horizontal_rule(1) .into(); - let column: Element<_> = column(vec![ - input, - separator, + let content: Element<_> = column(vec![ inline_view_container(self.client_context.clone()).into(), list, ]).into(); - let element: Element<_> = container(column) + let action_panel_items = if let Some(search_item) = self.search_results.get(self.focused_search_result) { + let title = match search_item.entrypoint_type { + SearchResultEntrypointType::Command => "Run Command", + SearchResultEntrypointType::View => "Open View", + SearchResultEntrypointType::GeneratedCommand => "Run Command", + }.to_string(); + + vec![ActionPanelItems::Action { + title, + widget_id: 0, + physical_shortcut: Some(PhysicalShortcut { + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }), + }] + } else { + vec![] + }; + + let root = render_root( + self.show_action_panel, + input, + separator, + content, + Some(ActionPanel { + title: None, + items: action_panel_items, + }), + "".to_string(), + || AppMsg::ToggleActionPanel, + AppMsg::RunAction + ); + + let root: Element<_> = container(root) .width(Length::Fill) .height(Length::Fill) .themed(ContainerStyle::Main); - element + root } Some(data) => { let PluginViewData { @@ -1149,6 +1197,7 @@ impl AppModel { fn reset_window_state(&mut self) -> Command { self.focused_search_result = 0; self.search_result_offset = 0; + self.show_action_panel = false; let mut commands = vec![ scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: 0.0 }), diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index ec8c181..5c8bb16 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -145,12 +145,6 @@ pub enum ComponentRenderContext { entrypoint_name: String, action_shortcuts: HashMap, }, - ActionPanel { - action_shortcuts: HashMap, - }, - Action { - action_shortcut: Option, - } } impl ComponentRenderContext { @@ -237,104 +231,8 @@ impl ComponentWidgetWrapper { let (widget, state) = &*self.get(); match widget { ComponentWidget::TextPart { value } => render_text_part(value, context), - ComponentWidget::Action { id, title } => { - let ComponentRenderContext::ActionPanel { action_shortcuts } = context else { - panic!("not supposed to be passed to action panel: {:?}", context) - }; - - let shortcut_element: Option> = id.as_ref() - .map(|id| action_shortcuts.get(id)) - .flatten() - .map(|shortcut| render_shortcut(shortcut)); - - let content: Element<_> = if let Some(shortcut_element) = shortcut_element { - let text: Element<_> = text(title) - .into(); - - let space: Element<_> = horizontal_space() - .into(); - - row([text, space, shortcut_element]) - .align_items(Alignment::Center) - .into() - } else { - text(title) - .into() - }; - - button(content) - .on_press(ComponentWidgetEvent::ActionClick { widget_id }) - .width(Length::Fill) - .themed(ButtonStyle::Action) - } - ComponentWidget::ActionPanelSection { children, .. } => { - column(render_children(children, context)) - .into() - } - ComponentWidget::ActionPanel { children, title } => { - let ComponentRenderContext::ActionPanel { action_shortcuts } = context else { - panic!("not supposed to be passed to action panel: {:?}", context) - }; - - let mut columns = vec![]; - if let Some(title) = title { - let text: Element<_> = text(title) - .font(Font { - weight: Weight::Bold, - ..Font::DEFAULT - }) - .into(); - - let text = container(text) - .themed(ContainerStyle::ActionPanelTitle); - - columns.push(text) - } - - let mut place_separator = false; - - for child in children { - let (widget, _) = &*child.get(); - - match widget { - ComponentWidget::Action { .. } => { - if place_separator { - let separator: Element<_> = horizontal_rule(1) - .themed(RuleStyle::ActionPanel); - - columns.push(separator); - - place_separator = false; - } - - columns.push(child.render_widget(ComponentRenderContext::ActionPanel { action_shortcuts: action_shortcuts.clone() })); - } - ComponentWidget::ActionPanelSection { .. } => { - let separator: Element<_> = horizontal_rule(1) - .themed(RuleStyle::ActionPanel); - - columns.push(separator); - - columns.push(child.render_widget(ComponentRenderContext::ActionPanel { action_shortcuts: action_shortcuts.clone() })); - - place_separator = true; - } - _ => { - panic!("unexpected widget kind {:?}", widget) - } - }; - - } - - let actions: Element<_> = column(columns) - .into(); - - let actions: Element<_> = scrollable(actions) - .width(Length::Fill) - .into(); - - container(actions) - .themed(ContainerStyle::ActionPanel) + ComponentWidget::ActionPanel { .. } | ComponentWidget::Action { .. } | ComponentWidget::ActionPanelSection { .. } => { + unreachable!() } ComponentWidget::MetadataTagItem { children } => { let content: Element<_> = render_children_string(children, ComponentRenderContext::None); @@ -582,7 +480,7 @@ impl ComponentWidgetWrapper { if is_in_list { content } else { - render_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) + render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } } ComponentWidget::Root { children } => { @@ -769,7 +667,7 @@ impl ComponentWidgetWrapper { .width(Length::Fill) .themed(ContainerStyle::Form); - render_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) + render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } ComponentWidget::InlineSeparator { icon } => { match icon { @@ -1131,7 +1029,7 @@ impl ComponentWidgetWrapper { .height(Length::Fill) .into(); - render_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) + render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } ComponentWidget::GridItem { children, title, subtitle } => { // TODO should be just one @@ -1245,7 +1143,7 @@ impl ComponentWidgetWrapper { .width(Length::Fill) .themed(ContainerStyle::Grid); - render_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) + render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } } } @@ -1354,7 +1252,7 @@ fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option< title_content.push(title) } - if let Some(subtitle) = subtitle { // TODO remove ? + if let Some(subtitle) = subtitle { let subtitle: Element<_> = text(subtitle) .size(15) .themed(theme_kind_subtitle_text); @@ -1377,7 +1275,7 @@ fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option< .into() } -fn render_root<'a>( +fn render_plugin_root<'a>( show_action_panel: bool, widget_id: UiWidgetId, children: &[ComponentWidgetWrapper], @@ -1389,58 +1287,8 @@ fn render_root<'a>( panic!("not supposed to be passed to root item: {:?}", context) }; - let entrypoint_name: Element<_> = text(entrypoint_name) - .into(); - - let action_panel_element = render_child_by_type(children, |widget| matches!(widget, ComponentWidget::ActionPanel { .. }), ComponentRenderContext::ActionPanel { action_shortcuts }) - .ok(); - - let (hide_action_panel, action_panel_element, bottom_panel) = if let Some(action_panel_element) = action_panel_element { - let actions_text: Element<_> = text("Actions") - .into(); - - let actions_text: Element<_> = container(actions_text) - .themed(ContainerStyle::RootBottomPanelActionButtonText); - - let shortcut = render_shortcut(&PhysicalShortcut { - physical_key: PhysicalKey::KeyK, - modifier_shift: false, - modifier_control: false, - modifier_alt: true, - modifier_meta: false, - }); - - let action_panel_toggle_content: Element<_> = row(vec![actions_text, shortcut]) - .into(); - - let action_panel_toggle: Element<_> = button(action_panel_toggle_content) - .on_press(ComponentWidgetEvent::ToggleActionPanel { widget_id }) - .themed(ButtonStyle::RootBottomPanelActionButton); - - let space = horizontal_space() - .into(); - - let bottom_panel: Element<_> = row(vec![entrypoint_name, space, action_panel_toggle]) - .align_items(Alignment::Center) - .into(); - - (!show_action_panel, action_panel_element, bottom_panel) - } else { - let space: Element<_> = Space::with_height(16 + 8 + 2) // TODO get value from theme - .into(); - - let bottom_panel: Element<_> = row(vec![entrypoint_name, space]) - .into(); - - (true, Space::with_height(1).into(), bottom_panel) - }; - let top_panel = create_top_panel(); - let bottom_panel: Element<_> = container(bottom_panel) - .width(Length::Fill) - .themed(ContainerStyle::RootBottomPanel); - let top_separator = if is_loading { LoadingBar::new() .into() @@ -1449,6 +1297,250 @@ fn render_root<'a>( .into() }; + let action_panel = convert_action_panel(children, action_shortcuts); + + render_root( + show_action_panel, + top_panel, + top_separator, + content, + action_panel, + entrypoint_name, + || ComponentWidgetEvent::ToggleActionPanel { widget_id }, + |widget_id| ComponentWidgetEvent::ActionClick { widget_id } + ) +} + +pub struct ActionPanel { + pub title: Option, + pub items: Vec +} + +pub enum ActionPanelItems { + Action { + title: String, + widget_id: UiWidgetId, + physical_shortcut: Option + }, + ActionSection { + title: Option, + items: Vec + } +} + +fn convert_action_panel(children: &[ComponentWidgetWrapper], action_shortcuts: HashMap) -> Option { + let action_panel: Vec<_> = children + .into_iter() + .filter(|child| { + let (widget, _) = &*child.get(); + matches!(widget, ComponentWidget::ActionPanel { .. }) + }) + .collect(); + + let action_panel = match action_panel[..] { + [] => None?, + [single] => single, + [_, _, ..] => None?, + }; + + let (action_panel, _) = &*action_panel.get(); + + match action_panel { + ComponentWidget::ActionPanel { children, title } => { + fn convert_to_items(children: &[ComponentWidgetWrapper], action_shortcuts: &HashMap) -> Vec { + let mut items = vec![]; + + for child in children { + let widget_id = child.id; + let (widget, _) = &*child.get(); + + match widget { + ComponentWidget::Action { id, title } => { + let physical_shortcut: Option = id.as_ref() + .map(|id| action_shortcuts.get(id)) + .flatten() + .cloned(); + + items.push(ActionPanelItems::Action { + title: title.clone(), + widget_id, + physical_shortcut, + }); + } + ComponentWidget::ActionPanelSection { children, title } => { + items.push(ActionPanelItems::ActionSection { + title: title.clone(), + items: convert_to_items(children, action_shortcuts), + }); + } + _ => { + panic!("unexpected widget kind {:?}", widget) + } + }; + } + + items + } + + Some(ActionPanel { + title: title.clone(), + items: convert_to_items(children, &action_shortcuts), + }) + } + _ => None + } +} + +fn render_action_panel_items<'a, T: 'a + Clone>(title: Option, items: Vec, on_action: &dyn Fn(UiWidgetId) -> T) -> Vec> { + let mut columns = vec![]; + + if let Some(title) = title { + let text: Element<_> = text(title) + .font(Font { + weight: Weight::Bold, + ..Font::DEFAULT + }) + .into(); + + let text = container(text) + .themed(ContainerStyle::ActionPanelTitle); + + columns.push(text) + } + + let mut place_separator = false; + + for item in items { + match item { + ActionPanelItems::Action { title, widget_id, physical_shortcut } => { + if place_separator { + let separator: Element<_> = horizontal_rule(1) + .themed(RuleStyle::ActionPanel); + + columns.push(separator); + + place_separator = false; + } + + let shortcut_element: Option> = physical_shortcut.as_ref() + .map(|shortcut| render_shortcut(shortcut)); + + let content: Element<_> = if let Some(shortcut_element) = shortcut_element { + let text: Element<_> = text(title) + .into(); + + let space: Element<_> = horizontal_space() + .into(); + + row([text, space, shortcut_element]) + .align_items(Alignment::Center) + .into() + } else { + text(title) + .into() + }; + + let content = button(content) + .on_press(on_action(widget_id)) + .width(Length::Fill) + .themed(ButtonStyle::Action); + + columns.push(content); + } + ActionPanelItems::ActionSection { title, items } => { + let separator: Element<_> = horizontal_rule(1) + .themed(RuleStyle::ActionPanel); + + columns.push(separator); + + let content = render_action_panel_items(title, items, on_action); + + for content in content { + columns.push(content); + } + + place_separator = true; + } + }; + } + + columns +} + +fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T>(action_panel: ActionPanel, on_action: F) -> Element<'a, T> { + let columns = render_action_panel_items(action_panel.title, action_panel.items, &on_action); + + let actions: Element<_> = column(columns) + .into(); + + let actions: Element<_> = scrollable(actions) + .width(Length::Fill) + .into(); + + container(actions) + .themed(ContainerStyle::ActionPanel) +} + +pub fn render_root<'a, T: 'a + Clone>( + show_action_panel: bool, + top_panel: Element<'a, T>, + top_separator: Element<'a, T>, + content: Element<'a, T>, + action_panel: Option, + entrypoint_name: String, + on_panel_toggle: impl Fn() -> T, + on_action: impl Fn(UiWidgetId) -> T, +) -> Element<'a, T> { + let entrypoint_name: Element<_> = text(entrypoint_name) + .into(); + + let (hide_action_panel, action_panel, bottom_panel) = match action_panel { + Some(action_panel) => { + let actions_text: Element<_> = text("Actions") + .into(); + + let actions_text: Element<_> = container(actions_text) + .themed(ContainerStyle::RootBottomPanelActionButtonText); + + let shortcut = render_shortcut(&PhysicalShortcut { + physical_key: PhysicalKey::KeyK, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + }); + + let action_panel_toggle_content: Element<_> = row(vec![actions_text, shortcut]) + .into(); + + let action_panel_toggle: Element<_> = button(action_panel_toggle_content) + .on_press(on_panel_toggle()) + .themed(ButtonStyle::RootBottomPanelActionButton); + + let space = horizontal_space() + .into(); + + let bottom_panel: Element<_> = row(vec![entrypoint_name, space, action_panel_toggle]) + .align_items(Alignment::Center) + .into(); + + (!show_action_panel, Some(action_panel), bottom_panel) + } + None => { + let space: Element<_> = Space::with_height(16 + 8 + 2) // TODO get value from theme + .into(); + + let bottom_panel: Element<_> = row(vec![entrypoint_name, space]) + .into(); + + (true, None, bottom_panel) + } + }; + + let bottom_panel: Element<_> = container(bottom_panel) + .width(Length::Fill) + .themed(ContainerStyle::RootBottomPanel); + let content: Element<_> = container(content) .width(Length::Fill) .height(Length::Fill) @@ -1461,10 +1553,15 @@ fn render_root<'a>( content } else { mouse_area(content) - .on_press(ComponentWidgetEvent::ToggleActionPanel { widget_id }) + .on_press(on_panel_toggle()) .into() }; + let action_panel_element = match action_panel { + None => Space::with_height(1).into(), + Some(action_panel) => render_action_panel(action_panel, on_action) + }; + floating_element(content, action_panel_element) .offset(Offset::from([8.0, 48.0])) // TODO calculate based on theme .hide(hide_action_panel) @@ -1483,8 +1580,6 @@ fn render_text_part<'a>(value: &str, context: ComponentRenderContext) -> Element ComponentRenderContext::List { .. } => panic!("not supposed to be passed to text part"), ComponentRenderContext::Grid { .. } => panic!("not supposed to be passed to text part"), ComponentRenderContext::Root { .. } => panic!("not supposed to be passed to text part"), - ComponentRenderContext::ActionPanel { .. } => panic!("not supposed to be passed to text part"), - ComponentRenderContext::Action { .. } => panic!("not supposed to be passed to text part"), ComponentRenderContext::Inline => panic!("not supposed to be passed to text part"), ComponentRenderContext::GridItem => panic!("not supposed to be passed to text part"), ComponentRenderContext::InlineRoot { .. } => panic!("not supposed to be passed to text part") @@ -1504,7 +1599,7 @@ fn render_text_part<'a>(value: &str, context: ComponentRenderContext) -> Element text.into() } -fn render_shortcut<'a>(shortcut: &PhysicalShortcut) -> Element<'a, ComponentWidgetEvent> { +fn render_shortcut<'a, T: 'a>(shortcut: &PhysicalShortcut) -> Element<'a, T> { let mut result = vec![]; let ( @@ -1515,9 +1610,9 @@ fn render_shortcut<'a>(shortcut: &PhysicalShortcut) -> Element<'a, ComponentWidg shift_modifier_text ) = shortcut_to_text(shortcut); - fn apply_modifier<'result, 'element>( - result: &'result mut Vec>, - modifier: Option> + fn apply_modifier<'result, 'element, T: 'element>( + result: &'result mut Vec>, + modifier: Option> ) { if let Some(modifier) = modifier { let modifier: Element<_> = container(modifier) From f39b3034715e2f3cdbb413654f136934e86f3421 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 27 Sep 2024 19:02:00 +0200 Subject: [PATCH 085/540] Implement Alt + K shortcut for main search view action panel --- rust/client/src/ui/mod.rs | 79 ++++++++++--------- rust/common_ui/src/lib.rs | 16 +++- .../src/components/shortcut_selector.rs | 15 +--- 3 files changed, 60 insertions(+), 50 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index c3ef523..e98fc8e 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -490,6 +490,7 @@ impl Application for AppModel { match event { keyboard::Event::KeyPressed { key, modifiers, physical_key, text, .. } => { + tracing::debug!("Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", key, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); match key { Key::Named(Named::ArrowUp) => self.focus_previous(), Key::Named(Named::ArrowDown) => self.focus_next(), @@ -500,50 +501,46 @@ impl Application for AppModel { }, _ => { if self.plugin_view_data.is_none() { - match text { - Some(text) => { - self.append_prompt(text.to_string()); - focus(self.search_field_id.clone()) + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { + Command::perform(async {}, |_| AppMsg::ToggleActionPanel) } - None => { - Command::none() + _ => { + match text { + Some(text) => { + self.append_prompt(text.to_string()); + focus(self.search_field_id.clone()) + } + None => { + Command::none() + } + } } } } else { - match physical_key_model(physical_key) { - Some(name) => { - tracing::debug!("physical key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", name, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { + let client_context = self.client_context.read().expect("lock is poisoned"); - let modifier_shift = modifiers.shift(); - let modifier_control = modifiers.control(); - let modifier_alt = modifiers.alt(); - let modifier_meta = modifiers.logo(); + client_context.show_action_panel(); - match (&name, modifier_shift, modifier_control, modifier_alt, modifier_meta) { - (PhysicalKey::KeyK, false, false, true, false) => { - let client_context = self.client_context.read().expect("lock is poisoned"); + Command::none() + } + Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { + let (plugin_id, entrypoint_id) = { + let client_context = self.client_context.read().expect("lock is poisoned"); + (client_context.get_view_plugin_id(), client_context.get_view_entrypoint_id()) + }; - client_context.show_action_panel(); + Command::perform( + async move { + backend_client.send_keyboard_event(plugin_id, entrypoint_id, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + .await?; - Command::none() - } - (_, _, _, _, _) => { - let (plugin_id, entrypoint_id) = { - let client_context = self.client_context.read().expect("lock is poisoned"); - (client_context.get_view_plugin_id(), client_context.get_view_entrypoint_id()) - }; - - Command::perform( - async move { - backend_client.send_keyboard_event(plugin_id, entrypoint_id, name, modifier_shift, modifier_control, modifier_alt, modifier_meta) - .await?; - - Ok(()) - }, - |result| handle_backend_error(result, |()| AppMsg::Noop), - ) - } - } + Ok(()) + }, + |result| handle_backend_error(result, |()| AppMsg::Noop), + ) } None => { Command::none() @@ -1086,8 +1083,16 @@ impl Application for AppModel { let events_subscription = event::listen_with(|event, status| match status { event::Status::Ignored => Some(AppMsg::IcedEvent(event)), - event::Status::Captured => match event { + event::Status::Captured => match &event { Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Named(Named::Escape), .. }) => Some(AppMsg::IcedEvent(event)), + Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Character(char), modifiers, .. }) => { + if char == "k" && modifiers.alt() { + // TODO this still enters "k" into a search bar which is undesirable + Some(AppMsg::IcedEvent(event)) + } else { + None + } + }, _ => None } }); diff --git a/rust/common_ui/src/lib.rs b/rust/common_ui/src/lib.rs index df24c1a..bba0880 100644 --- a/rust/common_ui/src/lib.rs +++ b/rust/common_ui/src/lib.rs @@ -1,5 +1,6 @@ use iced::advanced::widget::Text; use iced::Element; +use iced::keyboard::Modifiers; use iced::widget::text; use iced_aw::core::icons; @@ -88,7 +89,7 @@ pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>(shortcut: &Ph (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) } -pub fn physical_key_model(key: iced::keyboard::key::PhysicalKey) -> Option { +pub fn physical_key_model(key: iced::keyboard::key::PhysicalKey, modifiers: Modifiers) -> Option { let model_key = match key { iced::keyboard::key::PhysicalKey::Backquote => PhysicalKey::Backquote, iced::keyboard::key::PhysicalKey::Backslash => PhysicalKey::Backslash, @@ -290,7 +291,18 @@ pub fn physical_key_model(key: iced::keyboard::key::PhysicalKey) -> Option (&'static str, bool) { diff --git a/rust/management_client/src/components/shortcut_selector.rs b/rust/management_client/src/components/shortcut_selector.rs index 6200982..85307dc 100644 --- a/rust/management_client/src/components/shortcut_selector.rs +++ b/rust/management_client/src/components/shortcut_selector.rs @@ -197,18 +197,11 @@ where event::Status::Ignored } _ => { - match physical_key_model(physical_key) { + match physical_key_model(physical_key, modifiers) { None => event::Status::Ignored, - Some(physical_key) => { - let message = (self.on_shortcut_captured)( - PhysicalShortcut { - physical_key, - modifier_shift: modifiers.shift(), - modifier_control: modifiers.control(), - modifier_alt: modifiers.alt(), - modifier_meta: modifiers.logo(), - } - ); + Some(shortcut) => { + let message = (self.on_shortcut_captured)(shortcut); + shell.publish(message); state.is_capturing = false; From 66cd004d32271ae3db90d56db8062eb02680a6ab Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 27 Sep 2024 19:57:07 +0200 Subject: [PATCH 086/540] Add default action label with shortcut to the action bar --- rust/client/src/ui/mod.rs | 39 ++++++++------- rust/client/src/ui/theme/button.rs | 10 ++-- rust/client/src/ui/theme/container.rs | 10 ++-- rust/client/src/ui/theme/mod.rs | 26 +++++++--- rust/client/src/ui/theme/row.rs | 10 +++- rust/client/src/ui/theme/rule.rs | 9 ++++ rust/client/src/ui/theme/text.rs | 8 +++ rust/client/src/ui/widget.rs | 71 +++++++++++++++++++++++---- 8 files changed, 141 insertions(+), 42 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index e98fc8e..9121af6 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -996,26 +996,33 @@ impl Application for AppModel { list, ]).into(); - let action_panel_items = if let Some(search_item) = self.search_results.get(self.focused_search_result) { + let (default_action, action_panel) = if let Some(search_item) = self.search_results.get(self.focused_search_result) { let title = match search_item.entrypoint_type { SearchResultEntrypointType::Command => "Run Command", SearchResultEntrypointType::View => "Open View", SearchResultEntrypointType::GeneratedCommand => "Run Command", }.to_string(); - vec![ActionPanelItems::Action { - title, - widget_id: 0, - physical_shortcut: Some(PhysicalShortcut { - physical_key: PhysicalKey::Enter, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: false, - }), - }] + let shortcut = PhysicalShortcut { + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }; + + // let action_panel = ActionPanel { + // title: None, + // items: vec![ActionPanelItems::Action { + // title: title.clone(), + // widget_id: 0, + // physical_shortcut: Some(shortcut.clone()), + // }], + // }; + + (Some((title, shortcut)), None) } else { - vec![] + (None, None) }; let root = render_root( @@ -1023,10 +1030,8 @@ impl Application for AppModel { input, separator, content, - Some(ActionPanel { - title: None, - items: action_panel_items, - }), + default_action, + action_panel, "".to_string(), || AppMsg::ToggleActionPanel, AppMsg::RunAction diff --git a/rust/client/src/ui/theme/button.rs b/rust/client/src/ui/theme/button.rs index ce0de69..1b13b0a 100644 --- a/rust/client/src/ui/theme/button.rs +++ b/rust/client/src/ui/theme/button.rs @@ -17,7 +17,7 @@ pub enum ButtonStyle { MainListItem, MainListItemFocused, MetadataLink, - RootBottomPanelActionButton, + RootBottomPanelActionToggleButton, RootTopPanelBackButton, MetadataTagItem, } @@ -32,8 +32,8 @@ impl ButtonStyle { let theme = get_theme(); match self { - ButtonStyle::RootBottomPanelActionButton => { - let theme = &theme.root_bottom_panel_action_button; + ButtonStyle::RootBottomPanelActionToggleButton => { + let theme = &theme.root_bottom_panel_action_toggle_button; theme.padding.to_iced() }, @@ -80,8 +80,8 @@ impl ButtonStyle { fn appearance(&self, theme: &GauntletTheme, state: ButtonState) -> Appearance { let (background_color, background_color_hover, text_color, text_color_hover, border_radius, border_width, border_color) = match &self { - ButtonStyle::RootBottomPanelActionButton => { - let theme = &theme.root_bottom_panel_action_button; + ButtonStyle::RootBottomPanelActionToggleButton => { + let theme = &theme.root_bottom_panel_action_toggle_button; (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) }, ButtonStyle::RootTopPanelBackButton => { diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index b051070..56c81d1 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -44,7 +44,8 @@ pub enum ContainerStyle { PreferenceRequiredViewDescription, Root, RootBottomPanel, - RootBottomPanelActionButtonText, + RootBottomPanelDefaultActionText, + RootBottomPanelActionToggleText, RootInner, RootTopPanel, Grid, @@ -379,8 +380,11 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau ContainerStyle::ListInner => { self.padding(theme.list_inner.padding.to_iced()) } - ContainerStyle::RootBottomPanelActionButtonText => { - self.padding(theme.root_bottom_panel_action_button_text.padding.to_iced()) + ContainerStyle::RootBottomPanelActionToggleText => { + self.padding(theme.root_bottom_panel_action_toggle_text.padding.to_iced()) + } + ContainerStyle::RootBottomPanelDefaultActionText => { + self.padding(theme.root_bottom_panel_default_action_text.padding.to_iced()) } ContainerStyle::TextAccessory => { self.padding(theme.text_accessory.padding.to_iced()) diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 7dce43b..2b480f6 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -109,9 +109,10 @@ pub struct GauntletTheme { plugin_error_view_description: ThemePaddingOnly, plugin_error_view_title: ThemePaddingOnly, preference_required_view_description: ThemePaddingOnly, - root_bottom_panel: ThemePaddingBackgroundColor, - root_bottom_panel_action_button: ThemeButton, - root_bottom_panel_action_button_text: ThemePaddingOnly, + root_bottom_panel: ThemeBottomPanel, + root_bottom_panel_action_toggle_button: ThemeButton, + root_bottom_panel_action_toggle_text: ThemePaddingTextColor, + root_bottom_panel_default_action_text: ThemePaddingTextColor, root_content: ThemePaddingOnly, root_top_panel: ThemePaddingOnly, root_top_panel_button: ThemeButton, @@ -387,11 +388,12 @@ impl GauntletTheme { border_width: 0.0, border_color: TRANSPARENT, }, - root_bottom_panel: ThemePaddingBackgroundColor { + root_bottom_panel: ThemeBottomPanel { padding: padding_axis(6.0, 8.0), background_color: background_darker_color, + spacing: 8.0 }, - root_bottom_panel_action_button: ThemeButton { + root_bottom_panel_action_toggle_button: ThemeButton { padding: padding_axis(3.0, 5.0), background_color: TRANSPARENT, background_color_hovered: background_lighter_color, @@ -401,8 +403,13 @@ impl GauntletTheme { border_width: 0.0, border_color: TRANSPARENT, }, - root_bottom_panel_action_button_text: ThemePaddingOnly { + root_bottom_panel_action_toggle_text: ThemePaddingTextColor { padding: padding(0.0, 8.0, 0.0, 4.0), + text_color: text_lighter_color + }, + root_bottom_panel_default_action_text: ThemePaddingTextColor { + padding: padding(0.0, 8.0, 0.0, 4.0), + text_color: text_lightest_color }, list_item: ThemeButton { padding: padding_all(5.0), @@ -825,6 +832,13 @@ pub struct ThemePaddingBackgroundColor { background_color: ThemeColor, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThemeBottomPanel { + padding: ThemePadding, + background_color: ThemeColor, + spacing: f32, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ThemeTooltip { padding: f32, // TODO for some reason padding on tooltip is a single number in iced-rs diff --git a/rust/client/src/ui/theme/row.rs b/rust/client/src/ui/theme/row.rs index b2f50aa..382f77d 100644 --- a/rust/client/src/ui/theme/row.rs +++ b/rust/client/src/ui/theme/row.rs @@ -1,4 +1,4 @@ -use iced::Renderer; +use iced::{Padding, Renderer}; use iced::widget::Row; use crate::ui::theme::{Element, GauntletTheme, get_theme, ThemableWidget}; @@ -8,6 +8,8 @@ pub enum RowStyle { ListSectionTitle, GridSectionTitle, GridItemTitle, + RootBottomPanel, + RootBottomPanelDefaultAction, } impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletTheme, Renderer> { @@ -34,6 +36,12 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletT RowStyle::GridItemTitle => { self.padding(theme.grid_item_title.padding.to_iced()) } + RowStyle::RootBottomPanel => { + self.spacing(theme.root_bottom_panel.spacing) + } + RowStyle::RootBottomPanelDefaultAction => { + self.padding(Padding::from([0.0, theme.root_bottom_panel.spacing, 0.0, 0.0])) + } }.into() } } \ No newline at end of file diff --git a/rust/client/src/ui/theme/rule.rs b/rust/client/src/ui/theme/rule.rs index 600616b..4a659a6 100644 --- a/rust/client/src/ui/theme/rule.rs +++ b/rust/client/src/ui/theme/rule.rs @@ -8,6 +8,7 @@ pub enum RuleStyle { #[default] Default, ActionPanel, + DefaultActionSeparator, } impl rule::StyleSheet for GauntletTheme { @@ -33,6 +34,14 @@ impl rule::StyleSheet for GauntletTheme { fill_mode: rule::FillMode::Percent(96.0), } } + RuleStyle::DefaultActionSeparator => { + Appearance { + color: theme.separator.color.to_iced(), + width: 1, + radius: 0.0.into(), + fill_mode: rule::FillMode::Percent(70.0), + } + } } } } diff --git a/rust/client/src/ui/theme/text.rs b/rust/client/src/ui/theme/text.rs index c404b55..c19748d 100644 --- a/rust/client/src/ui/theme/text.rs +++ b/rust/client/src/ui/theme/text.rs @@ -23,6 +23,8 @@ pub enum TextStyle { GridItemSubTitle, InlineName, InlineSeparator, + RootBottomPanelDefaultActionText, + RootBottomPanelActionToggleText, } impl<'a, Message: 'a> ThemableWidget<'a, Message> for Text<'a, GauntletTheme, Renderer> { @@ -97,6 +99,12 @@ impl text::StyleSheet for GauntletTheme { }, TextStyle::InlineSeparator => Appearance { color: Some(self.inline_separator.text_color.to_iced()), + }, + TextStyle::RootBottomPanelDefaultActionText => Appearance { + color: Some(self.root_bottom_panel_default_action_text.text_color.to_iced()), + }, + TextStyle::RootBottomPanelActionToggleText => Appearance { + color: Some(self.root_bottom_panel_action_toggle_text.text_color.to_iced()), } } } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 5c8bb16..5b40ec7 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1304,6 +1304,7 @@ fn render_plugin_root<'a>( top_panel, top_separator, content, + None, action_panel, entrypoint_name, || ComponentWidgetEvent::ToggleActionPanel { widget_id }, @@ -1486,6 +1487,7 @@ pub fn render_root<'a, T: 'a + Clone>( top_panel: Element<'a, T>, top_separator: Element<'a, T>, content: Element<'a, T>, + default_action: Option<(String, PhysicalShortcut)>, action_panel: Option, entrypoint_name: String, on_panel_toggle: impl Fn() -> T, @@ -1494,13 +1496,33 @@ pub fn render_root<'a, T: 'a + Clone>( let entrypoint_name: Element<_> = text(entrypoint_name) .into(); + let panel_height = 16 + 8 + 2; // TODO get value from theme + + let default_action = match default_action { + Some((label, shortcut)) => { + let label: Element<_> = text(label) + .themed(TextStyle::RootBottomPanelDefaultActionText); + + let label: Element<_> = container(label) + .themed(ContainerStyle::RootBottomPanelDefaultActionText); + + let shortcut = render_shortcut(&shortcut); + + let content: Element<_> = row(vec![label, shortcut]) + .themed(RowStyle::RootBottomPanelDefaultAction); + + Some(content) + } + None => None + }; + let (hide_action_panel, action_panel, bottom_panel) = match action_panel { Some(action_panel) => { let actions_text: Element<_> = text("Actions") - .into(); + .themed(TextStyle::RootBottomPanelActionToggleText); let actions_text: Element<_> = container(actions_text) - .themed(ContainerStyle::RootBottomPanelActionButtonText); + .themed(ContainerStyle::RootBottomPanelActionToggleText); let shortcut = render_shortcut(&PhysicalShortcut { physical_key: PhysicalKey::KeyK, @@ -1510,28 +1532,57 @@ pub fn render_root<'a, T: 'a + Clone>( modifier_meta: false, }); + let mut bottom_panel_content = vec![entrypoint_name]; + + let space = horizontal_space() + .into(); + + bottom_panel_content.push(space); + + if let Some(default_action) = default_action { + bottom_panel_content.push(default_action); + + let rule: Element<_> = vertical_rule(1) + .style(RuleStyle::DefaultActionSeparator) + .into(); + + let rule: Element<_> = container(rule) + .width(Length::Shrink) + .height(panel_height) + .max_height(panel_height) + .into(); + + bottom_panel_content.push(rule); + } + let action_panel_toggle_content: Element<_> = row(vec![actions_text, shortcut]) .into(); let action_panel_toggle: Element<_> = button(action_panel_toggle_content) .on_press(on_panel_toggle()) - .themed(ButtonStyle::RootBottomPanelActionButton); + .themed(ButtonStyle::RootBottomPanelActionToggleButton); - let space = horizontal_space() - .into(); + bottom_panel_content.push(action_panel_toggle); - let bottom_panel: Element<_> = row(vec![entrypoint_name, space, action_panel_toggle]) + let bottom_panel: Element<_> = row(bottom_panel_content) .align_items(Alignment::Center) - .into(); + .themed(RowStyle::RootBottomPanel); (!show_action_panel, Some(action_panel), bottom_panel) } None => { - let space: Element<_> = Space::with_height(16 + 8 + 2) // TODO get value from theme + let space: Element<_> = Space::new(Length::Fill, panel_height) .into(); - let bottom_panel: Element<_> = row(vec![entrypoint_name, space]) - .into(); + let mut bottom_panel_content = vec![entrypoint_name, space]; + + if let Some(default_action) = default_action { + bottom_panel_content.push(default_action); + } + + let bottom_panel: Element<_> = row(bottom_panel_content) + .align_items(Alignment::Center) + .themed(RowStyle::RootBottomPanel); (true, None, bottom_panel) } From 4b33f2af5122056c4b29ec6b3d5863cea783196d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 27 Sep 2024 20:09:38 +0200 Subject: [PATCH 087/540] Make action panel slightly bigger --- rust/client/src/ui/theme/container.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 56c81d1..4f6e216 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -247,8 +247,8 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau ContainerStyle::ActionPanel => { self.style(ContainerStyleInner::ActionPanel) .padding(theme.action_panel.padding.to_iced()) - .height(Length::Fixed(200.0)) - .width(Length::Fixed(300.0)) + .height(Length::Fixed(250.0)) + .width(Length::Fixed(350.0)) } ContainerStyle::MetadataTagItem => { self.padding(theme.metadata_tag_item.padding.to_iced()) From 1e181ea1a839f707d938c9f026a9a11d93031d19 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 28 Sep 2024 16:42:51 +0200 Subject: [PATCH 088/540] Rename action title to label --- dev_plugin/src/detail-view.tsx | 8 ++++---- dev_plugin/src/form-view.tsx | 6 +++--- .../components/action/props/{title.md => label.md} | 0 js/api/src/gen/components.tsx | 6 +++--- rust/client/src/ui/widget.rs | 12 ++++++------ rust/component_model/src/lib.rs | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) rename docs/js/components/action/props/{title.md => label.md} (100%) diff --git a/dev_plugin/src/detail-view.tsx b/dev_plugin/src/detail-view.tsx index fc66b0b..97d63a4 100644 --- a/dev_plugin/src/detail-view.tsx +++ b/dev_plugin/src/detail-view.tsx @@ -84,21 +84,21 @@ export default function DetailView(): ReactElement { actions={ { console.log("ActionTest 1") }} /> { console.log("ActionTest 2.1") }} /> { console.log("ActionTest 2.2") }} @@ -107,7 +107,7 @@ export default function DetailView(): ReactElement { { console.log("ActionTest 3") }} diff --git a/dev_plugin/src/form-view.tsx b/dev_plugin/src/form-view.tsx index 07c34e5..1f9ed5b 100644 --- a/dev_plugin/src/form-view.tsx +++ b/dev_plugin/src/form-view.tsx @@ -13,20 +13,20 @@ export default function FormView(): ReactElement { actions={ { console.log("ActionTest Form 1") }} /> { console.log("ActionTest Form 2") }} /> { console.log("ActionTest Form 3") }} diff --git a/docs/js/components/action/props/title.md b/docs/js/components/action/props/label.md similarity index 100% rename from docs/js/components/action/props/title.md rename to docs/js/components/action/props/label.md diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index 7d81c0e..e04fa92 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -5,7 +5,7 @@ declare global { interface IntrinsicElements { ["gauntlet:action"]: { id?: string; - title: string; + label: string; onAction: () => void; }; ["gauntlet:action_panel_section"]: { @@ -355,11 +355,11 @@ export enum Icons { } export interface ActionProps { id?: string; - title: string; + label: string; onAction: () => void; } export const Action: FC = (props: ActionProps): ReactNode => { - return ; + return ; }; export interface ActionPanelSectionProps { children?: ElementComponent; diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 5b40ec7..f187773 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1319,7 +1319,7 @@ pub struct ActionPanel { pub enum ActionPanelItems { Action { - title: String, + label: String, widget_id: UiWidgetId, physical_shortcut: Option }, @@ -1356,14 +1356,14 @@ fn convert_action_panel(children: &[ComponentWidgetWrapper], action_shortcuts: H let (widget, _) = &*child.get(); match widget { - ComponentWidget::Action { id, title } => { + ComponentWidget::Action { id, label } => { let physical_shortcut: Option = id.as_ref() .map(|id| action_shortcuts.get(id)) .flatten() .cloned(); items.push(ActionPanelItems::Action { - title: title.clone(), + label: label.clone(), widget_id, physical_shortcut, }); @@ -1413,7 +1413,7 @@ fn render_action_panel_items<'a, T: 'a + Clone>(title: Option, items: Ve for item in items { match item { - ActionPanelItems::Action { title, widget_id, physical_shortcut } => { + ActionPanelItems::Action { label, widget_id, physical_shortcut } => { if place_separator { let separator: Element<_> = horizontal_rule(1) .themed(RuleStyle::ActionPanel); @@ -1427,7 +1427,7 @@ fn render_action_panel_items<'a, T: 'a + Clone>(title: Option, items: Ve .map(|shortcut| render_shortcut(shortcut)); let content: Element<_> = if let Some(shortcut_element) = shortcut_element { - let text: Element<_> = text(title) + let text: Element<_> = text(label) .into(); let space: Element<_> = horizontal_space() @@ -1437,7 +1437,7 @@ fn render_action_panel_items<'a, T: 'a + Clone>(title: Option, items: Ve .align_items(Alignment::Center) .into() } else { - text(title) + text(label) .into() }; diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index f4c1057..3849612 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -576,7 +576,7 @@ pub fn create_component_model() -> Vec { "Action", [ property("id", mark_doc!("/action/props/id.md"), true, PropertyType::String), - property("title", mark_doc!("/action/props/title.md"), false, PropertyType::String), + property("label", mark_doc!("/action/props/label.md"), false, PropertyType::String), event("onAction", mark_doc!("/action/props/onAction.md"), false, []) ], children_none(), From 9346c6f39460cd930a6e5486034594ae8b991589 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 28 Sep 2024 20:18:55 +0200 Subject: [PATCH 089/540] Implement secondary command actions for main search view --- dev_plugin/src/command-generator.ts | 17 +- js/api/src/helpers.ts | 7 + js/core/src/command-generator.ts | 25 ++- js/core/src/init.tsx | 2 +- js/typings/index.d.ts | 7 + rust/client/src/ui/mod.rs | 79 ++++--- rust/common/src/model.rs | 12 +- rust/common/src/rpc/backend_api.rs | 3 +- rust/server/src/lib.rs | 4 +- rust/server/src/model.rs | 7 +- rust/server/src/plugins/data_db_repository.rs | 60 ++++++ rust/server/src/plugins/js/mod.rs | 9 +- rust/server/src/plugins/js/search.rs | 56 ++++- rust/server/src/plugins/mod.rs | 64 +----- rust/server/src/search.rs | 194 +++++++++--------- 15 files changed, 340 insertions(+), 206 deletions(-) diff --git a/dev_plugin/src/command-generator.ts b/dev_plugin/src/command-generator.ts index 39f91af..76ce1f1 100644 --- a/dev_plugin/src/command-generator.ts +++ b/dev_plugin/src/command-generator.ts @@ -24,7 +24,22 @@ export default function CommandGenerator(): GeneratedCommand[] { localStorage.setItem("test", "test") console.dir(localStorage.getItem("test")) - } + }, + actions: [ + { + ref: "testAction1", + label: "Test 1", + fn: () => { + console.log('generated-action-1') + } + }, + { + label: "Test 2", + fn: () => { + console.log('generated-action-2') + } + } + ] }, { id: 'generated-test-3', diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index 26ab642..e1127ea 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -22,6 +22,13 @@ export interface GeneratedCommand { name: string icon?: ArrayBuffer fn: () => void + actions?: GeneratedCommandAction[] +} + +export interface GeneratedCommandAction { + ref?: string + label: string + fn: () => void } export const Clipboard: Clipboard = { diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index fdf4009..4cab0ee 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -7,6 +7,13 @@ interface GeneratedCommand { // TODO is it possible to import api here name: string icon?: ArrayBuffer fn: () => void + actions?: GeneratedCommandAction[] +} + +export interface GeneratedCommandAction { + ref?: string + label: string + fn: () => void } type ProcessedGeneratedCommand = GeneratedCommand & { lookupId: string, uuid: string }; @@ -49,14 +56,28 @@ export function generatedCommandSearchIndex(): AdditionalSearchItem[] { entrypoint_uuid: value.uuid, entrypoint_name: value.name, entrypoint_icon: value.icon, + entrypoint_actions: (value.actions || []) + .map(action => ({ + id: action.ref, + label: action.label + })), })) } -export function runGeneratedCommand(entrypointId: string) { +export function runGeneratedCommand(entrypointId: string, action_index: number | undefined) { const generatedCommand = storedGeneratedCommands.find(value => value.lookupId === entrypointId); if (generatedCommand) { - generatedCommand.fn() + if (typeof action_index == "number") { + const actions = generatedCommand.actions; + if (actions) { + actions[action_index].fn() + } else { + throw new Error("Generated command with entrypoint id '" + entrypointId + "' doesn't have actions, action index: " + action_index) + } + } else { + generatedCommand.fn() + } } else { throw new Error("Generated command with entrypoint id '" + entrypointId + "' not found") } diff --git a/js/core/src/init.tsx b/js/core/src/init.tsx index 697537f..bc312d3 100644 --- a/js/core/src/init.tsx +++ b/js/core/src/init.tsx @@ -181,7 +181,7 @@ async function runLoop() { } case "RunGeneratedCommand": { try { - runGeneratedCommand(pluginEvent.entrypointId) + runGeneratedCommand(pluginEvent.entrypointId, pluginEvent.actionIndex) } catch (e) { console.error("Error occurred when running a generated command", pluginEvent.entrypointId, e) } diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 052c03a..65b2c1b 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -47,6 +47,7 @@ type RunCommand = { type RunGeneratedCommand = { type: "RunGeneratedCommand" entrypointId: string + actionIndex: number | undefined } type OpenInlineView = { @@ -83,6 +84,12 @@ type AdditionalSearchItem = { entrypoint_id: string, entrypoint_uuid: string, entrypoint_icon: ArrayBuffer | undefined, + entrypoint_actions: AdditionalSearchItemAction[], +} + +type AdditionalSearchItemAction = { + id?: string, + label: string, } interface InternalApi { diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 9121af6..559cf9d 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -120,6 +120,7 @@ pub enum AppMsg { RunGeneratedCommandEvent { plugin_id: PluginId, entrypoint_id: EntrypointId, + action_index: Option }, PromptChanged(String), PromptSubmit, @@ -139,7 +140,7 @@ pub enum AppMsg { ShowWindow, HideWindow, ToggleActionPanel, - RunAction(UiWidgetId), + OnEntrypointAction(UiWidgetId), ShowPreferenceRequiredView { plugin_id: PluginId, entrypoint_id: EntrypointId, @@ -158,7 +159,7 @@ pub enum AppMsg { entrypoint_id: EntrypointId, render_location: UiRenderLocation }, - SelectSearchItem(SearchResult), + RunSearchItemAction(SearchResult, Option), RequestSearchResultUpdate, Screenshot { save_path: String @@ -424,10 +425,10 @@ impl Application for AppModel { self.run_command(plugin_id, entrypoint_id), ]) } - AppMsg::RunGeneratedCommandEvent { plugin_id, entrypoint_id } => { + AppMsg::RunGeneratedCommandEvent { plugin_id, entrypoint_id, action_index } => { Command::batch([ self.hide_window(), - self.run_generated_command(plugin_id, entrypoint_id), + self.run_generated_command(plugin_id, entrypoint_id, action_index), ]) } AppMsg::PromptChanged(mut new_prompt) => { @@ -465,7 +466,7 @@ impl Application for AppModel { AppMsg::PromptSubmit => { if let Some(search_item) = self.search_results.get(self.focused_search_result) { let search_item = search_item.clone(); - Command::perform(async {}, |_| AppMsg::SelectSearchItem(search_item)) + Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) } else { Command::none() } @@ -647,7 +648,7 @@ impl Application for AppModel { } Command::none() } - AppMsg::SelectSearchItem(search_result) => { + AppMsg::RunSearchItemAction(search_result, action_index) => { let event = match search_result.entrypoint_type { SearchResultEntrypointType::Command => AppMsg::RunCommand { entrypoint_id: search_result.entrypoint_id.clone(), @@ -661,7 +662,8 @@ impl Application for AppModel { }, SearchResultEntrypointType::GeneratedCommand => AppMsg::RunGeneratedCommandEvent { entrypoint_id: search_result.entrypoint_id.clone(), - plugin_id: search_result.plugin_id.clone() + plugin_id: search_result.plugin_id.clone(), + action_index, }, }; @@ -736,10 +738,17 @@ impl Application for AppModel { Command::none() } - AppMsg::RunAction(widget_id) => { - println!("widget_id: {}", widget_id); - - Command::none() + AppMsg::OnEntrypointAction(widget_id) => { + if let Some(search_item) = self.search_results.get(self.focused_search_result) { + let search_item = search_item.clone(); + if widget_id == 0 { + Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) + } else { + Command::perform(async {}, move |_| AppMsg::RunSearchItemAction(search_item, Some(widget_id - 1))) + } + } else { + Command::none() + } } } } @@ -967,7 +976,7 @@ impl Application for AppModel { let search_list = search_list( search_results, self.focused_search_result, - AppMsg::SelectSearchItem + |search_result| AppMsg::RunSearchItemAction(search_result, None) ); let search_list = container(search_list) @@ -997,13 +1006,13 @@ impl Application for AppModel { ]).into(); let (default_action, action_panel) = if let Some(search_item) = self.search_results.get(self.focused_search_result) { - let title = match search_item.entrypoint_type { + let label = match search_item.entrypoint_type { SearchResultEntrypointType::Command => "Run Command", SearchResultEntrypointType::View => "Open View", SearchResultEntrypointType::GeneratedCommand => "Run Command", }.to_string(); - let shortcut = PhysicalShortcut { + let default_shortcut = PhysicalShortcut { physical_key: PhysicalKey::Enter, modifier_shift: false, modifier_control: false, @@ -1011,16 +1020,34 @@ impl Application for AppModel { modifier_meta: false, }; - // let action_panel = ActionPanel { - // title: None, - // items: vec![ActionPanelItems::Action { - // title: title.clone(), - // widget_id: 0, - // physical_shortcut: Some(shortcut.clone()), - // }], - // }; + let mut actions: Vec<_> = search_item.entrypoint_actions + .iter() + .enumerate() + .map(|(index, action)| ActionPanelItems::Action { + label: action.label.clone(), + widget_id: index + 1, + physical_shortcut: action.shortcut.clone(), + }) + .collect(); - (Some((title, shortcut)), None) + if actions.len() == 0 { + (Some((label, default_shortcut)), None) + } else { + let default_action = ActionPanelItems::Action { + label: label.clone(), + widget_id: 0, + physical_shortcut: Some(default_shortcut.clone()), + }; + + actions.insert(0, default_action); + + let action_panel = ActionPanel { + title: Some(search_item.entrypoint_name.clone()), + items: actions, + }; + + (Some((label, default_shortcut)), Some(action_panel)) + } } else { (None, None) }; @@ -1034,7 +1061,7 @@ impl Application for AppModel { action_panel, "".to_string(), || AppMsg::ToggleActionPanel, - AppMsg::RunAction + AppMsg::OnEntrypointAction ); let root: Element<_> = container(root) @@ -1318,11 +1345,11 @@ impl AppModel { }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } - fn run_generated_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Command { + fn run_generated_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: Option) -> Command { let mut backend_client = self.backend_api.clone(); Command::perform(async move { - backend_client.request_run_generated_command(plugin_id, entrypoint_id) + backend_client.request_run_generated_command(plugin_id, entrypoint_id, action_index) .await?; Ok(()) diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index d78baed..0c733de 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -99,6 +99,13 @@ pub struct SearchResult { pub entrypoint_name: String, pub entrypoint_icon: Option, pub entrypoint_type: SearchResultEntrypointType, + pub entrypoint_actions: Vec, +} + +#[derive(Debug, Clone)] +pub struct SearchResultEntrypointAction { + pub label: String, + pub shortcut: Option, } #[derive(Debug, Clone)] @@ -172,7 +179,8 @@ pub enum BackendRequestData { }, RequestRunGeneratedCommand { plugin_id: PluginId, - entrypoint_id: EntrypointId + entrypoint_id: EntrypointId, + action_index: Option }, SendViewEvent { plugin_id: PluginId, @@ -273,7 +281,7 @@ pub trait UiPropertyValueToEnum { fn convert(value: &UiPropertyValue) -> anyhow::Result where Self: Sized; } -pub type UiWidgetId = u32; +pub type UiWidgetId = usize; #[derive(Debug, Clone)] pub struct SettingsEntrypoint { diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index 8686cb8..c419399 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -88,10 +88,11 @@ impl BackendForFrontendApi { Ok(()) } - pub async fn request_run_generated_command(&mut self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Result<(), BackendForFrontendApiError> { + pub async fn request_run_generated_command(&mut self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: Option) -> Result<(), BackendForFrontendApiError> { let request = BackendRequestData::RequestRunGeneratedCommand { plugin_id, entrypoint_id, + action_index, }; let BackendResponseData::Nothing = self.backend_sender.send_receive(request).await? else { diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index cd19ae6..78626b3 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -184,8 +184,8 @@ async fn handle_request(application_manager: Arc, request_da BackendResponseData::Nothing } - BackendRequestData::RequestRunGeneratedCommand { plugin_id, entrypoint_id } => { - application_manager.handle_run_generated_command(plugin_id, entrypoint_id) + BackendRequestData::RequestRunGeneratedCommand { plugin_id, entrypoint_id, action_index } => { + application_manager.handle_run_generated_command(plugin_id, entrypoint_id, action_index) .await; BackendResponseData::Nothing diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 46d7596..92a855c 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -51,7 +51,9 @@ pub enum JsUiEvent { }, RunGeneratedCommand { #[serde(rename = "entrypointId")] - entrypoint_id: String + entrypoint_id: String, + #[serde(rename = "actionIndex")] + action_index: Option }, ViewEvent { #[serde(rename = "widgetId")] @@ -120,7 +122,8 @@ pub enum IntermediateUiEvent { entrypoint_id: String }, RunGeneratedCommand { - entrypoint_id: String + entrypoint_id: String, + action_index: Option }, HandleViewEvent { widget_id: UiWidgetId, diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index b92baa0..69df4c9 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -491,6 +491,66 @@ impl DataDbRepository { Ok(entrypoint_id) } + pub async fn action_shortcuts(&self, plugin_id: &str, entrypoint_id: &str) -> anyhow::Result> { + let DbReadPluginEntrypoint { actions, actions_user_data, .. } = self.get_entrypoint_by_id(plugin_id, entrypoint_id) + .await?; + + let actions_user_data: HashMap<_, _> = actions_user_data.into_iter() + .map(|data| (data.id, (data.key, data.modifier_shift, data.modifier_control, data.modifier_alt, data.modifier_meta))) + .collect(); + + let action_shortcuts = actions.into_iter() + .map(|action| { + let id = action.id; + + let shortcut = match actions_user_data.get(&id) { + None => { + let (physical_key, modifier_shift) = match ActionShortcutKey::from_value(&action.key) { + Some(key) => key.to_physical_key(), + None => { + return Err(anyhow!("unknown key: {}", &action.key)) + }, + }; + + let (modifier_control, modifier_alt, modifier_meta) = match action.kind { + DbPluginActionShortcutKind::Main => { + if cfg!(target_os = "macos") { + (false, false, true) + } else { + (true, false, false) + } + }, + DbPluginActionShortcutKind::Alternative => { + (false, true, false) + }, + }; + + PhysicalShortcut { + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + } + } + Some(&(ref key, modifier_shift, modifier_control, modifier_alt, modifier_meta)) => { + PhysicalShortcut { + physical_key: PhysicalKey::from_value(key.to_owned()), + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + } + } + }; + + Ok((id, shortcut)) + }) + .collect::, _>>()?; + + Ok(action_shortcuts) + } + pub async fn get_action_id_for_shortcut( &self, plugin_id: &str, diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 809619c..498060b 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -107,6 +107,7 @@ pub enum OnePluginCommandData { }, RunGeneratedCommand { entrypoint_id: String, + action_index: Option }, HandleViewEvent { widget_id: UiWidgetId, @@ -166,9 +167,10 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run entrypoint_id, }) } - OnePluginCommandData::RunGeneratedCommand { entrypoint_id } => { + OnePluginCommandData::RunGeneratedCommand { entrypoint_id, action_index } => { Some(IntermediateUiEvent::RunGeneratedCommand { entrypoint_id, + action_index }) } OnePluginCommandData::HandleViewEvent { widget_id, event_name, event_arguments } => { @@ -642,8 +644,9 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { IntermediateUiEvent::RunCommand { entrypoint_id } => JsUiEvent::RunCommand { entrypoint_id }, - IntermediateUiEvent::RunGeneratedCommand { entrypoint_id } => JsUiEvent::RunGeneratedCommand { - entrypoint_id + IntermediateUiEvent::RunGeneratedCommand { entrypoint_id, action_index } => JsUiEvent::RunGeneratedCommand { + entrypoint_id, + action_index, }, IntermediateUiEvent::HandleViewEvent { widget_id, event_name, event_arguments } => { let event_arguments = event_arguments.into_iter() diff --git a/rust/server/src/plugins/js/search.rs b/rust/server/src/plugins/js/search.rs index 8a156e6..ee82b06 100644 --- a/rust/server/src/plugins/js/search.rs +++ b/rust/server/src/plugins/js/search.rs @@ -1,14 +1,14 @@ +use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginEntrypointType, DbReadPlugin, DbReadPluginEntrypoint}; +use crate::plugins::icon_cache::IconCache; +use crate::plugins::js::PluginData; +use crate::search::{SearchIndex, SearchIndexItem, SearchIndexItemAction}; +use anyhow::Context; +use common::model::{EntrypointId, PhysicalShortcut, SearchResultEntrypointType}; +use deno_core::{op, OpState}; +use serde::Deserialize; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use anyhow::Context; -use deno_core::{op, OpState}; -use serde::Deserialize; -use common::model::SearchResultEntrypointType; -use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginEntrypointType, DbReadPlugin}; -use crate::plugins::icon_cache::IconCache; -use crate::plugins::js::PluginData; -use crate::search::{SearchIndex, SearchIndexItem}; #[op] async fn reload_search_index(state: Rc>, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { @@ -55,6 +55,13 @@ async fn reload_search_index(state: Rc>, generated_commands: Ve .await .context("error when getting frecency for plugin")?; + let mut shortcuts = HashMap::new(); + + for DbReadPluginEntrypoint { id, .. } in &entrypoints { + let entrypoint_shortcuts = repository.action_shortcuts(&plugin_id.to_string(), id).await?; + shortcuts.insert(id.clone(), entrypoint_shortcuts); + } + let mut plugins_search_items = generated_commands.into_iter() .map(|item| { let entrypoint_icon_path = match item.entrypoint_icon { @@ -64,12 +71,32 @@ async fn reload_search_index(state: Rc>, generated_commands: Ve let entrypoint_frecency = frecency_map.get(&item.entrypoint_id).cloned().unwrap_or(0.0); + let shortcuts = shortcuts + .get(&item.entrypoint_id); + + let entrypoint_actions = item.entrypoint_actions.iter() + .map(|action| { + let shortcut = match (shortcuts, &action.id) { + (Some(shortcuts), Some(id)) => { + shortcuts.get(id).cloned() + } + _ => None + }; + + SearchIndexItemAction { + label: action.label.clone(), + shortcut, + } + }) + .collect(); + Ok(SearchIndexItem { entrypoint_type: SearchResultEntrypointType::GeneratedCommand, - entrypoint_id: item.entrypoint_id, + entrypoint_id: EntrypointId::from_string(item.entrypoint_id), entrypoint_name: item.entrypoint_name, entrypoint_icon_path, entrypoint_frecency, + entrypoint_actions, }) }) .collect::>>()?; @@ -105,6 +132,8 @@ async fn reload_search_index(state: Rc>, generated_commands: Ve }, }; + let entrypoint_id = EntrypointId::from_string(entrypoint_id); + match &entrypoint_type { DbPluginEntrypointType::Command => { Ok(Some(SearchIndexItem { @@ -113,6 +142,7 @@ async fn reload_search_index(state: Rc>, generated_commands: Ve entrypoint_id, entrypoint_icon_path, entrypoint_frecency, + entrypoint_actions: vec![], })) }, DbPluginEntrypointType::View => { @@ -122,6 +152,7 @@ async fn reload_search_index(state: Rc>, generated_commands: Ve entrypoint_id, entrypoint_icon_path, entrypoint_frecency, + entrypoint_actions: vec![], })) }, DbPluginEntrypointType::CommandGenerator | DbPluginEntrypointType::InlineView => { @@ -148,4 +179,11 @@ struct AdditionalSearchItem { entrypoint_id: String, entrypoint_uuid: String, entrypoint_icon: Option>, + entrypoint_actions: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct AdditionalSearchItemAction { + id: Option, + label: String, } \ No newline at end of file diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 7a6d91a..7fe80b9 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -108,8 +108,7 @@ impl ApplicationManager { } pub fn search(&self, text: &str, render_inline_view: bool) -> anyhow::Result> { - let result = self.search_index.create_handle() - .search(&text); + let result = self.search_index.search(&text); if render_inline_view { self.handle_inline_view(&text); @@ -360,11 +359,12 @@ impl ApplicationManager { self.mark_entrypoint_frecency(plugin_id, entrypoint_id).await } - pub async fn handle_run_generated_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) { + pub async fn handle_run_generated_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: Option) { self.send_command(PluginCommand::One { id: plugin_id.clone(), data: OnePluginCommandData::RunGeneratedCommand { entrypoint_id: entrypoint_id.to_string(), + action_index, } }); @@ -492,63 +492,7 @@ impl ApplicationManager { } async fn action_shortcuts(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> anyhow::Result> { - let DbReadPluginEntrypoint { actions, actions_user_data, .. } = self.db_repository.get_entrypoint_by_id(&plugin_id.to_string(), &entrypoint_id.to_string()) - .await?; - - let actions_user_data: HashMap<_, _> = actions_user_data.into_iter() - .map(|data| (data.id, (data.key, data.modifier_shift, data.modifier_control, data.modifier_alt, data.modifier_meta))) - .collect(); - - let action_shortcuts = actions.into_iter() - .map(|action| { - let id = action.id; - - let shortcut = match actions_user_data.get(&id) { - None => { - let (physical_key, modifier_shift) = match ActionShortcutKey::from_value(&action.key) { - Some(key) => key.to_physical_key(), - None => { - return Err(anyhow!("unknown key: {}", &action.key)) - }, - }; - - let (modifier_control, modifier_alt, modifier_meta) = match action.kind { - DbPluginActionShortcutKind::Main => { - if cfg!(target_os = "macos") { - (false, false, true) - } else { - (true, false, false) - } - }, - DbPluginActionShortcutKind::Alternative => { - (false, true, false) - }, - }; - - PhysicalShortcut { - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - } - } - Some(&(ref key, modifier_shift, modifier_control, modifier_alt, modifier_meta)) => { - PhysicalShortcut { - physical_key: PhysicalKey::from_value(key.to_owned()), - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - } - } - }; - - Ok((id, shortcut)) - }) - .collect::, _>>()?; - - Ok(action_shortcuts) + self.db_repository.action_shortcuts(&plugin_id.to_string(), &entrypoint_id.to_string()).await } async fn start_plugin(&self, plugin_id: PluginId) -> anyhow::Result<()> { diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index 822fcdd..e7dade9 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -1,11 +1,12 @@ use std::cmp::Ordering; +use std::collections::HashMap; use std::sync::{Arc, Mutex}; use tantivy::{doc, Index, IndexReader, ReloadPolicy, Searcher}; use tantivy::collector::TopDocs; use tantivy::query::{AllQuery, BooleanQuery, FuzzyTermQuery, Query, RegexQuery, TermQuery}; use tantivy::schema::*; use tantivy::tokenizer::TokenizerManager; -use common::model::{EntrypointId, PluginId, SearchResult, SearchResultEntrypointType}; +use common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType}; use common::rpc::frontend_api::FrontendApi; #[derive(Clone)] @@ -15,36 +16,57 @@ pub struct SearchIndex { index_reader: IndexReader, index_writer_mutex: Arc>, - entrypoint_type: Field, + entrypoint_data: Arc>>>, + entrypoint_name: Field, entrypoint_id: Field, - entrypoint_icon_path: Field, - entrypoint_frecency: Field, plugin_name: Field, plugin_id: Field, } +struct EntrypointData { + entrypoint_type: SearchResultEntrypointType, + icon_path: Option, + frecency: f64, + actions: Vec, +} + +struct EntrypointActionData { + label: String, + shortcut: Option, +} + +#[derive(Clone, Debug)] +pub struct SearchIndexItem { + pub entrypoint_type: SearchResultEntrypointType, + pub entrypoint_name: String, + pub entrypoint_id: EntrypointId, + pub entrypoint_icon_path: Option, + pub entrypoint_frecency: f64, + pub entrypoint_actions: Vec, +} + +#[derive(Clone, Debug)] +pub struct SearchIndexItemAction { + pub label: String, + pub shortcut: Option, +} + impl SearchIndex { pub fn create_index(frontend_api: FrontendApi) -> tantivy::Result { let schema = { let mut schema_builder = Schema::builder(); - schema_builder.add_text_field("entrypoint_type", STORED); schema_builder.add_text_field("entrypoint_name", TEXT | STORED); schema_builder.add_text_field("entrypoint_id", STRING | STORED); - schema_builder.add_text_field("entrypoint_icon_path", STORED); - schema_builder.add_text_field("entrypoint_frecency", STORED); schema_builder.add_text_field("plugin_name", TEXT | STORED); schema_builder.add_text_field("plugin_id", STRING | STORED); schema_builder.build() }; - let entrypoint_type = schema.get_field("entrypoint_type").expect("entrypoint_type field should exist"); let entrypoint_name = schema.get_field("entrypoint_name").expect("entrypoint_name field should exist"); let entrypoint_id = schema.get_field("entrypoint_id").expect("entrypoint_id field should exist"); - let entrypoint_icon_path = schema.get_field("entrypoint_icon_path").expect("entrypoint_icon_path field should exist"); - let entrypoint_frecency = schema.get_field("entrypoint_frecency").expect("entrypoint_frecency field should exist"); let plugin_name = schema.get_field("plugin_name").expect("plugin_name field should exist"); let plugin_id = schema.get_field("plugin_id").expect("plugin_id field should exist"); @@ -60,11 +82,9 @@ impl SearchIndex { index, index_reader, index_writer_mutex: Arc::new(Mutex::new(())), - entrypoint_type, + entrypoint_data: Arc::new(Mutex::new(HashMap::new())), entrypoint_name, entrypoint_id, - entrypoint_icon_path, - entrypoint_frecency, plugin_name, plugin_id, }) @@ -73,6 +93,7 @@ impl SearchIndex { pub fn remove_for_plugin(&self, plugin_id: PluginId) -> tantivy::Result<()> { // writer panics if another writer exists let _guard = self.index_writer_mutex.lock().expect("lock is poisoned"); + let mut entrypoint_data = self.entrypoint_data.lock().expect("lock is poisoned"); let mut index_writer = self.index.writer(5_000_000)?; @@ -81,6 +102,8 @@ impl SearchIndex { ))?; index_writer.commit()?; + entrypoint_data.remove(&plugin_id); + Ok(()) } @@ -89,6 +112,7 @@ impl SearchIndex { // writer panics if another writer exists let _guard = self.index_writer_mutex.lock().expect("lock is poisoned"); + let mut entrypoint_data = self.entrypoint_data.lock().expect("lock is poisoned"); let mut index_writer = self.index.writer(3_000_000)?; @@ -96,13 +120,10 @@ impl SearchIndex { TermQuery::new(Term::from_field_text(self.plugin_id, &plugin_id.to_string()), IndexRecordOption::Basic) ))?; - for search_item in search_items { + for search_item in &search_items { index_writer.add_document(doc!( - self.entrypoint_name => search_item.entrypoint_name, - self.entrypoint_type => search_index_entrypoint_to_str(search_item.entrypoint_type), - self.entrypoint_id => search_item.entrypoint_id, - self.entrypoint_icon_path => search_item.entrypoint_icon_path.unwrap_or_default(), - self.entrypoint_frecency => search_item.entrypoint_frecency, + self.entrypoint_name => search_item.entrypoint_name.clone(), + self.entrypoint_id => search_item.entrypoint_id.to_string(), self.plugin_name => plugin_name.clone(), self.plugin_id => plugin_id.to_string(), ))?; @@ -110,6 +131,28 @@ impl SearchIndex { index_writer.commit()?; + let data = search_items.iter() + .map(|item| { + let actions = item.entrypoint_actions.iter() + .map(|action| EntrypointActionData { + label: action.label.clone(), + shortcut: action.shortcut.clone(), + }) + .collect(); + + let data = EntrypointData { + entrypoint_type: item.entrypoint_type.clone(), + icon_path: item.entrypoint_icon_path.clone(), + frecency: item.entrypoint_frecency, + actions, + }; + + (item.entrypoint_id.clone(), data) + }) + .collect(); + + entrypoint_data.insert(plugin_id.clone(), data); + if refresh_search_list { let mut frontend_api = self.frontend_api.clone(); tokio::spawn(async move { @@ -127,7 +170,7 @@ impl SearchIndex { Ok(()) } - pub fn create_handle(&self) -> SearchHandle { + pub fn search(&self, query: &str) -> anyhow::Result> { let searcher = self.index_reader.searcher(); let query_parser = QueryParser::new( @@ -136,50 +179,12 @@ impl SearchIndex { self.plugin_name, ); - SearchHandle { - searcher, - query_parser, - entrypoint_type: self.entrypoint_type, - entrypoint_name: self.entrypoint_name, - entrypoint_id: self.entrypoint_id, - entrypoint_icon_path: self.entrypoint_icon_path, - entrypoint_frecency: self.entrypoint_frecency, - plugin_name: self.plugin_name, - plugin_id: self.plugin_id, - } - } -} - -#[derive(Clone, Debug)] -pub struct SearchIndexItem { - pub entrypoint_type: SearchResultEntrypointType, - pub entrypoint_name: String, - pub entrypoint_id: String, - pub entrypoint_icon_path: Option, - pub entrypoint_frecency: f64, -} - -pub struct SearchHandle { - searcher: Searcher, - query_parser: QueryParser, - - entrypoint_name: Field, - entrypoint_type: Field, - entrypoint_id: Field, - entrypoint_icon_path: Field, - entrypoint_frecency: Field, - plugin_name: Field, - plugin_id: Field, -} - -impl SearchHandle { - pub(crate) fn search(&self, query: &str) -> anyhow::Result> { - let query = self.query_parser.create_query(query); + let query = query_parser.create_query(query); let mut index = 0; let fetch = std::iter::from_fn(|| -> Option>> { - let result = self.fetch(&query, TopDocs::with_limit(20).and_offset(index * 20)); + let result = self.fetch(&query, TopDocs::with_limit(20).and_offset(index * 20), &searcher); index += 1; @@ -212,40 +217,52 @@ impl SearchHandle { Ok(result) } - fn fetch(&self, query: &dyn Query, collector: TopDocs) -> anyhow::Result> { + fn fetch(&self, query: &dyn Query, collector: TopDocs, searcher: &Searcher) -> anyhow::Result> { + let entrypoint_data = self.entrypoint_data.lock().expect("lock is poisoned"); + let get_str_field = |retrieved_doc: &Document, field: Field| -> String { retrieved_doc.get_first(field) - .unwrap_or_else(|| panic!("there should be a field with name {:?}", self.searcher.schema().get_field_name(field))) + .unwrap_or_else(|| panic!("there should be a field with name {:?}", searcher.schema().get_field_name(field))) .as_text() - .unwrap_or_else(|| panic!("field with name {:?} should contain string", self.searcher.schema().get_field_name(field))) + .unwrap_or_else(|| panic!("field with name {:?} should contain string", searcher.schema().get_field_name(field))) .to_owned() }; - let get_f64_field = |retrieved_doc: &Document, field: Field| -> f64 { - retrieved_doc.get_first(field) - .unwrap_or_else(|| panic!("there should be a field with name {:?}", self.searcher.schema().get_field_name(field))) - .as_f64() - .unwrap_or_else(|| panic!("field with name {:?} should contain string", self.searcher.schema().get_field_name(field))) - }; - - let result = self.searcher.search(query, &collector)? + let result = searcher.search(query, &collector)? .into_iter() .map(|(_score, doc_address)| { - let retrieved_doc = self.searcher.doc(doc_address) + let retrieved_doc = searcher.doc(doc_address) .expect("index should contain just searched results"); - let score = get_f64_field(&retrieved_doc, self.entrypoint_frecency); + let entrypoint_id = EntrypointId::from_string(get_str_field(&retrieved_doc, self.entrypoint_id)); + let plugin_id = PluginId::from_string(get_str_field(&retrieved_doc, self.plugin_id)); + let entrypoint_name = get_str_field(&retrieved_doc, self.entrypoint_name); + let plugin_name = get_str_field(&retrieved_doc, self.plugin_name); + + let entrypoint_data = entrypoint_data + .get(&plugin_id) + .expect("Plugin should always exist in entrypoint data") + .get(&entrypoint_id) + .expect("Plugin should always exist in entrypoint data"); + + let entrypoint_actions = entrypoint_data.actions.iter() + .map(|data| SearchResultEntrypointAction { + label: data.label.clone(), + shortcut: data.shortcut.clone(), + }) + .collect(); let result_item = SearchResult { - entrypoint_type: search_index_entrypoint_from_str(&get_str_field(&retrieved_doc, self.entrypoint_type)), - entrypoint_name: get_str_field(&retrieved_doc, self.entrypoint_name), - entrypoint_id: EntrypointId::from_string(get_str_field(&retrieved_doc, self.entrypoint_id)), - entrypoint_icon: Some(get_str_field(&retrieved_doc, self.entrypoint_icon_path)).filter(|value| value != ""), - plugin_name: get_str_field(&retrieved_doc, self.plugin_name), - plugin_id: PluginId::from_string(get_str_field(&retrieved_doc, self.plugin_id)), + entrypoint_type: entrypoint_data.entrypoint_type.clone(), + entrypoint_name, + entrypoint_id, + entrypoint_icon: entrypoint_data.icon_path.clone(), + plugin_name, + plugin_id, + entrypoint_actions, }; - (result_item, score) + (result_item, entrypoint_data.frecency) }) .collect::>(); @@ -320,20 +337,3 @@ impl QueryParser { terms } } - -fn search_index_entrypoint_to_str(value: SearchResultEntrypointType) -> &'static str { - match value { - SearchResultEntrypointType::Command => "command", - SearchResultEntrypointType::View => "view", - SearchResultEntrypointType::GeneratedCommand => "generated-command", - } -} - -fn search_index_entrypoint_from_str(value: &str) -> SearchResultEntrypointType { - match value { - "command" => SearchResultEntrypointType::Command, - "view" => SearchResultEntrypointType::View, - "generated-command" => SearchResultEntrypointType::GeneratedCommand, - _ => panic!("index contains illegal entrypoint_type: {}", value) - } -} \ No newline at end of file From f831cd4eb337e3c06fd3fff6942b3a29d991d9f9 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 28 Sep 2024 21:32:17 +0200 Subject: [PATCH 090/540] Implement keyboard navigation for main view action panel --- rust/client/src/ui/mod.rs | 197 ++++++++++++++++++++--------- rust/client/src/ui/search_list.rs | 6 +- rust/client/src/ui/theme/button.rs | 7 +- rust/client/src/ui/widget.rs | 81 ++++++++---- 4 files changed, 202 insertions(+), 89 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 559cf9d..6650d15 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -58,7 +58,6 @@ pub struct AppModel { backend_api: BackendForFrontendApi, frontend_receiver: Arc>>, search_field_id: text_input::Id, - scrollable_id: scrollable::Id, focused: bool, theme: GauntletTheme, wayland: bool, @@ -67,8 +66,8 @@ pub struct AppModel { // ephemeral state prompt: String, - focused_search_result: usize, - search_result_offset: usize, + focused_search_result: ScrollHandle, + focused_action_item: ScrollHandle, // state client_context: Arc>, @@ -78,6 +77,68 @@ pub struct AppModel { show_action_panel: bool, } +#[derive(Clone, Debug)] +struct ScrollHandle { + scrollable_id: scrollable::Id, + index: usize, + offset: usize, +} + +impl ScrollHandle { + fn new(scrollable_id: scrollable::Id) -> ScrollHandle { + ScrollHandle { + scrollable_id, + index: 0, + offset: 0, + } + } + + fn reset(&mut self) { + self.index = 0; + self.offset = 0; + } + + fn get<'a, T>(&self, search_results: &'a Vec) -> Option<&'a T> { + search_results.get(self.index) + } + + fn focus_next(&mut self, item_amount: usize) -> Command { + self.offset = if self.offset < 8 { + self.offset + 1 + } else { + 8 + }; + + if self.index < item_amount - 1 { + self.index = self.index + 1; + + let pos_y = self.index as f32 * ESTIMATED_ITEM_SIZE - (self.offset as f32 * ESTIMATED_ITEM_SIZE); + + scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) + } else { + Command::none() + } + } + + fn focus_previous(&mut self) -> Command { + self.offset = if self.offset > 1 { + self.offset - 1 + } else { + 1 + }; + + if self.index > 0 { + self.index = self.index - 1; + + let pos_y = self.index as f32 * ESTIMATED_ITEM_SIZE - (self.offset as f32 * ESTIMATED_ITEM_SIZE); + + scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) + } else { + Command::none() + } + } +} + struct PluginViewData { top_level_view: bool, plugin_id: PluginId, @@ -377,7 +438,6 @@ impl Application for AppModel { backend_api, frontend_receiver: Arc::new(TokioRwLock::new(frontend_receiver)), search_field_id: text_input::Id::unique(), - scrollable_id: scrollable::Id::unique(), focused: false, theme: GauntletTheme::new(), wayland, @@ -386,8 +446,8 @@ impl Application for AppModel { // ephemeral state prompt: "".to_string(), - focused_search_result: 0, - search_result_offset: 0, + focused_search_result: ScrollHandle::new(scrollable::Id::unique()), + focused_action_item: ScrollHandle::new(scrollable::Id::unique()), // state client_context: Arc::new(StdRwLock::new(client_context)), @@ -437,9 +497,10 @@ impl Application for AppModel { } else { new_prompt.truncate(100); // search query uses regex so just to be safe truncate the prompt + self.show_action_panel = false; self.prompt = new_prompt.clone(); - self.focused_search_result = 0; - self.search_result_offset = 0; + self.focused_search_result.reset(); + self.focused_action_item.reset(); let mut backend_api = self.backend_api.clone(); @@ -464,11 +525,18 @@ impl Application for AppModel { }, |result| handle_backend_error(result, |search_results| AppMsg::SetSearchResults(search_results))) } AppMsg::PromptSubmit => { - if let Some(search_item) = self.search_results.get(self.focused_search_result) { - let search_item = search_item.clone(); - Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) + if self.show_action_panel { + self.show_action_panel = false; + let widget_id = self.focused_action_item.index; + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) } else { - Command::none() + self.show_action_panel = false; + if let Some(search_item) = self.focused_search_result.get(&self.search_results) { + let search_item = search_item.clone(); + Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) + } else { + Command::none() + } } } AppMsg::SetSearchResults(search_results) => { @@ -496,6 +564,10 @@ impl Application for AppModel { Key::Named(Named::ArrowUp) => self.focus_previous(), Key::Named(Named::ArrowDown) => self.focus_next(), Key::Named(Named::Escape) => self.previous_view(), + Key::Named(Named::Enter) => { + // fired in cases where main text field is not focused + Command ::perform(async {}, |_| AppMsg::PromptSubmit) + }, Key::Named(Named::Backspace) => { self.backspace_prompt(); focus(self.search_field_id.clone()) @@ -739,7 +811,7 @@ impl Application for AppModel { Command::none() } AppMsg::OnEntrypointAction(widget_id) => { - if let Some(search_item) = self.search_results.get(self.focused_search_result) { + if let Some(search_item) = self.focused_search_result.get(&self.search_results) { let search_item = search_item.clone(); if widget_id == 0 { Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) @@ -975,7 +1047,7 @@ impl Application for AppModel { let search_list = search_list( search_results, - self.focused_search_result, + &self.focused_search_result, |search_result| AppMsg::RunSearchItemAction(search_result, None) ); @@ -984,7 +1056,7 @@ impl Application for AppModel { .themed(ContainerStyle::MainListInner); let list: Element<_> = scrollable(search_list) - .id(self.scrollable_id.clone()) + .id(self.focused_search_result.scrollable_id.clone()) .width(Length::Fill) .into(); @@ -1005,7 +1077,7 @@ impl Application for AppModel { list, ]).into(); - let (default_action, action_panel) = if let Some(search_item) = self.search_results.get(self.focused_search_result) { + let (default_action, action_panel) = if let Some(search_item) = self.focused_search_result.get(&self.search_results) { let label = match search_item.entrypoint_type { SearchResultEntrypointType::Command => "Run Command", SearchResultEntrypointType::View => "Open View", @@ -1059,6 +1131,7 @@ impl Application for AppModel { content, default_action, action_panel, + &self.focused_action_item, "".to_string(), || AppMsg::ToggleActionPanel, AppMsg::OnEntrypointAction @@ -1232,12 +1305,13 @@ impl AppModel { } fn reset_window_state(&mut self) -> Command { - self.focused_search_result = 0; - self.search_result_offset = 0; + self.focused_action_item.reset(); + self.focused_search_result.reset(); self.show_action_panel = false; let mut commands = vec![ - scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: 0.0 }), + scroll_to(self.focused_action_item.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: 0.0 }), + scroll_to(self.focused_search_result.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: 0.0 }), Command::perform(async {}, |_| AppMsg::PromptChanged("".to_owned())), focus(self.search_field_id.clone()) ]; @@ -1252,38 +1326,38 @@ impl AppModel { } fn focus_next(&mut self) -> Command { - self.search_result_offset = if self.search_result_offset < 8 { - self.search_result_offset + 1 + if self.show_action_panel { + if let Some(search_item) = self.focused_search_result.get(&self.search_results) { + if search_item.entrypoint_actions.len() != 0 { + self.focused_action_item.focus_next(search_item.entrypoint_actions.len() + 1) + } else { + self.show_action_panel = false; + Command::none() + } + } else { + self.show_action_panel = false; + Command::none() + } } else { - 8 - }; - - if self.focused_search_result < self.search_results.len() - 1 { - self.focused_search_result = self.focused_search_result + 1; - - let pos_y = self.focused_search_result as f32 * ESTIMATED_ITEM_SIZE - (self.search_result_offset as f32 * ESTIMATED_ITEM_SIZE); - - scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) - } else { - Command::none() + self.focused_search_result.focus_next(self.search_results.len()) } } fn focus_previous(&mut self) -> Command { - self.search_result_offset = if self.search_result_offset > 1 { - self.search_result_offset - 1 + if self.show_action_panel { + if let Some(search_item) = self.focused_search_result.get(&self.search_results) { + if search_item.entrypoint_actions.len() != 0 { + self.focused_action_item.focus_previous() + } else { + self.show_action_panel = false; + Command::none() + } + } else { + self.show_action_panel = false; + Command::none() + } } else { - 1 - }; - - if self.focused_search_result > 0 { - self.focused_search_result = self.focused_search_result - 1; - - let pos_y = self.focused_search_result as f32 * ESTIMATED_ITEM_SIZE - (self.search_result_offset as f32 * ESTIMATED_ITEM_SIZE); - - scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) - } else { - Command::none() + self.focused_search_result.focus_previous() } } @@ -1292,22 +1366,27 @@ impl AppModel { } fn previous_view(&mut self) -> Command { - match &self.plugin_view_data { - None => { - self.hide_window() - } - Some(PluginViewData { top_level_view: true, plugin_id, .. }) => { - let plugin_id = plugin_id.clone(); + if self.show_action_panel { + self.show_action_panel = false; + Command::none() + } else { + match &self.plugin_view_data { + None => { + self.hide_window() + } + Some(PluginViewData { top_level_view: true, plugin_id, .. }) => { + let plugin_id = plugin_id.clone(); - self.plugin_view_data.take(); + self.plugin_view_data.take(); - Command::batch([ - self.close_view(plugin_id), - focus(self.search_field_id.clone()), - ]) - } - Some(PluginViewData { top_level_view: false, plugin_id, entrypoint_id, .. }) => { - self.open_view(plugin_id.clone(), entrypoint_id.clone()) + Command::batch([ + self.close_view(plugin_id), + focus(self.search_field_id.clone()), + ]) + } + Some(PluginViewData { top_level_view: false, plugin_id, entrypoint_id, .. }) => { + self.open_view(plugin_id.clone(), entrypoint_id.clone()) + } } } } diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index 0ae4008..5569899 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -7,7 +7,7 @@ use iced::widget::row; use iced::widget::text; use common::model::SearchResult; - +use crate::ui::ScrollHandle; use crate::ui::theme::{Element, GauntletTheme, ThemableWidget}; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; @@ -23,10 +23,10 @@ pub struct SearchList { pub fn search_list( search_results: Vec, - focused_search_result: usize, + focused_search_result: &ScrollHandle, on_select: impl Fn(SearchResult) -> Message + 'static, ) -> SearchList { - SearchList::new(search_results, focused_search_result, on_select) + SearchList::new(search_results, focused_search_result.index, on_select) } #[derive(Debug, Clone)] diff --git a/rust/client/src/ui/theme/button.rs b/rust/client/src/ui/theme/button.rs index 1b13b0a..e31cbee 100644 --- a/rust/client/src/ui/theme/button.rs +++ b/rust/client/src/ui/theme/button.rs @@ -12,6 +12,7 @@ pub enum ButtonStyle { DatePicker, Action, + ActionFocused, GridItem, ListItem, MainListItem, @@ -47,7 +48,7 @@ impl ButtonStyle { theme.padding.to_iced() } - ButtonStyle::Action => { + ButtonStyle::Action | ButtonStyle::ActionFocused => { let theme = &theme.action; theme.padding.to_iced() @@ -96,6 +97,10 @@ impl ButtonStyle { let theme = &theme.action; (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } + ButtonStyle::ActionFocused => { + let theme = &theme.action; + (Some(&theme.background_color_hovered), Some(&theme.background_color_hovered), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + } ButtonStyle::ListItem => { let theme = &theme.list_item; (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index f187773..4329ec5 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -21,6 +21,7 @@ use itertools::Itertools; use crate::model::UiViewEvent; use crate::ui::custom_widgets::loading_bar::LoadingBar; +use crate::ui::ScrollHandle; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; use crate::ui::theme::date_picker::DatePickerStyle; @@ -61,16 +62,20 @@ pub enum ComponentWidgetState { state_value: Option }, Detail { - show_action_panel: bool + show_action_panel: bool, + focused_action_item: ScrollHandle, }, Form { - show_action_panel: bool + show_action_panel: bool, + focused_action_item: ScrollHandle, }, List { - show_action_panel: bool + show_action_panel: bool, + focused_action_item: ScrollHandle, }, Grid { - show_action_panel: bool + show_action_panel: bool, + focused_action_item: ScrollHandle, }, None } @@ -105,15 +110,19 @@ impl ComponentWidgetState { }, ComponentWidget::Detail { .. } => ComponentWidgetState::Detail { show_action_panel: false, + focused_action_item: ScrollHandle::new(scrollable::Id::unique()), }, ComponentWidget::Form { .. } => ComponentWidgetState::Form { show_action_panel: false, + focused_action_item: ScrollHandle::new(scrollable::Id::unique()), }, ComponentWidget::List { .. } => ComponentWidgetState::List { show_action_panel: false, + focused_action_item: ScrollHandle::new(scrollable::Id::unique()), }, ComponentWidget::Grid { .. } => ComponentWidgetState::Grid { show_action_panel: false, + focused_action_item: ScrollHandle::new(scrollable::Id::unique()), }, _ => ComponentWidgetState::None } @@ -196,16 +205,16 @@ impl ComponentWidgetWrapper { let (_, ref mut state) = &mut *self.get_mut(); match state { - ComponentWidgetState::Detail { show_action_panel } => { + ComponentWidgetState::Detail { show_action_panel, .. } => { *show_action_panel = !*show_action_panel; }, - ComponentWidgetState::Form { show_action_panel } => { + ComponentWidgetState::Form { show_action_panel, .. } => { *show_action_panel = !*show_action_panel; }, - ComponentWidgetState::List { show_action_panel } => { + ComponentWidgetState::List { show_action_panel, .. } => { *show_action_panel = !*show_action_panel; }, - ComponentWidgetState::Grid { show_action_panel } => { + ComponentWidgetState::Grid { show_action_panel, .. } => { *show_action_panel = !*show_action_panel; }, _ => {} @@ -410,7 +419,7 @@ impl ComponentWidgetWrapper { } } ComponentWidget::Detail { children, isLoading: is_loading } => { - let ComponentWidgetState::Detail { show_action_panel } = *state else { + let ComponentWidgetState::Detail { show_action_panel, ref focused_action_item } = *state else { panic!("unexpected state kind {:?}", state) }; @@ -480,7 +489,7 @@ impl ComponentWidgetWrapper { if is_in_list { content } else { - render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) + render_plugin_root(show_action_panel, &focused_action_item, widget_id, children, content, context, is_loading.unwrap_or(false)) } } ComponentWidget::Root { children } => { @@ -591,7 +600,7 @@ impl ComponentWidgetWrapper { .into() } ComponentWidget::Form { children, isLoading: is_loading } => { - let ComponentWidgetState::Form { show_action_panel } = *state else { + let ComponentWidgetState::Form { show_action_panel, ref focused_action_item } = *state else { panic!("unexpected state kind {:?}", state) }; @@ -667,7 +676,7 @@ impl ComponentWidgetWrapper { .width(Length::Fill) .themed(ContainerStyle::Form); - render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) + render_plugin_root(show_action_panel, &focused_action_item, widget_id, children, content, context, is_loading.unwrap_or(false)) } ComponentWidget::InlineSeparator { icon } => { match icon { @@ -944,7 +953,7 @@ impl ComponentWidgetWrapper { render_section(content, Some(title), subtitle, RowStyle::ListSectionTitle, TextStyle::ListSectionTitle, TextStyle::ListSectionSubtitle) } ComponentWidget::List { children, isLoading: is_loading } => { - let ComponentWidgetState::List { show_action_panel } = *state else { + let ComponentWidgetState::List { show_action_panel, ref focused_action_item } = *state else { panic!("unexpected state kind {:?}", state) }; @@ -1029,7 +1038,7 @@ impl ComponentWidgetWrapper { .height(Length::Fill) .into(); - render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) + render_plugin_root(show_action_panel, &focused_action_item, widget_id, children, content, context, is_loading.unwrap_or(false)) } ComponentWidget::GridItem { children, title, subtitle } => { // TODO should be just one @@ -1092,7 +1101,7 @@ impl ComponentWidgetWrapper { render_section(content, Some(title), subtitle, RowStyle::GridSectionTitle, TextStyle::GridSectionTitle, TextStyle::GridSectionSubtitle) } ComponentWidget::Grid { children, columns, isLoading: is_loading } => { - let ComponentWidgetState::Grid { show_action_panel } = *state else { + let ComponentWidgetState::Grid { show_action_panel, ref focused_action_item } = *state else { panic!("unexpected state kind {:?}", state) }; @@ -1143,7 +1152,7 @@ impl ComponentWidgetWrapper { .width(Length::Fill) .themed(ContainerStyle::Grid); - render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) + render_plugin_root(show_action_panel, &focused_action_item, widget_id, children, content, context, is_loading.unwrap_or(false)) } } } @@ -1277,6 +1286,7 @@ fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option< fn render_plugin_root<'a>( show_action_panel: bool, + action_panel_scroll_handle: &ScrollHandle, widget_id: UiWidgetId, children: &[ComponentWidgetWrapper], content: Element<'a, ComponentWidgetEvent>, @@ -1306,6 +1316,7 @@ fn render_plugin_root<'a>( content, None, action_panel, + action_panel_scroll_handle, entrypoint_name, || ComponentWidgetEvent::ToggleActionPanel { widget_id }, |widget_id| ComponentWidgetEvent::ActionClick { widget_id } @@ -1392,7 +1403,12 @@ fn convert_action_panel(children: &[ComponentWidgetWrapper], action_shortcuts: H } } -fn render_action_panel_items<'a, T: 'a + Clone>(title: Option, items: Vec, on_action: &dyn Fn(UiWidgetId) -> T) -> Vec> { +fn render_action_panel_items<'a, T: 'a + Clone>( + title: Option, + items: Vec, + action_panel_scroll_handle: &ScrollHandle, + on_action: &dyn Fn(UiWidgetId) -> T +) -> Vec> { let mut columns = vec![]; if let Some(title) = title { @@ -1441,10 +1457,17 @@ fn render_action_panel_items<'a, T: 'a + Clone>(title: Option, items: Ve .into() }; + // TODO broken for plugin views + let style = if action_panel_scroll_handle.index == widget_id { + ButtonStyle::ActionFocused + } else { + ButtonStyle::Action + }; + let content = button(content) .on_press(on_action(widget_id)) .width(Length::Fill) - .themed(ButtonStyle::Action); + .themed(style); columns.push(content); } @@ -1454,7 +1477,7 @@ fn render_action_panel_items<'a, T: 'a + Clone>(title: Option, items: Ve columns.push(separator); - let content = render_action_panel_items(title, items, on_action); + let content = render_action_panel_items(title, items, action_panel_scroll_handle, on_action); for content in content { columns.push(content); @@ -1468,13 +1491,18 @@ fn render_action_panel_items<'a, T: 'a + Clone>(title: Option, items: Ve columns } -fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T>(action_panel: ActionPanel, on_action: F) -> Element<'a, T> { - let columns = render_action_panel_items(action_panel.title, action_panel.items, &on_action); +fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T>( + action_panel: ActionPanel, + on_action: F, + action_panel_scroll_handle: &ScrollHandle, +) -> Element<'a, T> { + let columns = render_action_panel_items(action_panel.title, action_panel.items, action_panel_scroll_handle, &on_action); let actions: Element<_> = column(columns) .into(); let actions: Element<_> = scrollable(actions) + .id(action_panel_scroll_handle.scrollable_id.clone()) .width(Length::Fill) .into(); @@ -1489,6 +1517,7 @@ pub fn render_root<'a, T: 'a + Clone>( content: Element<'a, T>, default_action: Option<(String, PhysicalShortcut)>, action_panel: Option, + action_panel_scroll_handle: &ScrollHandle, entrypoint_name: String, on_panel_toggle: impl Fn() -> T, on_action: impl Fn(UiWidgetId) -> T, @@ -1610,7 +1639,7 @@ pub fn render_root<'a, T: 'a + Clone>( let action_panel_element = match action_panel { None => Space::with_height(1).into(), - Some(action_panel) => render_action_panel(action_panel, on_action) + Some(action_panel) => render_action_panel(action_panel, on_action, action_panel_scroll_handle) }; floating_element(content, action_panel_element) @@ -1902,10 +1931,10 @@ impl ComponentWidgetEvent { let (widget, ref mut state) = &mut *widget.get_mut(); let show_action_panel = match state { - ComponentWidgetState::Detail { show_action_panel } => show_action_panel, - ComponentWidgetState::Form { show_action_panel } => show_action_panel, - ComponentWidgetState::List { show_action_panel } => show_action_panel, - ComponentWidgetState::Grid { show_action_panel } => show_action_panel, + ComponentWidgetState::Detail { show_action_panel, .. } => show_action_panel, + ComponentWidgetState::Form { show_action_panel, .. } => show_action_panel, + ComponentWidgetState::List { show_action_panel, .. } => show_action_panel, + ComponentWidgetState::Grid { show_action_panel, .. } => show_action_panel, _ => panic!("unexpected state kind, widget: {:?} state: {:?}", widget, state) }; From 8793d678f6b43aa1f99d34a91bcdea0276f50173 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:43:30 +0200 Subject: [PATCH 091/540] Refactor focus management --- rust/client/src/ui/mod.rs | 1014 ++++++++++++------------- rust/client/src/ui/scroll_handle.rs | 71 ++ rust/client/src/ui/search_list.rs | 24 +- rust/client/src/ui/state/main_view.rs | 87 +++ rust/client/src/ui/state/mod.rs | 236 ++++++ rust/client/src/ui/widget.rs | 62 +- 6 files changed, 907 insertions(+), 587 deletions(-) create mode 100644 rust/client/src/ui/scroll_handle.rs create mode 100644 rust/client/src/ui/state/main_view.rs create mode 100644 rust/client/src/ui/state/mod.rs diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 6650d15..7300d44 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -4,7 +4,7 @@ use std::path::Path; use std::rc::Rc; use std::sync::{Arc, RwLock as StdRwLock}; use anyhow::anyhow; -use iced::{Alignment, Command, Event, event, executor, font, Font, futures, keyboard, Length, Padding, Pixels, Settings, Size, Subscription, subscription, window}; +use iced::{event, executor, font, futures, keyboard, subscription, window, Alignment, Command, Event, Font, Length, Padding, Pixels, Settings, Size, Subscription}; use iced::advanced::graphics::core::SmolStr; use iced::advanced::layout::Limits; use iced::multi_window::Application; @@ -12,8 +12,8 @@ use iced::futures::channel::mpsc::Sender; use iced::futures::SinkExt; use iced::keyboard::Key; use iced::keyboard::key::Named; -use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, Space, text, text_input}; -use iced::widget::scrollable::{AbsoluteOffset, scroll_to}; +use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, Space}; +use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; use iced::widget::text_input::focus; use iced::window::{Level, Position, Screenshot}; use iced::window::settings::PlatformSpecific; @@ -24,7 +24,7 @@ use tokio::sync::RwLock as TokioRwLock; use tonic::transport::Server; use client_context::ClientContext; -use common::model::{BackendRequestData, BackendResponseData, EntrypointId, PhysicalKey, PhysicalShortcut, PluginId, SearchResult, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; +use common::model::{BackendRequestData, BackendResponseData, EntrypointId, PhysicalKey, PhysicalShortcut, PluginId, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; use common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFrontendApiError}; use common::scenario_convert::{ui_render_location_from_scenario, ui_widget_from_scenario}; use common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; @@ -50,121 +50,28 @@ mod inline_view_container; #[cfg(any(target_os = "macos", target_os = "windows"))] mod sys_tray; mod custom_widgets; +mod scroll_handle; +mod state; pub use theme::GauntletTheme; +use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::state::{ErrorViewData, Focus, GlobalState, MainViewState, PluginViewData}; pub struct AppModel { // logic backend_api: BackendForFrontendApi, frontend_receiver: Arc>>, - search_field_id: text_input::Id, focused: bool, theme: GauntletTheme, wayland: bool, #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: tray_icon::TrayIcon, - // ephemeral state - prompt: String, - focused_search_result: ScrollHandle, - focused_action_item: ScrollHandle, - // state client_context: Arc>, - plugin_view_data: Option, - error_view: Option, - search_results: Vec, - show_action_panel: bool, + global_state: GlobalState, } -#[derive(Clone, Debug)] -struct ScrollHandle { - scrollable_id: scrollable::Id, - index: usize, - offset: usize, -} - -impl ScrollHandle { - fn new(scrollable_id: scrollable::Id) -> ScrollHandle { - ScrollHandle { - scrollable_id, - index: 0, - offset: 0, - } - } - - fn reset(&mut self) { - self.index = 0; - self.offset = 0; - } - - fn get<'a, T>(&self, search_results: &'a Vec) -> Option<&'a T> { - search_results.get(self.index) - } - - fn focus_next(&mut self, item_amount: usize) -> Command { - self.offset = if self.offset < 8 { - self.offset + 1 - } else { - 8 - }; - - if self.index < item_amount - 1 { - self.index = self.index + 1; - - let pos_y = self.index as f32 * ESTIMATED_ITEM_SIZE - (self.offset as f32 * ESTIMATED_ITEM_SIZE); - - scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) - } else { - Command::none() - } - } - - fn focus_previous(&mut self) -> Command { - self.offset = if self.offset > 1 { - self.offset - 1 - } else { - 1 - }; - - if self.index > 0 { - self.index = self.index - 1; - - let pos_y = self.index as f32 * ESTIMATED_ITEM_SIZE - (self.offset as f32 * ESTIMATED_ITEM_SIZE); - - scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) - } else { - Command::none() - } - } -} - -struct PluginViewData { - top_level_view: bool, - plugin_id: PluginId, - plugin_name: String, - entrypoint_id: EntrypointId, - entrypoint_name: String, - action_shortcuts: HashMap, - waiting_for_first_render: bool, -} - -enum ErrorViewData { - PreferenceRequired { - plugin_id: PluginId, - entrypoint_id: EntrypointId, - plugin_preferences_required: bool, - entrypoint_preferences_required: bool, - }, - PluginError { - plugin_id: PluginId, - entrypoint_id: EntrypointId, - }, - BackendTimeout, - UnknownError { - display: String - } -} #[derive(Debug, Clone)] pub enum AppMsg { @@ -212,7 +119,7 @@ pub enum AppMsg { plugin_id: PluginId, entrypoint_id: Option, }, - SaveActionShortcuts { + OnOpenView { action_shortcuts: HashMap }, ShowPluginErrorView { @@ -221,7 +128,6 @@ pub enum AppMsg { render_location: UiRenderLocation }, RunSearchItemAction(SearchResult, Option), - RequestSearchResultUpdate, Screenshot { save_path: String }, @@ -231,7 +137,9 @@ pub enum AppMsg { }, Close, ResetWindowState, - HandleBackendError(BackendForFrontendApiError), + ShowBackendError(BackendForFrontendApiError), + ClosePluginView(PluginId), + OpenPluginView(PluginId, EntrypointId), } pub struct AppFlags { @@ -352,7 +260,7 @@ impl Application for AppModel { ) } - let (client_context, plugin_view_data, error_view) = if cfg!(feature = "scenario_runner") { + let (client_context, global_state) = if cfg!(feature = "scenario_runner") { let gen_in = std::env::var("GAUNTLET_SCREENSHOT_GEN_IN") .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_IN"); @@ -394,42 +302,41 @@ impl Application for AppModel { commands.push(Command::perform(async move { top_level_view }, |top_level_view| AppMsg::ReplaceView { top_level_view })); - let plugin_view_data= match render_location { - ScenarioUiRenderLocation::InlineView => None, - ScenarioUiRenderLocation::View => Some(PluginViewData { + let state= match render_location { + ScenarioUiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique()), + ScenarioUiRenderLocation::View => GlobalState::new_plugin(PluginViewData { top_level_view, plugin_id, plugin_name: "Screenshot Gen".to_string(), entrypoint_id, entrypoint_name: gen_name, action_shortcuts: Default::default(), - waiting_for_first_render: false, }) }; - (context, plugin_view_data, None) + (context, state) } ScenarioFrontendEvent::ShowPreferenceRequiredView { entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { - let error_view = Some(ErrorViewData::PreferenceRequired { + let error_view = ErrorViewData::PreferenceRequired { plugin_id: PluginId::from_string("__SCREENSHOT_GEN___"), entrypoint_id: EntrypointId::from_string(entrypoint_id), plugin_preferences_required, entrypoint_preferences_required, - }); + }; - (ClientContext::new(), None, error_view) + (ClientContext::new(), GlobalState::new_error(error_view)) } ScenarioFrontendEvent::ShowPluginErrorView { entrypoint_id, render_location: _ } => { - let error_view = Some(ErrorViewData::PluginError { + let error_view = ErrorViewData::PluginError { plugin_id: PluginId::from_string("__SCREENSHOT_GEN___"), entrypoint_id: EntrypointId::from_string(entrypoint_id), - }); + }; - (ClientContext::new(), None, error_view) + (ClientContext::new(), GlobalState::new_error(error_view)) } } } else { - (ClientContext::new(), None, None) + (ClientContext::new(), GlobalState::new(text_input::Id::unique())) }; ( @@ -437,24 +344,15 @@ impl Application for AppModel { // logic backend_api, frontend_receiver: Arc::new(TokioRwLock::new(frontend_receiver)), - search_field_id: text_input::Id::unique(), focused: false, theme: GauntletTheme::new(), wayland, #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: sys_tray::create_tray(), - // ephemeral state - prompt: "".to_string(), - focused_search_result: ScrollHandle::new(scrollable::Id::unique()), - focused_action_item: ScrollHandle::new(scrollable::Id::unique()), - // state + global_state, client_context: Arc::new(StdRwLock::new(client_context)), - plugin_view_data, - error_view, - search_results: vec![], - show_action_panel: false, }, Command::batch(commands), ) @@ -467,17 +365,26 @@ impl Application for AppModel { fn update(&mut self, message: Self::Message) -> Command { match message { AppMsg::OpenView { plugin_id, plugin_name, entrypoint_id, entrypoint_name } => { - self.plugin_view_data.replace(PluginViewData { - top_level_view: true, - plugin_id: plugin_id.clone(), - plugin_name, - entrypoint_id: entrypoint_id.clone(), - entrypoint_name, - action_shortcuts: HashMap::new(), - waiting_for_first_render: true, - }); + match &mut self.global_state { + GlobalState::MainView { pending_plugin_view_data, .. } => { + *pending_plugin_view_data = Some(PluginViewData { + top_level_view: true, + plugin_id: plugin_id.clone(), + plugin_name, + entrypoint_id: entrypoint_id.clone(), + entrypoint_name, + action_shortcuts: HashMap::new(), + }); - self.open_view(plugin_id, entrypoint_id) + self.open_plugin_view(plugin_id, entrypoint_id) + } + GlobalState::ErrorView { .. } => { + Command::none() + } + GlobalState::PluginView { .. } => { + Command::none() + } + } } AppMsg::RunCommand { plugin_id, entrypoint_id } => { Command::batch([ @@ -495,12 +402,19 @@ impl Application for AppModel { if cfg!(feature = "scenario_runner") { Command::none() } else { - new_prompt.truncate(100); // search query uses regex so just to be safe truncate the prompt + match &mut self.global_state { + GlobalState::MainView { focused_search_result, sub_state, prompt, ..} => { + new_prompt.truncate(100); // search query uses regex so just to be safe truncate the prompt - self.show_action_panel = false; - self.prompt = new_prompt.clone(); - self.focused_search_result.reset(); - self.focused_action_item.reset(); + *prompt = new_prompt.clone(); + + focused_search_result.reset(); + + MainViewState::initial(sub_state); + } + GlobalState::ErrorView { .. } => {} + GlobalState::PluginView { .. } => {} + } let mut backend_api = self.backend_api.clone(); @@ -513,42 +427,52 @@ impl Application for AppModel { } } AppMsg::UpdateSearchResults => { - let prompt = self.prompt.clone(); + match &mut self.global_state { + GlobalState::MainView { prompt, .. } => { + let prompt = prompt.clone(); - let mut backend_api = self.backend_api.clone(); + let mut backend_api = self.backend_api.clone(); - Command::perform(async move { - let search_results = backend_api.search(prompt, false) - .await?; + Command::perform(async move { + let search_results = backend_api.search(prompt, false) + .await?; + + Ok(search_results) + }, |result| handle_backend_error(result, |search_results| AppMsg::SetSearchResults(search_results))) - Ok(search_results) - }, |result| handle_backend_error(result, |search_results| AppMsg::SetSearchResults(search_results))) - } - AppMsg::PromptSubmit => { - if self.show_action_panel { - self.show_action_panel = false; - let widget_id = self.focused_action_item.index; - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) - } else { - self.show_action_panel = false; - if let Some(search_item) = self.focused_search_result.get(&self.search_results) { - let search_item = search_item.clone(); - Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) - } else { - Command::none() } + _ => Command::none() } } - AppMsg::SetSearchResults(search_results) => { - self.search_results = search_results; + AppMsg::PromptSubmit => self.global_state.enter(), + AppMsg::SetSearchResults(new_search_results) => { + match &mut self.global_state { + GlobalState::MainView { search_results, .. } => { + *search_results = new_search_results; + } + GlobalState::ErrorView { .. } => {} + GlobalState::PluginView { .. } => {} + } + Command::none() } AppMsg::ReplaceView { top_level_view } => { - match &mut self.plugin_view_data { - None => Command::none(), - Some(view_data) => { - view_data.top_level_view = top_level_view; - view_data.waiting_for_first_render = false; + match &mut self.global_state { + GlobalState::MainView { pending_plugin_view_data, .. } => { + match pending_plugin_view_data { + None => Command::none(), + Some(pending_plugin_view_data) => { + let pending_plugin_view_data = pending_plugin_view_data.clone(); + GlobalState::plugin(&mut self.global_state, PluginViewData { + top_level_view, + ..pending_plugin_view_data + }) + } + } + } + GlobalState::ErrorView { .. } => Command::none(), + GlobalState::PluginView(plugin_view_data) => { + plugin_view_data.top_level_view = top_level_view; Command::none() } @@ -561,62 +485,64 @@ impl Application for AppModel { keyboard::Event::KeyPressed { key, modifiers, physical_key, text, .. } => { tracing::debug!("Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", key, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); match key { - Key::Named(Named::ArrowUp) => self.focus_previous(), - Key::Named(Named::ArrowDown) => self.focus_next(), - Key::Named(Named::Escape) => self.previous_view(), + Key::Named(Named::ArrowUp) => self.global_state.arrow_up(), + Key::Named(Named::ArrowDown) => self.global_state.arrow_down(), + Key::Named(Named::Escape) => self.global_state.escape(), Key::Named(Named::Enter) => { // fired in cases where main text field is not focused - Command ::perform(async {}, |_| AppMsg::PromptSubmit) - }, - Key::Named(Named::Backspace) => { - self.backspace_prompt(); - focus(self.search_field_id.clone()) + Command::perform(async {}, |_| AppMsg::PromptSubmit) }, + Key::Named(Named::Backspace) => self.backspace_prompt(), _ => { - if self.plugin_view_data.is_none() { - match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - Command::perform(async {}, |_| AppMsg::ToggleActionPanel) - } - _ => { - match text { - Some(text) => { - self.append_prompt(text.to_string()); - focus(self.search_field_id.clone()) - } - None => { - Command::none() - } + match self.global_state { + GlobalState::MainView { .. } => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { + let client_context = self.client_context.read().expect("lock is poisoned"); + + client_context.show_action_panel(); + + Command::none() + } + Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { + let (plugin_id, entrypoint_id) = { + let client_context = self.client_context.read().expect("lock is poisoned"); + (client_context.get_view_plugin_id(), client_context.get_view_entrypoint_id()) + }; + + Command::perform( + async move { + backend_client.send_keyboard_event(plugin_id, entrypoint_id, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + .await?; + + Ok(()) + }, + |result| handle_backend_error(result, |()| AppMsg::Noop), + ) + } + None => { + Command::none() } } } - } else { - match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - let client_context = self.client_context.read().expect("lock is poisoned"); - - client_context.show_action_panel(); - - Command::none() - } - Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - let (plugin_id, entrypoint_id) = { - let client_context = self.client_context.read().expect("lock is poisoned"); - (client_context.get_view_plugin_id(), client_context.get_view_entrypoint_id()) - }; - - Command::perform( - async move { - backend_client.send_keyboard_event(plugin_id, entrypoint_id, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) - .await?; - - Ok(()) - }, - |result| handle_backend_error(result, |()| AppMsg::Noop), - ) - } - None => { - Command::none() + GlobalState::ErrorView { .. } => { + Command::none() + } + GlobalState::PluginView { .. } => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { + Command::perform(async {}, |_| AppMsg::ToggleActionPanel) + } + _ => { + match text { + Some(text) => { + self.append_prompt(text.to_string()) + } + None => { + Command::none() + } + } + } } } } @@ -648,7 +574,7 @@ impl Application for AppModel { self.hide_window() } AppMsg::IcedEvent(_) => Command::none(), - AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => self.previous_view(), + AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => self.global_state.escape(), AppMsg::WidgetEvent { widget_event, plugin_id, render_location } => { let mut backend_client = self.backend_api.clone(); let client_context = self.client_context.clone(); @@ -689,20 +615,32 @@ impl Application for AppModel { plugin_preferences_required, entrypoint_preferences_required } => { - self.error_view = Some(ErrorViewData::PreferenceRequired { - plugin_id, - entrypoint_id, - plugin_preferences_required, - entrypoint_preferences_required, - }); - Command::none() + GlobalState::error( + &mut self.global_state, + ErrorViewData::PreferenceRequired { + plugin_id, + entrypoint_id, + plugin_preferences_required, + entrypoint_preferences_required, + }, + ) } - AppMsg::ShowPluginErrorView { plugin_id, entrypoint_id, render_location } => { - self.error_view = Some(ErrorViewData::PluginError { - plugin_id, - entrypoint_id, - }); - Command::none() + AppMsg::ShowPluginErrorView { plugin_id, entrypoint_id, .. } => { + GlobalState::error( + &mut self.global_state, + ErrorViewData::PluginError { + plugin_id, + entrypoint_id, + }, + ) + } + AppMsg::ShowBackendError(err) => { + GlobalState::error( + &mut self.global_state, + match err { + BackendForFrontendApiError::TimeoutError => ErrorViewData::BackendTimeout, + } + ) } AppMsg::OpenSettingsPreferences { plugin_id, entrypoint_id, } => { let mut backend_api = self.backend_api.clone(); @@ -714,10 +652,22 @@ impl Application for AppModel { Ok(()) }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } - AppMsg::SaveActionShortcuts { action_shortcuts } => { - if let Some(data) = self.plugin_view_data.as_mut() { - data.action_shortcuts = action_shortcuts; + AppMsg::OnOpenView { action_shortcuts } => { + match &mut self.global_state { + GlobalState::MainView { pending_plugin_view_data, .. } => { + match pending_plugin_view_data { + None => {} + Some(pending_plugin_view_data) => { + pending_plugin_view_data.action_shortcuts = action_shortcuts; + } + }; + } + GlobalState::ErrorView { .. } => { }, + GlobalState::PluginView(plugin_view_data) => { + plugin_view_data.action_shortcuts = action_shortcuts; + } } + Command::none() } AppMsg::RunSearchItemAction(search_result, action_index) => { @@ -741,9 +691,6 @@ impl Application for AppModel { Command::perform(async {}, |_| event) } - AppMsg::RequestSearchResultUpdate => { - Command::perform(async {}, move |_| AppMsg::UpdateSearchResults) - } AppMsg::Screenshot { save_path } => { println!("Creating screenshot at: {}", save_path); @@ -798,257 +745,279 @@ impl Application for AppModel { #[cfg(not(target_os = "linux"))] window::close(window::Id::MAIN) } - AppMsg::HandleBackendError(err) => { - self.error_view = Some(match err { - BackendForFrontendApiError::TimeoutError => ErrorViewData::BackendTimeout, - }); - - Command::none() - } AppMsg::ToggleActionPanel => { - self.show_action_panel = !self.show_action_panel; + match &mut self.global_state { + GlobalState::MainView { sub_state, search_results, focused_search_result, .. } => { + match sub_state { + MainViewState::None => { + if let Some(search_item) = focused_search_result.get(&search_results) { + MainViewState::action_panel(sub_state, &search_item.entrypoint_actions); + } + } + MainViewState::ActionPanel { .. } => { + MainViewState::initial(sub_state); + } + } + } + GlobalState::ErrorView { .. } => { }, + GlobalState::PluginView { .. } => { + // todo + // self.show_action_panel = !self.show_action_panel; + } + } Command::none() } AppMsg::OnEntrypointAction(widget_id) => { - if let Some(search_item) = self.focused_search_result.get(&self.search_results) { - let search_item = search_item.clone(); - if widget_id == 0 { - Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) - } else { - Command::perform(async {}, move |_| AppMsg::RunSearchItemAction(search_item, Some(widget_id - 1))) + match &self.global_state { + GlobalState::MainView { focused_search_result, search_results, .. } => { + if let Some(search_item) = focused_search_result.get(&search_results) { + let search_item = search_item.clone(); + if widget_id == 0 { + Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) + } else { + Command::perform(async {}, move |_| AppMsg::RunSearchItemAction(search_item, Some(widget_id - 1))) + } + } else { + Command::none() + } + } + GlobalState::ErrorView { .. } => Command::none(), + GlobalState::PluginView { .. } => { + todo!() } - } else { - Command::none() } } + AppMsg::OpenPluginView(plugin_id, entrypoint_id) => { + self.open_plugin_view(plugin_id, entrypoint_id) + } + AppMsg::ClosePluginView(plugin_id) => { + self.close_plugin_view(plugin_id) + } } } fn view(&self, _window: window::Id) -> Element<'_, Self::Message> { - if let Some(view_data) = &self.error_view { - return match view_data { - ErrorViewData::PreferenceRequired { plugin_id, entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { + match &self.global_state { + GlobalState::ErrorView { error_view } => { + match error_view { + ErrorViewData::PreferenceRequired { plugin_id, entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { - let (description_text, msg) = match (plugin_preferences_required, entrypoint_preferences_required) { - (true, true) => { - // TODO do not show "entrypoint" name to user - let description_text = "Before using, plugin and entrypoint preferences need to be specified"; - // note: - // we open plugin view and not entrypoint even though both need to be specified - let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: None }; - (description_text, msg) - } - (false, true) => { - // TODO do not show "entrypoint" name to user - let description_text = "Before using, entrypoint preferences need to be specified"; - let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: Some(entrypoint_id.clone()) }; - (description_text, msg) - } - (true, false) => { - let description_text = "Before using, plugin preferences need to be specified"; - let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: None }; - (description_text, msg) - } - (false, false) => unreachable!() - }; + let (description_text, msg) = match (plugin_preferences_required, entrypoint_preferences_required) { + (true, true) => { + // TODO do not show "entrypoint" name to user + let description_text = "Before using, plugin and entrypoint preferences need to be specified"; + // note: + // we open plugin view and not entrypoint even though both need to be specified + let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: None }; + (description_text, msg) + } + (false, true) => { + // TODO do not show "entrypoint" name to user + let description_text = "Before using, entrypoint preferences need to be specified"; + let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: Some(entrypoint_id.clone()) }; + (description_text, msg) + } + (true, false) => { + let description_text = "Before using, plugin preferences need to be specified"; + let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: None }; + (description_text, msg) + } + (false, false) => unreachable!() + }; - let description: Element<_> = text(description_text) - .into(); + let description: Element<_> = text(description_text) + .into(); - let description = container(description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PreferenceRequiredViewDescription); + let description = container(description) + .width(Length::Fill) + .center_x() + .themed(ContainerStyle::PreferenceRequiredViewDescription); - let button_label: Element<_> = text("Open Settings") - .into(); + let button_label: Element<_> = text("Open Settings") + .into(); - let button: Element<_> = button(button_label) - .on_press(msg) - .into(); + let button: Element<_> = button(button_label) + .on_press(msg) + .into(); - let button = container(button) - .width(Length::Fill) - .center_x() - .into(); + let button = container(button) + .width(Length::Fill) + .center_x() + .into(); - let content: Element<_> = column([ - description, - button - ]).into(); + let content: Element<_> = column([ + description, + button + ]).into(); - let content: Element<_> = container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .themed(ContainerStyle::Main); + let content: Element<_> = container(content) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill) + .themed(ContainerStyle::Main); - content - } - ErrorViewData::PluginError { plugin_id, entrypoint_id } => { - let description: Element<_> = text("Error occurred in plugin when trying to show the view") - .into(); + content + } + ErrorViewData::PluginError { .. } => { + let description: Element<_> = text("Error occurred in plugin when trying to show the view") + .into(); - let description = container(description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PluginErrorViewTitle); + let description = container(description) + .width(Length::Fill) + .center_x() + .themed(ContainerStyle::PluginErrorViewTitle); - let sub_description: Element<_> = text("Please report this to plugin author") - .into(); + let sub_description: Element<_> = text("Please report this to plugin author") + .into(); - let sub_description = container(sub_description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PluginErrorViewDescription); + let sub_description = container(sub_description) + .width(Length::Fill) + .center_x() + .themed(ContainerStyle::PluginErrorViewDescription); - let button_label: Element<_> = text("Close") - .into(); + let button_label: Element<_> = text("Close") + .into(); - let button: Element<_> = button(button_label) - .on_press(AppMsg::HideWindow) - .into(); + let button: Element<_> = button(button_label) + .on_press(AppMsg::HideWindow) + .into(); - let button = container(button) - .width(Length::Fill) - .center_x() - .into(); + let button = container(button) + .width(Length::Fill) + .center_x() + .into(); - let content: Element<_> = column([ - description, - sub_description, - button - ]).into(); + let content: Element<_> = column([ + description, + sub_description, + button + ]).into(); - let content: Element<_> = container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .themed(ContainerStyle::Main); + let content: Element<_> = container(content) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill) + .themed(ContainerStyle::Main); - content - } - ErrorViewData::UnknownError { display } => { - let description: Element<_> = text("Unknown error occurred") - .into(); + content + } + ErrorViewData::UnknownError { display } => { + let description: Element<_> = text("Unknown error occurred") + .into(); - let description = container(description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PluginErrorViewTitle); + let description = container(description) + .width(Length::Fill) + .center_x() + .themed(ContainerStyle::PluginErrorViewTitle); - let sub_description: Element<_> = text("Please report") // TODO link - .into(); + let sub_description: Element<_> = text("Please report") // TODO link + .into(); - let sub_description = container(sub_description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PluginErrorViewDescription); + let sub_description = container(sub_description) + .width(Length::Fill) + .center_x() + .themed(ContainerStyle::PluginErrorViewDescription); - let error_description: Element<_> = text(display) - .into(); + let error_description: Element<_> = text(display) + .into(); - let error_description = container(error_description) - .width(Length::Fill) - .themed(ContainerStyle::PluginErrorViewDescription); + let error_description = container(error_description) + .width(Length::Fill) + .themed(ContainerStyle::PluginErrorViewDescription); - let error_description = scrollable(error_description) - .width(Length::Fill) - .into(); + let error_description = scrollable(error_description) + .width(Length::Fill) + .into(); - let button_label: Element<_> = text("Close") - .into(); + let button_label: Element<_> = text("Close") + .into(); - let button: Element<_> = button(button_label) - .on_press(AppMsg::HideWindow) - .into(); + let button: Element<_> = button(button_label) + .on_press(AppMsg::HideWindow) + .into(); - let button = container(button) - .width(Length::Fill) - .center_x() - .into(); + let button = container(button) + .width(Length::Fill) + .center_x() + .into(); - let content: Element<_> = column([ - description, - sub_description, - error_description, - button - ]).into(); + let content: Element<_> = column([ + description, + sub_description, + error_description, + button + ]).into(); - let content: Element<_> = container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .themed(ContainerStyle::Main); + let content: Element<_> = container(content) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill) + .themed(ContainerStyle::Main); - content - } - ErrorViewData::BackendTimeout => { - let description: Element<_> = text("Error occurred") - .into(); + content + } + ErrorViewData::BackendTimeout => { + let description: Element<_> = text("Error occurred") + .into(); - let description = container(description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PluginErrorViewTitle); + let description = container(description) + .width(Length::Fill) + .center_x() + .themed(ContainerStyle::PluginErrorViewTitle); - let sub_description: Element<_> = text("Backend was unable to process message in a timely manner") - .into(); + let sub_description: Element<_> = text("Backend was unable to process message in a timely manner") + .into(); - let sub_description = container(sub_description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PluginErrorViewDescription); + let sub_description = container(sub_description) + .width(Length::Fill) + .center_x() + .themed(ContainerStyle::PluginErrorViewDescription); - let button_label: Element<_> = text("Close") - .into(); + let button_label: Element<_> = text("Close") + .into(); - let button: Element<_> = button(button_label) - .on_press(AppMsg::HideWindow) - .into(); + let button: Element<_> = button(button_label) + .on_press(AppMsg::HideWindow) + .into(); - let button = container(button) - .width(Length::Fill) - .center_x() - .into(); + let button = container(button) + .width(Length::Fill) + .center_x() + .into(); - let content: Element<_> = column([ - description, - sub_description, - button - ]).into(); + let content: Element<_> = column([ + description, + sub_description, + button + ]).into(); - let content: Element<_> = container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .themed(ContainerStyle::Main); + let content: Element<_> = container(content) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill) + .themed(ContainerStyle::Main); - content + content + } } } - } - - match &self.plugin_view_data { - None | Some(PluginViewData { waiting_for_first_render: true, .. }) => { - let input: Element<_> = text_input("Search...", &self.prompt) + GlobalState::MainView { focused_search_result, sub_state, search_results, prompt, search_field_id, .. } => { + let input: Element<_> = text_input("Search...", prompt) .on_input(AppMsg::PromptChanged) .on_submit(AppMsg::PromptSubmit) - .id(self.search_field_id.clone()) + .id(search_field_id.clone()) .width(Length::Fill) .themed(TextInputStyle::MainSearch); - let search_results = self.search_results.iter().cloned().collect(); - let search_list = search_list( search_results, - &self.focused_search_result, - |search_result| AppMsg::RunSearchItemAction(search_result, None) + &focused_search_result, + |search_result| AppMsg::RunSearchItemAction(search_result, None), ); let search_list = container(search_list) @@ -1056,7 +1025,7 @@ impl Application for AppModel { .themed(ContainerStyle::MainListInner); let list: Element<_> = scrollable(search_list) - .id(self.focused_search_result.scrollable_id.clone()) + .id(focused_search_result.scrollable_id.clone()) .width(Length::Fill) .into(); @@ -1077,7 +1046,7 @@ impl Application for AppModel { list, ]).into(); - let (default_action, action_panel) = if let Some(search_item) = self.focused_search_result.get(&self.search_results) { + let (default_action, action_panel) = if let Some(search_item) = focused_search_result.get(search_results) { let label = match search_item.entrypoint_type { SearchResultEntrypointType::Command => "Run Command", SearchResultEntrypointType::View => "Open View", @@ -1124,18 +1093,36 @@ impl Application for AppModel { (None, None) }; - let root = render_root( - self.show_action_panel, - input, - separator, - content, - default_action, - action_panel, - &self.focused_action_item, - "".to_string(), - || AppMsg::ToggleActionPanel, - AppMsg::OnEntrypointAction - ); + let root = match sub_state { + MainViewState::None => { + render_root( + false, + input, + separator, + content, + default_action, + action_panel, + None::<&ScrollHandle>, + "".to_string(), + || AppMsg::ToggleActionPanel, + AppMsg::OnEntrypointAction + ) + } + MainViewState::ActionPanel { focused_action_item, .. } => { + render_root( + true, + input, + separator, + content, + default_action, + action_panel, + Some(focused_action_item), + "".to_string(), + || AppMsg::ToggleActionPanel, + AppMsg::OnEntrypointAction + ) + } + }; let root: Element<_> = container(root) .width(Length::Fill) @@ -1144,7 +1131,7 @@ impl Application for AppModel { root } - Some(data) => { + GlobalState::PluginView(plugin_view_data) => { let PluginViewData { top_level_view: _, plugin_id, @@ -1152,8 +1139,7 @@ impl Application for AppModel { entrypoint_id, entrypoint_name, action_shortcuts, - waiting_for_first_render: _ - } = data; + } = plugin_view_data; let container_element: Element<_> = view_container( self.client_context.clone(), @@ -1217,8 +1203,6 @@ impl Application for AppModel { } } -const ESTIMATED_ITEM_SIZE: f32 = 38.8; - impl AppModel { fn on_focused(&mut self) -> Command { self.focused = true; @@ -1226,7 +1210,7 @@ impl AppModel { } fn on_unfocused(&mut self) -> Command { - // for some reason (on both macos and linux x11) duplicate Unfocused fires right before Focus event + // for some reason (on both macOS and linux x11) duplicate Unfocused fires right before Focus event if self.focused { self.focused = false; self.hide_window() @@ -1259,21 +1243,22 @@ impl AppModel { window::change_mode(window::Id::MAIN, window::Mode::Hidden) ); - if let Some(PluginViewData { plugin_id, .. }) = &self.plugin_view_data { - commands.push(self.close_view(plugin_id.clone())); + match &self.global_state { + GlobalState::PluginView(PluginViewData { plugin_id, .. }) => { + commands.push(self.close_plugin_view(plugin_id.clone())); + } + GlobalState::MainView { .. } => {} + GlobalState::ErrorView { .. } => {} } - self.prompt = "".to_string(); - self.plugin_view_data = None; - self.search_results = vec![]; - self.close_error_view(); + commands.push( + GlobalState::initial(&mut self.global_state) + ); Command::batch(commands) } fn show_window(&mut self) -> Command { - self.close_error_view(); - let mut commands = vec![]; #[cfg(target_os = "linux")] @@ -1305,15 +1290,8 @@ impl AppModel { } fn reset_window_state(&mut self) -> Command { - self.focused_action_item.reset(); - self.focused_search_result.reset(); - self.show_action_panel = false; - let mut commands = vec![ - scroll_to(self.focused_action_item.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: 0.0 }), - scroll_to(self.focused_search_result.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: 0.0 }), - Command::perform(async {}, |_| AppMsg::PromptChanged("".to_owned())), - focus(self.search_field_id.clone()) + GlobalState::initial(&mut self.global_state), ]; if !self.wayland { @@ -1325,73 +1303,7 @@ impl AppModel { Command::batch(commands) } - fn focus_next(&mut self) -> Command { - if self.show_action_panel { - if let Some(search_item) = self.focused_search_result.get(&self.search_results) { - if search_item.entrypoint_actions.len() != 0 { - self.focused_action_item.focus_next(search_item.entrypoint_actions.len() + 1) - } else { - self.show_action_panel = false; - Command::none() - } - } else { - self.show_action_panel = false; - Command::none() - } - } else { - self.focused_search_result.focus_next(self.search_results.len()) - } - } - - fn focus_previous(&mut self) -> Command { - if self.show_action_panel { - if let Some(search_item) = self.focused_search_result.get(&self.search_results) { - if search_item.entrypoint_actions.len() != 0 { - self.focused_action_item.focus_previous() - } else { - self.show_action_panel = false; - Command::none() - } - } else { - self.show_action_panel = false; - Command::none() - } - } else { - self.focused_search_result.focus_previous() - } - } - - fn close_error_view(&mut self) { - self.error_view = None; - } - - fn previous_view(&mut self) -> Command { - if self.show_action_panel { - self.show_action_panel = false; - Command::none() - } else { - match &self.plugin_view_data { - None => { - self.hide_window() - } - Some(PluginViewData { top_level_view: true, plugin_id, .. }) => { - let plugin_id = plugin_id.clone(); - - self.plugin_view_data.take(); - - Command::batch([ - self.close_view(plugin_id), - focus(self.search_field_id.clone()), - ]) - } - Some(PluginViewData { top_level_view: false, plugin_id, entrypoint_id, .. }) => { - self.open_view(plugin_id.clone(), entrypoint_id.clone()) - } - } - } - } - - fn open_view(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Command { + fn open_plugin_view(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Command { let mut backend_client = self.backend_api.clone(); Command::perform(async move { @@ -1399,10 +1311,10 @@ impl AppModel { .await?; Ok(result) - }, |result| handle_backend_error(result, |action_shortcuts| AppMsg::SaveActionShortcuts { action_shortcuts })) + }, |result| handle_backend_error(result, |action_shortcuts| AppMsg::OnOpenView { action_shortcuts })) } - fn close_view(&self, plugin_id: PluginId) -> Command { + fn close_plugin_view(&self, plugin_id: PluginId) -> Command { let mut backend_client = self.backend_api.clone(); Command::perform(async move { @@ -1435,21 +1347,37 @@ impl AppModel { }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } - fn append_prompt(&mut self, value: String) { - self.prompt = format!("{}{}", self.prompt, value); + fn append_prompt(&mut self, value: String) -> Command { + match &mut self.global_state { + GlobalState::MainView { search_field_id, prompt, .. } => { + *prompt = format!("{}{}", prompt, value); + + focus(search_field_id.clone()) + } + GlobalState::ErrorView { .. } => Command::none(), + GlobalState::PluginView(_) => Command::none(), + } } - fn backspace_prompt(&mut self) { - let mut chars = self.prompt.chars(); - chars.next_back(); - self.prompt = chars.as_str().to_owned(); + fn backspace_prompt(&mut self) -> Command { + match &mut self.global_state { + GlobalState::MainView { search_field_id, prompt, .. } => { + let mut chars = prompt.chars(); + chars.next_back(); + *prompt = chars.as_str().to_owned(); + + focus(search_field_id.clone()) + } + GlobalState::ErrorView { .. } => Command::none(), + GlobalState::PluginView(_) => Command::none(), + } } } fn handle_backend_error(result: Result, convert: impl FnOnce(T) -> AppMsg) -> AppMsg { match result { Ok(val) => convert(val), - Err(err) => AppMsg::HandleBackendError(err) + Err(err) => AppMsg::ShowBackendError(err) } } @@ -1529,7 +1457,7 @@ async fn request_loop( UiRequestData::RequestSearchResultUpdate => { responder.respond(UiResponseData::Nothing); - AppMsg::RequestSearchResultUpdate + AppMsg::UpdateSearchResults } } }; diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs new file mode 100644 index 0000000..d291061 --- /dev/null +++ b/rust/client/src/ui/scroll_handle.rs @@ -0,0 +1,71 @@ +use std::marker::PhantomData; +use iced::Command; +use iced::widget::scrollable; +use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; +use crate::ui::AppMsg; + +const ESTIMATED_ITEM_SIZE: f32 = 38.8; + +#[derive(Clone, Debug)] +pub struct ScrollHandle { + phantom: PhantomData, + pub scrollable_id: scrollable::Id, + pub index: usize, + pub offset: usize, +} + +impl ScrollHandle { + pub fn new() -> ScrollHandle { + ScrollHandle { + phantom: PhantomData, + scrollable_id: scrollable::Id::unique(), + index: 0, + offset: 0, + } + } + + pub fn reset(&mut self) { + self.index = 0; + self.offset = 0; + } + + pub fn get<'a>(&self, search_results: &'a [T]) -> Option<&'a T> { + search_results.get(self.index) + } + + pub fn focus_next(&mut self, item_amount: usize) -> Command { + self.offset = if self.offset < 8 { + self.offset + 1 + } else { + 8 + }; + + if self.index < item_amount - 1 { + self.index = self.index + 1; + + let pos_y = self.index as f32 * ESTIMATED_ITEM_SIZE - (self.offset as f32 * ESTIMATED_ITEM_SIZE); + + scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) + } else { + Command::none() + } + } + + pub fn focus_previous(&mut self) -> Command { + self.offset = if self.offset > 1 { + self.offset - 1 + } else { + 1 + }; + + if self.index > 0 { + self.index = self.index - 1; + + let pos_y = self.index as f32 * ESTIMATED_ITEM_SIZE - (self.offset as f32 * ESTIMATED_ITEM_SIZE); + + scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) + } else { + Command::none() + } + } +} \ No newline at end of file diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index 5569899..b111eeb 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -7,7 +7,7 @@ use iced::widget::row; use iced::widget::text; use common::model::SearchResult; -use crate::ui::ScrollHandle; +use crate::ui::scroll_handle::ScrollHandle; use crate::ui::theme::{Element, GauntletTheme, ThemableWidget}; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; @@ -15,26 +15,26 @@ use crate::ui::theme::image::ImageStyle; use crate::ui::theme::space::ThemeKindSpace; use crate::ui::theme::text::TextStyle; -pub struct SearchList { +pub struct SearchList<'a, Message> { on_select: Box Message>, focused_search_result: usize, - search_results: Vec, + search_results: &'a[SearchResult], } -pub fn search_list( - search_results: Vec, - focused_search_result: &ScrollHandle, +pub fn search_list<'a, Message>( + search_results: &'a[SearchResult], + focused_search_result: &ScrollHandle, on_select: impl Fn(SearchResult) -> Message + 'static, -) -> SearchList { +) -> SearchList<'a, Message> { SearchList::new(search_results, focused_search_result.index, on_select) } #[derive(Debug, Clone)] pub struct SelectItemEvent(SearchResult); -impl SearchList { +impl<'a, Message> SearchList<'a, Message> { pub fn new( - search_results: Vec, + search_results: &'a[SearchResult], focused_search_result: usize, on_open_view: impl Fn(SearchResult) -> Message + 'static, ) -> Self { @@ -46,7 +46,7 @@ impl SearchList { } } -impl Component for SearchList { +impl<'a, Message> Component for SearchList<'a, Message> { type State = (); type Event = SelectItemEvent; @@ -122,11 +122,11 @@ impl Component for SearchList { } } -impl<'a, Message> From> for Element<'a, Message> +impl<'a, Message> From> for Element<'a, Message> where Message: 'a, { - fn from(search_list: SearchList) -> Self { + fn from(search_list: SearchList<'a, Message>) -> Self { component(search_list) } } \ No newline at end of file diff --git a/rust/client/src/ui/state/main_view.rs b/rust/client/src/ui/state/main_view.rs new file mode 100644 index 0000000..cf8c868 --- /dev/null +++ b/rust/client/src/ui/state/main_view.rs @@ -0,0 +1,87 @@ +use iced::Command; +use common::model::SearchResultEntrypointAction; +use crate::ui::{AppModel, AppMsg}; +use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::state::Focus; + +pub enum MainViewState { + None, + ActionPanel { + // ephemeral state + entrypoint_actions_size: usize, + focused_action_item: ScrollHandle, + } +} + +impl MainViewState { + pub fn new() -> Self { + MainViewState::None + } + + pub fn initial(prev_state: &mut MainViewState) { + *prev_state = Self::None + } + + pub fn action_panel(prev_state: &mut MainViewState, entrypoint_actions: &[SearchResultEntrypointAction]) { + *prev_state = Self::ActionPanel { + entrypoint_actions_size: entrypoint_actions.len(), + focused_action_item: ScrollHandle::new(), + } + } + + pub fn is_none(&self) -> bool { + matches!(self, MainViewState::None) + } + + pub fn is_action_panel(&self) -> bool { + matches!(self, MainViewState::None) + } +} + +impl Focus for MainViewState { + fn enter(&mut self) -> Command { + todo!() + } + + fn escape(&mut self) -> Command { + todo!() + } + + fn tab(&mut self) -> Command { + todo!() + } + + fn shift_tab(&mut self) -> Command { + todo!() + } + + fn arrow_up(&mut self) -> Command { + match self { + MainViewState::None => Command::none(), + MainViewState::ActionPanel { focused_action_item, .. } => { + focused_action_item.focus_previous() + } + } + } + + fn arrow_down(&mut self) -> Command { + match self { + MainViewState::None => Command::none(), + MainViewState::ActionPanel { entrypoint_actions_size, focused_action_item } => { + if *entrypoint_actions_size != 0 { + focused_action_item.focus_next(*entrypoint_actions_size + 1) + } else { + Command::none() + } + } + } + } + + fn arrow_left(&mut self) -> Command { + todo!() + } + + fn arrow_right(&mut self) -> Command { + todo!() + } +} diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs new file mode 100644 index 0000000..e0a1608 --- /dev/null +++ b/rust/client/src/ui/state/mod.rs @@ -0,0 +1,236 @@ +mod main_view; + +use crate::ui::scroll_handle::ScrollHandle; +pub use crate::ui::state::main_view::MainViewState; +use crate::ui::AppMsg; +use common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult}; +use iced::widget::text_input; +use iced::widget::text_input::focus; +use iced::Command; +use std::collections::HashMap; + +pub enum GlobalState { + MainView { + // logic + search_field_id: text_input::Id, + + // ephemeral state + prompt: String, + focused_search_result: ScrollHandle, + sub_state: MainViewState, + + // state + pending_plugin_view_data: Option, + search_results: Vec, + }, + ErrorView { + error_view: ErrorViewData, + }, + PluginView(PluginViewData) +} + +#[derive(Clone)] +pub struct PluginViewData { + pub top_level_view: bool, + pub plugin_id: PluginId, + pub plugin_name: String, + pub entrypoint_id: EntrypointId, + pub entrypoint_name: String, + pub action_shortcuts: HashMap, +} + +pub enum ErrorViewData { + PreferenceRequired { + plugin_id: PluginId, + entrypoint_id: EntrypointId, + plugin_preferences_required: bool, + entrypoint_preferences_required: bool, + }, + PluginError { + plugin_id: PluginId, + entrypoint_id: EntrypointId, + }, + BackendTimeout, + UnknownError { + display: String + } +} + +impl GlobalState { + pub fn new(search_field_id: text_input::Id) -> GlobalState { + GlobalState::MainView { + search_field_id, + prompt: "".to_string(), + focused_search_result: ScrollHandle::new(), + sub_state: MainViewState::new(), + pending_plugin_view_data: None, + search_results: vec![] + } + } + + pub fn new_error(error_view_data: ErrorViewData) -> GlobalState { + GlobalState::ErrorView { + error_view: error_view_data, + } + } + + pub fn new_plugin(plugin_view_data: PluginViewData) -> GlobalState { + GlobalState::PluginView(plugin_view_data) + } + + pub fn initial(prev_global_state: &mut GlobalState) -> Command { + let search_field_id = text_input::Id::unique(); + + *prev_global_state = GlobalState::new(search_field_id.clone()); + + Command::batch([ + focus(search_field_id), + Command::perform(async {}, |_| AppMsg::PromptChanged("".to_owned())), + ]) + } + + pub fn error(prev_global_state: &mut GlobalState, error_view_data: ErrorViewData) -> Command { + *prev_global_state = GlobalState::ErrorView { + error_view: error_view_data, + }; + + Command::none() + } + + pub fn plugin(prev_global_state: &mut GlobalState, plugin_view_data: PluginViewData) -> Command { + *prev_global_state = GlobalState::PluginView(plugin_view_data); + + Command::none() + } +} + +pub trait Focus { + fn enter(&mut self) -> Command; + fn escape(&mut self) -> Command; + fn tab(&mut self) -> Command; + fn shift_tab(&mut self) -> Command; + fn arrow_up(&mut self) -> Command; + fn arrow_down(&mut self) -> Command; + fn arrow_left(&mut self) -> Command; + fn arrow_right(&mut self) -> Command; +} + +impl Focus for GlobalState { + fn enter(&mut self) -> Command { + match self { + GlobalState::MainView { focused_search_result, sub_state, search_results, .. } => { + match sub_state { + MainViewState::None => { + if let Some(search_item) = focused_search_result.get(search_results) { + let search_item = search_item.clone(); + Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) + } else { + Command::none() + } + } + MainViewState::ActionPanel { focused_action_item, .. } => { + let widget_id = focused_action_item.index; + + MainViewState::initial(sub_state); + + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) + } + } + } + GlobalState::PluginView(_) => { + todo!() + } + GlobalState::ErrorView { .. } => Command::none() + } + } + + fn escape(&mut self) -> Command { + match self { + GlobalState::MainView { sub_state, .. } => { + match sub_state { + MainViewState::None => { + Command::perform(async {}, |_| AppMsg::HideWindow) + } + MainViewState::ActionPanel { .. } => { + MainViewState::initial(sub_state); + Command::none() + } + } + } + GlobalState::PluginView(PluginViewData { top_level_view: true, plugin_id, .. }) => { + let plugin_id = plugin_id.clone(); + + Command::batch([ + Command::perform(async {}, |_| AppMsg::ClosePluginView(plugin_id)), + GlobalState::initial(self) + ]) + } + GlobalState::PluginView(PluginViewData { top_level_view: false, plugin_id, entrypoint_id, .. }) => { + let plugin_id= plugin_id.clone(); + let entrypoint_id = entrypoint_id.clone(); + Command::perform(async {}, |_| AppMsg::OpenPluginView(plugin_id, entrypoint_id)) + } + GlobalState::ErrorView { .. } => { + Command::perform(async {}, |_| AppMsg::HideWindow) + } + } + } + fn tab(&mut self) -> Command { + match self { + GlobalState::MainView { .. } => Command::none(), + GlobalState::PluginView(_) => Command::none(), + GlobalState::ErrorView { .. } => Command::none(), + } + } + fn shift_tab(&mut self) -> Command { + match self { + GlobalState::MainView { .. } => Command::none(), + GlobalState::PluginView(_) => Command::none(), + GlobalState::ErrorView { .. } => Command::none(), + } + } + fn arrow_up(&mut self) -> Command { + match self { + GlobalState::MainView { focused_search_result, sub_state, .. } => { + if sub_state.is_none() { + focused_search_result.focus_previous() + } else { + sub_state.arrow_up() + } + } + GlobalState::ErrorView { .. } => Command::none(), + GlobalState::PluginView(_) => Command::none(), + } + } + fn arrow_down(&mut self) -> Command { + match self { + GlobalState::MainView { focused_search_result, search_results, sub_state, .. } => { + if sub_state.is_none() { + if search_results.len() != 0 { + focused_search_result.focus_next(search_results.len()) + } else { + Command::none() + } + } else { + sub_state.arrow_down() + } + } + GlobalState::ErrorView { .. } => Command::none(), + GlobalState::PluginView(_) => Command::none(), + } + } + fn arrow_left(&mut self) -> Command { + match self { + GlobalState::MainView { .. } => Command::none(), + GlobalState::PluginView(_) => Command::none(), + GlobalState::ErrorView { .. } => Command::none(), + } + } + fn arrow_right(&mut self) -> Command { + match self { + GlobalState::MainView { .. } => Command::none(), + GlobalState::PluginView(_) => Command::none(), + GlobalState::ErrorView { .. } => Command::none(), + } + } +} diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 4329ec5..a476b2f 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; use anyhow::anyhow; -use common::model::{PhysicalKey, PhysicalShortcut, PluginId, UiPropertyValue, UiPropertyValueToEnum, UiPropertyValueToStruct, UiWidgetId}; +use common::model::{PhysicalKey, PhysicalShortcut, PluginId, SearchResultEntrypointAction, UiPropertyValue, UiPropertyValueToEnum, UiPropertyValueToStruct, UiWidgetId}; use common_ui::shortcut_to_text; use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; @@ -21,7 +21,7 @@ use itertools::Itertools; use crate::model::UiViewEvent; use crate::ui::custom_widgets::loading_bar::LoadingBar; -use crate::ui::ScrollHandle; +use crate::ui::scroll_handle::ScrollHandle; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; use crate::ui::theme::date_picker::DatePickerStyle; @@ -63,19 +63,19 @@ pub enum ComponentWidgetState { }, Detail { show_action_panel: bool, - focused_action_item: ScrollHandle, + focused_action_item: ScrollHandle, }, Form { show_action_panel: bool, - focused_action_item: ScrollHandle, + focused_action_item: ScrollHandle, }, List { show_action_panel: bool, - focused_action_item: ScrollHandle, + focused_action_item: ScrollHandle, }, Grid { show_action_panel: bool, - focused_action_item: ScrollHandle, + focused_action_item: ScrollHandle, }, None } @@ -110,19 +110,19 @@ impl ComponentWidgetState { }, ComponentWidget::Detail { .. } => ComponentWidgetState::Detail { show_action_panel: false, - focused_action_item: ScrollHandle::new(scrollable::Id::unique()), + focused_action_item: ScrollHandle::new(), }, ComponentWidget::Form { .. } => ComponentWidgetState::Form { show_action_panel: false, - focused_action_item: ScrollHandle::new(scrollable::Id::unique()), + focused_action_item: ScrollHandle::new(), }, ComponentWidget::List { .. } => ComponentWidgetState::List { show_action_panel: false, - focused_action_item: ScrollHandle::new(scrollable::Id::unique()), + focused_action_item: ScrollHandle::new(), }, ComponentWidget::Grid { .. } => ComponentWidgetState::Grid { show_action_panel: false, - focused_action_item: ScrollHandle::new(scrollable::Id::unique()), + focused_action_item: ScrollHandle::new(), }, _ => ComponentWidgetState::None } @@ -1284,9 +1284,9 @@ fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option< .into() } -fn render_plugin_root<'a>( +fn render_plugin_root<'a, ACTION>( show_action_panel: bool, - action_panel_scroll_handle: &ScrollHandle, + action_panel_scroll_handle: &ScrollHandle, widget_id: UiWidgetId, children: &[ComponentWidgetWrapper], content: Element<'a, ComponentWidgetEvent>, @@ -1307,16 +1307,14 @@ fn render_plugin_root<'a>( .into() }; - let action_panel = convert_action_panel(children, action_shortcuts); - render_root( show_action_panel, top_panel, top_separator, content, None, - action_panel, - action_panel_scroll_handle, + convert_action_panel(children, action_shortcuts), + Some(action_panel_scroll_handle), entrypoint_name, || ComponentWidgetEvent::ToggleActionPanel { widget_id }, |widget_id| ComponentWidgetEvent::ActionClick { widget_id } @@ -1403,10 +1401,10 @@ fn convert_action_panel(children: &[ComponentWidgetWrapper], action_shortcuts: H } } -fn render_action_panel_items<'a, T: 'a + Clone>( +fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( title: Option, items: Vec, - action_panel_scroll_handle: &ScrollHandle, + action_panel_scroll_handle: &ScrollHandle, on_action: &dyn Fn(UiWidgetId) -> T ) -> Vec> { let mut columns = vec![]; @@ -1491,10 +1489,10 @@ fn render_action_panel_items<'a, T: 'a + Clone>( columns } -fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T>( +fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T, ACTION>( action_panel: ActionPanel, on_action: F, - action_panel_scroll_handle: &ScrollHandle, + action_panel_scroll_handle: &ScrollHandle, ) -> Element<'a, T> { let columns = render_action_panel_items(action_panel.title, action_panel.items, action_panel_scroll_handle, &on_action); @@ -1510,14 +1508,14 @@ fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T>( .themed(ContainerStyle::ActionPanel) } -pub fn render_root<'a, T: 'a + Clone>( +pub fn render_root<'a, T: 'a + Clone, ACTION>( show_action_panel: bool, top_panel: Element<'a, T>, top_separator: Element<'a, T>, content: Element<'a, T>, default_action: Option<(String, PhysicalShortcut)>, action_panel: Option, - action_panel_scroll_handle: &ScrollHandle, + action_panel_scroll_handle: Option<&ScrollHandle>, entrypoint_name: String, on_panel_toggle: impl Fn() -> T, on_action: impl Fn(UiWidgetId) -> T, @@ -1637,9 +1635,9 @@ pub fn render_root<'a, T: 'a + Clone>( .into() }; - let action_panel_element = match action_panel { - None => Space::with_height(1).into(), - Some(action_panel) => render_action_panel(action_panel, on_action, action_panel_scroll_handle) + let action_panel_element = match (action_panel, action_panel_scroll_handle) { + (Some(action_panel), Some(action_panel_scroll_handle)) => render_action_panel(action_panel, on_action, action_panel_scroll_handle), + _ => Space::with_height(1).into(), }; floating_element(content, action_panel_element) @@ -1736,7 +1734,7 @@ fn render_children_string<'a>( }) .join(""); - return render_text_part(&text_part, context); + render_text_part(&text_part, context) } @@ -1744,10 +1742,10 @@ fn render_children<'a>( content: &[ComponentWidgetWrapper], context: ComponentRenderContext ) -> Vec> { - return content + content .into_iter() .map(|child| child.render_widget(context.clone())) - .collect(); + .collect() } fn render_child_by_type<'a>( @@ -1774,14 +1772,14 @@ fn render_children_by_type<'a>( content: &[ComponentWidgetWrapper], predicate: impl Fn(&ComponentWidget) -> bool, context: ComponentRenderContext ) -> Vec> { - return content + content .into_iter() .filter(|child| { let (widget, _) = &*child.get(); predicate(widget) }) .map(|child| child.render_widget(context.clone())) - .collect(); + .collect() } @@ -1836,7 +1834,7 @@ pub enum ComponentWidgetEvent { } impl ComponentWidgetEvent { - pub fn handle(self, plugin_id: PluginId, widget: ComponentWidgetWrapper) -> Option { + pub fn handle(self, _plugin_id: PluginId, widget: ComponentWidgetWrapper) -> Option { match self { ComponentWidgetEvent::LinkClick { widget_id: _, href } => { Some(UiViewEvent::Open { @@ -1870,7 +1868,7 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::SubmitDatePicker { widget_id, value } => { { let (widget, ref mut state) = &mut *widget.get_mut(); - let ComponentWidgetState::DatePicker { state_value, show_picker, } = state else { + let ComponentWidgetState::DatePicker { state_value: _, show_picker, } = state else { panic!("unexpected state kind, widget: {:?} state: {:?}", widget, state) }; From 17ccf5b57c32e72d2895077f7e6d75b469186671 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:59:32 +0200 Subject: [PATCH 092/540] Slightly refactor usage of backend_api --- rust/client/src/ui/mod.rs | 146 +++++++++++++++++--------------- rust/client/src/ui/state/mod.rs | 2 +- 2 files changed, 77 insertions(+), 71 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 7300d44..1e0ed29 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -416,30 +416,13 @@ impl Application for AppModel { GlobalState::PluginView { .. } => {} } - let mut backend_api = self.backend_api.clone(); - - Command::perform(async move { - let search_results = backend_api.search(new_prompt, true) - .await?; - - Ok(search_results) - }, |result| handle_backend_error(result, |search_results| AppMsg::SetSearchResults(search_results))) + self.search(new_prompt, true) } } AppMsg::UpdateSearchResults => { - match &mut self.global_state { + match &self.global_state { GlobalState::MainView { prompt, .. } => { - let prompt = prompt.clone(); - - let mut backend_api = self.backend_api.clone(); - - Command::perform(async move { - let search_results = backend_api.search(prompt, false) - .await?; - - Ok(search_results) - }, |result| handle_backend_error(result, |search_results| AppMsg::SetSearchResults(search_results))) - + self.search(prompt.clone(), false) } _ => Command::none() } @@ -479,8 +462,6 @@ impl Application for AppModel { } } AppMsg::IcedEvent(Event::Keyboard(event)) => { - let mut backend_client = self.backend_api.clone(); - match event { keyboard::Event::KeyPressed { key, modifiers, physical_key, text, .. } => { tracing::debug!("Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", key, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); @@ -489,8 +470,8 @@ impl Application for AppModel { Key::Named(Named::ArrowDown) => self.global_state.arrow_down(), Key::Named(Named::Escape) => self.global_state.escape(), Key::Named(Named::Enter) => { - // fired in cases where main text field is not focused - Command::perform(async {}, |_| AppMsg::PromptSubmit) + // for main view, also fired in cases where main text field is not focused + self.global_state.enter() }, Key::Named(Named::Backspace) => self.backspace_prompt(), _ => { @@ -505,20 +486,7 @@ impl Application for AppModel { Command::none() } Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - let (plugin_id, entrypoint_id) = { - let client_context = self.client_context.read().expect("lock is poisoned"); - (client_context.get_view_plugin_id(), client_context.get_view_entrypoint_id()) - }; - - Command::perform( - async move { - backend_client.send_keyboard_event(plugin_id, entrypoint_id, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) - .await?; - - Ok(()) - }, - |result| handle_backend_error(result, |()| AppMsg::Noop), - ) + self.handle_plugin_keyboard_event(physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) } None => { Command::none() @@ -576,30 +544,7 @@ impl Application for AppModel { AppMsg::IcedEvent(_) => Command::none(), AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => self.global_state.escape(), AppMsg::WidgetEvent { widget_event, plugin_id, render_location } => { - let mut backend_client = self.backend_api.clone(); - let client_context = self.client_context.clone(); - - Command::perform(async move { - let event = { - let client_context = client_context.read().expect("lock is poisoned"); - client_context.handle_event(render_location, &plugin_id, widget_event) - }; - - if let Some(event) = event { - match event { - UiViewEvent::View { widget_id, event_name, event_arguments } => { - backend_client.send_view_event(plugin_id, widget_id, event_name, event_arguments) - .await?; - } - UiViewEvent::Open { href } => { - backend_client.send_open_event(plugin_id, href) - .await?; - } - } - } - - Ok(()) - }, |result| handle_backend_error(result, |()| AppMsg::Noop)) + self.handle_plugin_event(widget_event, plugin_id, render_location) } AppMsg::Noop => Command::none(), AppMsg::FontLoaded(result) => { @@ -643,14 +588,7 @@ impl Application for AppModel { ) } AppMsg::OpenSettingsPreferences { plugin_id, entrypoint_id, } => { - let mut backend_api = self.backend_api.clone(); - - Command::perform(async move { - backend_api.open_settings_window_preferences(plugin_id, entrypoint_id) - .await?; - - Ok(()) - }, |result| handle_backend_error(result, |()| AppMsg::Noop)) + self.open_settings_window_preferences(plugin_id, entrypoint_id) } AppMsg::OnOpenView { action_shortcuts } => { match &mut self.global_state { @@ -1347,6 +1285,74 @@ impl AppModel { }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } + fn handle_plugin_event(&self, widget_event: ComponentWidgetEvent, plugin_id: PluginId, render_location: UiRenderLocation) -> Command { + let mut backend_client = self.backend_api.clone(); + let client_context = self.client_context.clone(); + + Command::perform(async move { + let event = { + let client_context = client_context.read().expect("lock is poisoned"); + client_context.handle_event(render_location, &plugin_id, widget_event) + }; + + if let Some(event) = event { + match event { + UiViewEvent::View { widget_id, event_name, event_arguments } => { + backend_client.send_view_event(plugin_id, widget_id, event_name, event_arguments) + .await?; + } + UiViewEvent::Open { href } => { + backend_client.send_open_event(plugin_id, href) + .await?; + } + } + } + + Ok(()) + }, |result| handle_backend_error(result, |()| AppMsg::Noop)) + } + + fn handle_plugin_keyboard_event(&self, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Command { + let mut backend_client = self.backend_api.clone(); + + let (plugin_id, entrypoint_id) = { + let client_context = self.client_context.read().expect("lock is poisoned"); + (client_context.get_view_plugin_id(), client_context.get_view_entrypoint_id()) + }; + + Command::perform( + async move { + backend_client.send_keyboard_event(plugin_id, entrypoint_id, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + .await?; + + Ok(()) + }, + |result| handle_backend_error(result, |()| AppMsg::Noop), + ) + } + + fn search(&self, new_prompt: String, render_inline_view: bool) -> Command { + let mut backend_api = self.backend_api.clone(); + + Command::perform(async move { + let search_results = backend_api.search(new_prompt, render_inline_view) + .await?; + + Ok(search_results) + }, |result| handle_backend_error(result, |search_results| AppMsg::SetSearchResults(search_results))) + } + + fn open_settings_window_preferences(&self, plugin_id: PluginId, entrypoint_id: Option) -> Command { + let mut backend_api = self.backend_api.clone(); + + Command::perform(async move { + backend_api.open_settings_window_preferences(plugin_id, entrypoint_id) + .await?; + + Ok(()) + }, |result| handle_backend_error(result, |()| AppMsg::Noop)) + } + fn append_prompt(&mut self, value: String) -> Command { match &mut self.global_state { GlobalState::MainView { search_field_id, prompt, .. } => { diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index e0a1608..cebfebe 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -85,7 +85,7 @@ impl GlobalState { Command::batch([ focus(search_field_id), - Command::perform(async {}, |_| AppMsg::PromptChanged("".to_owned())), + Command::perform(async {}, |_| AppMsg::UpdateSearchResults), ]) } From 6f88f3f56ad57734cb4ab48ee3eddf1957a44343 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 29 Sep 2024 16:32:28 +0200 Subject: [PATCH 093/540] Fix flicker on main view after returning from plugin view after refactor --- rust/client/src/ui/mod.rs | 34 +++++------ rust/client/src/ui/state/main_view.rs | 36 +++++------- rust/client/src/ui/state/mod.rs | 84 +++++++++++++++------------ 3 files changed, 76 insertions(+), 78 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 1e0ed29..33b56fd 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -70,6 +70,7 @@ pub struct AppModel { // state client_context: Arc>, global_state: GlobalState, + search_results: Vec, } @@ -353,6 +354,7 @@ impl Application for AppModel { // state global_state, client_context: Arc::new(StdRwLock::new(client_context)), + search_results: vec![], }, Command::batch(commands), ) @@ -427,15 +429,9 @@ impl Application for AppModel { _ => Command::none() } } - AppMsg::PromptSubmit => self.global_state.enter(), + AppMsg::PromptSubmit => self.global_state.enter(&self.search_results), AppMsg::SetSearchResults(new_search_results) => { - match &mut self.global_state { - GlobalState::MainView { search_results, .. } => { - *search_results = new_search_results; - } - GlobalState::ErrorView { .. } => {} - GlobalState::PluginView { .. } => {} - } + self.search_results = new_search_results; Command::none() } @@ -466,12 +462,12 @@ impl Application for AppModel { keyboard::Event::KeyPressed { key, modifiers, physical_key, text, .. } => { tracing::debug!("Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", key, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); match key { - Key::Named(Named::ArrowUp) => self.global_state.arrow_up(), - Key::Named(Named::ArrowDown) => self.global_state.arrow_down(), + Key::Named(Named::ArrowUp) => self.global_state.arrow_up(&self.search_results), + Key::Named(Named::ArrowDown) => self.global_state.arrow_down(&self.search_results), Key::Named(Named::Escape) => self.global_state.escape(), Key::Named(Named::Enter) => { // for main view, also fired in cases where main text field is not focused - self.global_state.enter() + self.global_state.enter(&self.search_results) }, Key::Named(Named::Backspace) => self.backspace_prompt(), _ => { @@ -685,11 +681,11 @@ impl Application for AppModel { } AppMsg::ToggleActionPanel => { match &mut self.global_state { - GlobalState::MainView { sub_state, search_results, focused_search_result, .. } => { + GlobalState::MainView { sub_state, focused_search_result, .. } => { match sub_state { MainViewState::None => { - if let Some(search_item) = focused_search_result.get(&search_results) { - MainViewState::action_panel(sub_state, &search_item.entrypoint_actions); + if let Some(_) = focused_search_result.get(&self.search_results) { + MainViewState::action_panel(sub_state); } } MainViewState::ActionPanel { .. } => { @@ -708,8 +704,8 @@ impl Application for AppModel { } AppMsg::OnEntrypointAction(widget_id) => { match &self.global_state { - GlobalState::MainView { focused_search_result, search_results, .. } => { - if let Some(search_item) = focused_search_result.get(&search_results) { + GlobalState::MainView { focused_search_result, .. } => { + if let Some(search_item) = focused_search_result.get(&self.search_results) { let search_item = search_item.clone(); if widget_id == 0 { Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) @@ -944,7 +940,7 @@ impl Application for AppModel { } } } - GlobalState::MainView { focused_search_result, sub_state, search_results, prompt, search_field_id, .. } => { + GlobalState::MainView { focused_search_result, sub_state, prompt, search_field_id, .. } => { let input: Element<_> = text_input("Search...", prompt) .on_input(AppMsg::PromptChanged) .on_submit(AppMsg::PromptSubmit) @@ -953,7 +949,7 @@ impl Application for AppModel { .themed(TextInputStyle::MainSearch); let search_list = search_list( - search_results, + &self.search_results, &focused_search_result, |search_result| AppMsg::RunSearchItemAction(search_result, None), ); @@ -984,7 +980,7 @@ impl Application for AppModel { list, ]).into(); - let (default_action, action_panel) = if let Some(search_item) = focused_search_result.get(search_results) { + let (default_action, action_panel) = if let Some(search_item) = focused_search_result.get(&self.search_results) { let label = match search_item.entrypoint_type { SearchResultEntrypointType::Command => "Run Command", SearchResultEntrypointType::View => "Open View", diff --git a/rust/client/src/ui/state/main_view.rs b/rust/client/src/ui/state/main_view.rs index cf8c868..7882fa3 100644 --- a/rust/client/src/ui/state/main_view.rs +++ b/rust/client/src/ui/state/main_view.rs @@ -1,14 +1,13 @@ -use iced::Command; -use common::model::SearchResultEntrypointAction; -use crate::ui::{AppModel, AppMsg}; use crate::ui::scroll_handle::ScrollHandle; use crate::ui::state::Focus; +use crate::ui::AppMsg; +use common::model::SearchResultEntrypointAction; +use iced::Command; pub enum MainViewState { None, ActionPanel { // ephemeral state - entrypoint_actions_size: usize, focused_action_item: ScrollHandle, } } @@ -22,24 +21,15 @@ impl MainViewState { *prev_state = Self::None } - pub fn action_panel(prev_state: &mut MainViewState, entrypoint_actions: &[SearchResultEntrypointAction]) { + pub fn action_panel(prev_state: &mut MainViewState) { *prev_state = Self::ActionPanel { - entrypoint_actions_size: entrypoint_actions.len(), focused_action_item: ScrollHandle::new(), } } - - pub fn is_none(&self) -> bool { - matches!(self, MainViewState::None) - } - - pub fn is_action_panel(&self) -> bool { - matches!(self, MainViewState::None) - } } -impl Focus for MainViewState { - fn enter(&mut self) -> Command { +impl Focus for MainViewState { + fn enter(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { todo!() } @@ -55,7 +45,7 @@ impl Focus for MainViewState { todo!() } - fn arrow_up(&mut self) -> Command { + fn arrow_up(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { match self { MainViewState::None => Command::none(), MainViewState::ActionPanel { focused_action_item, .. } => { @@ -64,12 +54,12 @@ impl Focus for MainViewState { } } - fn arrow_down(&mut self) -> Command { + fn arrow_down(&mut self, focus_list: &[SearchResultEntrypointAction]) -> Command { match self { MainViewState::None => Command::none(), - MainViewState::ActionPanel { entrypoint_actions_size, focused_action_item } => { - if *entrypoint_actions_size != 0 { - focused_action_item.focus_next(*entrypoint_actions_size + 1) + MainViewState::ActionPanel { focused_action_item } => { + if focus_list.len() != 0 { + focused_action_item.focus_next(focus_list.len() + 1) } else { Command::none() } @@ -77,11 +67,11 @@ impl Focus for MainViewState { } } - fn arrow_left(&mut self) -> Command { + fn arrow_left(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { todo!() } - fn arrow_right(&mut self) -> Command { + fn arrow_right(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { todo!() } } diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index cebfebe..39b92ea 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -17,11 +17,10 @@ pub enum GlobalState { // ephemeral state prompt: String, focused_search_result: ScrollHandle, - sub_state: MainViewState, // state + sub_state: MainViewState, pending_plugin_view_data: Option, - search_results: Vec, }, ErrorView { error_view: ErrorViewData, @@ -64,7 +63,6 @@ impl GlobalState { focused_search_result: ScrollHandle::new(), sub_state: MainViewState::new(), pending_plugin_view_data: None, - search_results: vec![] } } @@ -104,24 +102,24 @@ impl GlobalState { } } -pub trait Focus { - fn enter(&mut self) -> Command; +pub trait Focus { + fn enter(&mut self, focus_list: &[T]) -> Command; fn escape(&mut self) -> Command; fn tab(&mut self) -> Command; fn shift_tab(&mut self) -> Command; - fn arrow_up(&mut self) -> Command; - fn arrow_down(&mut self) -> Command; - fn arrow_left(&mut self) -> Command; - fn arrow_right(&mut self) -> Command; + fn arrow_up(&mut self, focus_list: &[T]) -> Command; + fn arrow_down(&mut self, focus_list: &[T]) -> Command; + fn arrow_left(&mut self, focus_list: &[T]) -> Command; + fn arrow_right(&mut self, focus_list: &[T]) -> Command; } -impl Focus for GlobalState { - fn enter(&mut self) -> Command { +impl Focus for GlobalState { + fn enter(&mut self, focus_list: &[SearchResult]) -> Command { match self { - GlobalState::MainView { focused_search_result, sub_state, search_results, .. } => { + GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { MainViewState::None => { - if let Some(search_item) = focused_search_result.get(search_results) { + if let Some(search_item) = focused_search_result.get(focus_list) { let search_item = search_item.clone(); Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) } else { @@ -189,44 +187,58 @@ impl Focus for GlobalState { GlobalState::ErrorView { .. } => Command::none(), } } - fn arrow_up(&mut self) -> Command { + fn arrow_up(&mut self, focus_list: &[SearchResult]) -> Command { + match self { + GlobalState::MainView { focused_search_result, sub_state, .. } => { + match sub_state { + MainViewState::None => { + focused_search_result.focus_previous() + } + MainViewState::ActionPanel { .. } => { + if let Some(search_item) = focused_search_result.get(focus_list) { + sub_state.arrow_up(&search_item.entrypoint_actions) + } else { + Command::none() + } + } + } + } + GlobalState::ErrorView { .. } => Command::none(), + GlobalState::PluginView(_) => Command::none(), + } + } + fn arrow_down(&mut self, focus_list: &[SearchResult]) -> Command { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { - if sub_state.is_none() { - focused_search_result.focus_previous() - } else { - sub_state.arrow_up() - } - } - GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView(_) => Command::none(), - } - } - fn arrow_down(&mut self) -> Command { - match self { - GlobalState::MainView { focused_search_result, search_results, sub_state, .. } => { - if sub_state.is_none() { - if search_results.len() != 0 { - focused_search_result.focus_next(search_results.len()) - } else { - Command::none() + match sub_state { + MainViewState::None => { + if focus_list.len() != 0 { + focused_search_result.focus_next(focus_list.len()) + } else { + Command::none() + } + } + MainViewState::ActionPanel { .. } => { + if let Some(search_item) = focused_search_result.get(focus_list) { + sub_state.arrow_down(&search_item.entrypoint_actions) + } else { + Command::none() + } } - } else { - sub_state.arrow_down() } } GlobalState::ErrorView { .. } => Command::none(), GlobalState::PluginView(_) => Command::none(), } } - fn arrow_left(&mut self) -> Command { + fn arrow_left(&mut self, _focus_list: &[SearchResult]) -> Command { match self { GlobalState::MainView { .. } => Command::none(), GlobalState::PluginView(_) => Command::none(), GlobalState::ErrorView { .. } => Command::none(), } } - fn arrow_right(&mut self) -> Command { + fn arrow_right(&mut self, _focus_list: &[SearchResult]) -> Command { match self { GlobalState::MainView { .. } => Command::none(), GlobalState::PluginView(_) => Command::none(), From e320145abe3b09e1cd96c779c67e09556d7346f0 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 29 Sep 2024 20:43:30 +0200 Subject: [PATCH 094/540] Implement keyboard navigation for plugin view action panel --- rust/client/src/model.rs | 4 + rust/client/src/ui/client_context.rs | 10 +- rust/client/src/ui/mod.rs | 143 ++++++++++++++---------- rust/client/src/ui/state/mod.rs | 116 ++++++++++++++----- rust/client/src/ui/state/plugin_view.rs | 78 +++++++++++++ rust/client/src/ui/view_container.rs | 5 + rust/client/src/ui/widget.rs | 133 +++++++++++++--------- rust/client/src/ui/widget_container.rs | 10 +- 8 files changed, 358 insertions(+), 141 deletions(-) create mode 100644 rust/client/src/ui/state/plugin_view.rs diff --git a/rust/client/src/model.rs b/rust/client/src/model.rs index c1d5e22..06a866a 100644 --- a/rust/client/src/model.rs +++ b/rust/client/src/model.rs @@ -1,4 +1,5 @@ use common::model::{UiPropertyValue, UiWidgetId}; +use crate::ui::AppMsg; #[derive(Debug, Clone)] pub enum UiViewEvent { @@ -10,4 +11,7 @@ pub enum UiViewEvent { Open { href: String }, + AppEvent { + event: AppMsg + }, } diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index fe37440..07b1bb7 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -1,4 +1,4 @@ -use common::model::{EntrypointId, PluginId, UiRenderLocation, UiWidget}; +use common::model::{EntrypointId, PluginId, UiRenderLocation, UiWidget, UiWidgetId}; use crate::model::UiViewEvent; use crate::ui::widget::ComponentWidgetEvent; @@ -75,7 +75,11 @@ impl ClientContext { } } - pub fn show_action_panel(&self) { - self.view.show_action_panel() + pub fn toggle_action_panel(&self) { + self.view.toggle_action_panel() + } + + pub fn get_action_ids(&self) -> Vec { + self.view.get_action_ids() } } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 33b56fd..2177ea1 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -55,7 +55,7 @@ mod state; pub use theme::GauntletTheme; use crate::ui::scroll_handle::ScrollHandle; -use crate::ui::state::{ErrorViewData, Focus, GlobalState, MainViewState, PluginViewData}; +use crate::ui::state::{ErrorViewData, Focus, GlobalState, MainViewState, PluginViewData, PluginViewState}; pub struct AppModel { // logic @@ -301,18 +301,23 @@ impl Application for AppModel { "Screenshot Entrypoint", ); + let context = Arc::new(StdRwLock::new(context)); + commands.push(Command::perform(async move { top_level_view }, |top_level_view| AppMsg::ReplaceView { top_level_view })); let state= match render_location { ScenarioUiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique()), - ScenarioUiRenderLocation::View => GlobalState::new_plugin(PluginViewData { - top_level_view, - plugin_id, - plugin_name: "Screenshot Gen".to_string(), - entrypoint_id, - entrypoint_name: gen_name, - action_shortcuts: Default::default(), - }) + ScenarioUiRenderLocation::View => GlobalState::new_plugin( + PluginViewData { + top_level_view, + plugin_id, + plugin_name: "Screenshot Gen".to_string(), + entrypoint_id, + entrypoint_name: gen_name, + action_shortcuts: Default::default(), + }, + context.clone() + ) }; (context, state) @@ -325,7 +330,7 @@ impl Application for AppModel { entrypoint_preferences_required, }; - (ClientContext::new(), GlobalState::new_error(error_view)) + (Arc::new(StdRwLock::new(ClientContext::new())), GlobalState::new_error(error_view)) } ScenarioFrontendEvent::ShowPluginErrorView { entrypoint_id, render_location: _ } => { let error_view = ErrorViewData::PluginError { @@ -333,11 +338,11 @@ impl Application for AppModel { entrypoint_id: EntrypointId::from_string(entrypoint_id), }; - (ClientContext::new(), GlobalState::new_error(error_view)) + (Arc::new(StdRwLock::new(ClientContext::new())), GlobalState::new_error(error_view)) } } } else { - (ClientContext::new(), GlobalState::new(text_input::Id::unique())) + (Arc::new(StdRwLock::new(ClientContext::new())), GlobalState::new(text_input::Id::unique())) }; ( @@ -353,7 +358,7 @@ impl Application for AppModel { // state global_state, - client_context: Arc::new(StdRwLock::new(client_context)), + client_context, search_results: vec![], }, Command::batch(commands), @@ -442,15 +447,19 @@ impl Application for AppModel { None => Command::none(), Some(pending_plugin_view_data) => { let pending_plugin_view_data = pending_plugin_view_data.clone(); - GlobalState::plugin(&mut self.global_state, PluginViewData { - top_level_view, - ..pending_plugin_view_data - }) + GlobalState::plugin( + &mut self.global_state, + PluginViewData { + top_level_view, + ..pending_plugin_view_data + }, + self.client_context.clone() + ) } } } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView(plugin_view_data) => { + GlobalState::PluginView { plugin_view_data, ..} => { plugin_view_data.top_level_view = top_level_view; Command::none() @@ -471,42 +480,46 @@ impl Application for AppModel { }, Key::Named(Named::Backspace) => self.backspace_prompt(), _ => { - match self.global_state { - GlobalState::MainView { .. } => { - match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - let client_context = self.client_context.read().expect("lock is poisoned"); - - client_context.show_action_panel(); - - Command::none() + match &self.global_state { + GlobalState::MainView { sub_state, .. } => { + match sub_state { + MainViewState::None => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { + Command::perform(async {}, |_| AppMsg::ToggleActionPanel) + } + _ => { + match text { + Some(text) => { + self.append_prompt(text.to_string()) + } + None => { + Command::none() + } + } + } + } } - Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - self.handle_plugin_keyboard_event(physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) - } - None => { - Command::none() + MainViewState::ActionPanel { .. } => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { + Command::perform(async {}, |_| AppMsg::ToggleActionPanel) + } + _ => Command::none() + } } } } - GlobalState::ErrorView { .. } => { - Command::none() - } + GlobalState::ErrorView { .. } => Command::none(), GlobalState::PluginView { .. } => { match physical_key_model(physical_key, modifiers) { Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { Command::perform(async {}, |_| AppMsg::ToggleActionPanel) } - _ => { - match text { - Some(text) => { - self.append_prompt(text.to_string()) - } - None => { - Command::none() - } - } + Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { + self.handle_plugin_keyboard_event(physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) } + _ => Command::none() } } } @@ -597,7 +610,7 @@ impl Application for AppModel { }; } GlobalState::ErrorView { .. } => { }, - GlobalState::PluginView(plugin_view_data) => { + GlobalState::PluginView { plugin_view_data, ..} => { plugin_view_data.action_shortcuts = action_shortcuts; } } @@ -694,9 +707,19 @@ impl Application for AppModel { } } GlobalState::ErrorView { .. } => { }, - GlobalState::PluginView { .. } => { - // todo - // self.show_action_panel = !self.show_action_panel; + GlobalState::PluginView { sub_state, .. } => { + let client_context = self.client_context.read().expect("lock is poisoned"); + + client_context.toggle_action_panel(); + + match sub_state { + PluginViewState::None => { + PluginViewState::action_panel(sub_state) + } + PluginViewState::ActionPanel { .. } => { + PluginViewState::initial(sub_state) + } + } } } @@ -1065,7 +1088,7 @@ impl Application for AppModel { root } - GlobalState::PluginView(plugin_view_data) => { + GlobalState::PluginView { plugin_view_data, sub_state, .. } => { let PluginViewData { top_level_view: _, plugin_id, @@ -1077,6 +1100,7 @@ impl Application for AppModel { let container_element: Element<_> = view_container( self.client_context.clone(), + sub_state.clone(), plugin_id.to_owned(), plugin_name.to_owned(), entrypoint_id.to_owned(), @@ -1178,7 +1202,7 @@ impl AppModel { ); match &self.global_state { - GlobalState::PluginView(PluginViewData { plugin_id, .. }) => { + GlobalState::PluginView { plugin_view_data: PluginViewData { plugin_id, .. }, .. } => { commands.push(self.close_plugin_view(plugin_id.clone())); } GlobalState::MainView { .. } => {} @@ -1296,16 +1320,21 @@ impl AppModel { UiViewEvent::View { widget_id, event_name, event_arguments } => { backend_client.send_view_event(plugin_id, widget_id, event_name, event_arguments) .await?; + Ok(AppMsg::Noop) + } UiViewEvent::Open { href } => { backend_client.send_open_event(plugin_id, href) .await?; - } - } - } - Ok(()) - }, |result| handle_backend_error(result, |()| AppMsg::Noop)) + Ok(AppMsg::Noop) + } + UiViewEvent::AppEvent { event } => Ok(event) + } + } else { + Ok(AppMsg::Noop) + } + }, |result| handle_backend_error(result, |msg| msg)) } fn handle_plugin_keyboard_event(&self, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Command { @@ -1357,7 +1386,7 @@ impl AppModel { focus(search_field_id.clone()) } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView(_) => Command::none(), + GlobalState::PluginView { .. } => Command::none(), } } @@ -1371,7 +1400,7 @@ impl AppModel { focus(search_field_id.clone()) } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView(_) => Command::none(), + GlobalState::PluginView { .. } => Command::none(), } } } diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 39b92ea..3a24786 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -1,13 +1,17 @@ mod main_view; +mod plugin_view; +use crate::ui::client_context::ClientContext; use crate::ui::scroll_handle::ScrollHandle; pub use crate::ui::state::main_view::MainViewState; +pub use crate::ui::state::plugin_view::PluginViewState; use crate::ui::AppMsg; use common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult}; use iced::widget::text_input; use iced::widget::text_input::focus; use iced::Command; use std::collections::HashMap; +use std::sync::{Arc, RwLock as StdRwLock}; pub enum GlobalState { MainView { @@ -25,7 +29,11 @@ pub enum GlobalState { ErrorView { error_view: ErrorViewData, }, - PluginView(PluginViewData) + PluginView { + client_context: Arc>, + plugin_view_data: PluginViewData, + sub_state: PluginViewState, + }, } #[derive(Clone)] @@ -52,7 +60,7 @@ pub enum ErrorViewData { BackendTimeout, UnknownError { display: String - } + }, } impl GlobalState { @@ -72,8 +80,12 @@ impl GlobalState { } } - pub fn new_plugin(plugin_view_data: PluginViewData) -> GlobalState { - GlobalState::PluginView(plugin_view_data) + pub fn new_plugin(plugin_view_data: PluginViewData, client_context: Arc>) -> GlobalState { + GlobalState::PluginView { + client_context, + plugin_view_data, + sub_state: PluginViewState::new(), + } } pub fn initial(prev_global_state: &mut GlobalState) -> Command { @@ -95,8 +107,12 @@ impl GlobalState { Command::none() } - pub fn plugin(prev_global_state: &mut GlobalState, plugin_view_data: PluginViewData) -> Command { - *prev_global_state = GlobalState::PluginView(plugin_view_data); + pub fn plugin(prev_global_state: &mut GlobalState, plugin_view_data: PluginViewData, client_context: Arc>) -> Command { + *prev_global_state = GlobalState::PluginView { + client_context, + plugin_view_data, + sub_state: PluginViewState::new(), + }; Command::none() } @@ -135,8 +151,17 @@ impl Focus for GlobalState { } } } - GlobalState::PluginView(_) => { - todo!() + GlobalState::PluginView { sub_state, .. } => { + match sub_state { + PluginViewState::None => Command::none(), + PluginViewState::ActionPanel { focused_action_item, .. } => { + let widget_id = focused_action_item.index; + + PluginViewState::initial(sub_state); + + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) + } + } } GlobalState::ErrorView { .. } => Command::none() } @@ -155,18 +180,35 @@ impl Focus for GlobalState { } } } - GlobalState::PluginView(PluginViewData { top_level_view: true, plugin_id, .. }) => { - let plugin_id = plugin_id.clone(); + GlobalState::PluginView { + plugin_view_data: PluginViewData { + top_level_view, + plugin_id, + entrypoint_id, + .. + }, + sub_state, + .. + } => { + match sub_state { + PluginViewState::None => { + if *top_level_view { + let plugin_id = plugin_id.clone(); - Command::batch([ - Command::perform(async {}, |_| AppMsg::ClosePluginView(plugin_id)), - GlobalState::initial(self) - ]) - } - GlobalState::PluginView(PluginViewData { top_level_view: false, plugin_id, entrypoint_id, .. }) => { - let plugin_id= plugin_id.clone(); - let entrypoint_id = entrypoint_id.clone(); - Command::perform(async {}, |_| AppMsg::OpenPluginView(plugin_id, entrypoint_id)) + Command::batch([ + Command::perform(async {}, |_| AppMsg::ClosePluginView(plugin_id)), + GlobalState::initial(self) + ]) + } else { + let plugin_id = plugin_id.clone(); + let entrypoint_id = entrypoint_id.clone(); + Command::perform(async {}, |_| AppMsg::OpenPluginView(plugin_id, entrypoint_id)) + } + } + PluginViewState::ActionPanel { .. } => { + Command::perform(async {}, |_| AppMsg::ToggleActionPanel) + } + } } GlobalState::ErrorView { .. } => { Command::perform(async {}, |_| AppMsg::HideWindow) @@ -176,20 +218,20 @@ impl Focus for GlobalState { fn tab(&mut self) -> Command { match self { GlobalState::MainView { .. } => Command::none(), - GlobalState::PluginView(_) => Command::none(), + GlobalState::PluginView { .. } => Command::none(), GlobalState::ErrorView { .. } => Command::none(), } } fn shift_tab(&mut self) -> Command { match self { GlobalState::MainView { .. } => Command::none(), - GlobalState::PluginView(_) => Command::none(), + GlobalState::PluginView { .. } => Command::none(), GlobalState::ErrorView { .. } => Command::none(), } } fn arrow_up(&mut self, focus_list: &[SearchResult]) -> Command { match self { - GlobalState::MainView { focused_search_result, sub_state, .. } => { + GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { MainViewState::None => { focused_search_result.focus_previous() @@ -204,7 +246,18 @@ impl Focus for GlobalState { } } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView(_) => Command::none(), + GlobalState::PluginView { sub_state, client_context, .. } => { + match sub_state { + PluginViewState::None => Command::none(), + PluginViewState::ActionPanel { .. } => { + let client_context = client_context.read().expect("lock is poisoned"); + + let action_ids = client_context.get_action_ids(); + + sub_state.arrow_up(&action_ids) + } + } + }, } } fn arrow_down(&mut self, focus_list: &[SearchResult]) -> Command { @@ -228,20 +281,31 @@ impl Focus for GlobalState { } } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView(_) => Command::none(), + GlobalState::PluginView { sub_state, client_context, .. } => { + match sub_state { + PluginViewState::None => Command::none(), + PluginViewState::ActionPanel { .. } => { + let client_context = client_context.read().expect("lock is poisoned"); + + let action_ids = client_context.get_action_ids(); + + sub_state.arrow_down(&action_ids) + } + } + } } } fn arrow_left(&mut self, _focus_list: &[SearchResult]) -> Command { match self { GlobalState::MainView { .. } => Command::none(), - GlobalState::PluginView(_) => Command::none(), + GlobalState::PluginView { .. } => Command::none(), GlobalState::ErrorView { .. } => Command::none(), } } fn arrow_right(&mut self, _focus_list: &[SearchResult]) -> Command { match self { GlobalState::MainView { .. } => Command::none(), - GlobalState::PluginView(_) => Command::none(), + GlobalState::PluginView { .. } => Command::none(), GlobalState::ErrorView { .. } => Command::none(), } } diff --git a/rust/client/src/ui/state/plugin_view.rs b/rust/client/src/ui/state/plugin_view.rs new file mode 100644 index 0000000..7aa7d34 --- /dev/null +++ b/rust/client/src/ui/state/plugin_view.rs @@ -0,0 +1,78 @@ +use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::state::Focus; +use crate::ui::AppMsg; +use common::model::UiWidgetId; +use iced::Command; + +#[derive(Debug, Clone)] +pub enum PluginViewState { + None, + ActionPanel { + // ephemeral state + focused_action_item: ScrollHandle, + } +} + +impl PluginViewState { + pub fn new() -> Self { + PluginViewState::None + } + + pub fn initial(prev_state: &mut PluginViewState) { + *prev_state = Self::None + } + + pub fn action_panel(prev_state: &mut PluginViewState) { + *prev_state = Self::ActionPanel { + focused_action_item: ScrollHandle::new(), + } + } +} + +impl Focus for PluginViewState { + fn enter(&mut self, _focus_list: &[UiWidgetId]) -> Command { + todo!() + } + + fn escape(&mut self) -> Command { + todo!() + } + + fn tab(&mut self) -> Command { + todo!() + } + + fn shift_tab(&mut self) -> Command { + todo!() + } + + fn arrow_up(&mut self, _focus_list: &[UiWidgetId]) -> Command { + match self { + PluginViewState::None => Command::none(), + PluginViewState::ActionPanel { focused_action_item, .. } => { + focused_action_item.focus_previous() + } + } + } + + fn arrow_down(&mut self, focus_list: &[UiWidgetId]) -> Command { + match self { + PluginViewState::None => Command::none(), + PluginViewState::ActionPanel { focused_action_item } => { + if focus_list.len() != 0 { + focused_action_item.focus_next(focus_list.len()) + } else { + Command::none() + } + } + } + } + + fn arrow_left(&mut self, _focus_list: &[UiWidgetId]) -> Command { + todo!() + } + + fn arrow_right(&mut self, _focus_list: &[UiWidgetId]) -> Command { + todo!() + } +} diff --git a/rust/client/src/ui/view_container.rs b/rust/client/src/ui/view_container.rs index 537711d..16fba18 100644 --- a/rust/client/src/ui/view_container.rs +++ b/rust/client/src/ui/view_container.rs @@ -8,11 +8,13 @@ use common::model::{EntrypointId, PhysicalShortcut, PluginId, UiRenderLocation}; use crate::ui::{AppMsg}; use crate::ui::client_context::ClientContext; +use crate::ui::state::PluginViewState; use crate::ui::theme::{Element, GauntletTheme}; use crate::ui::widget::{ComponentRenderContext, ComponentWidgetEvent}; pub struct ViewContainer { client_context: Arc>, + plugin_view_state: PluginViewState, plugin_id: PluginId, plugin_name: String, entrypoint_id: EntrypointId, @@ -22,6 +24,7 @@ pub struct ViewContainer { pub fn view_container( client_context: Arc>, + plugin_view_state: PluginViewState, plugin_id: PluginId, plugin_name: String, entrypoint_id: EntrypointId, @@ -30,6 +33,7 @@ pub fn view_container( ) -> ViewContainer { ViewContainer { client_context, + plugin_view_state, plugin_id, plugin_name, entrypoint_id, @@ -60,6 +64,7 @@ impl Component for ViewContainer { view_container.render_widget(ComponentRenderContext::Root { entrypoint_name: self.entrypoint_name.clone(), action_shortcuts: self.action_shortcuts.clone(), + plugin_view_state: self.plugin_view_state.clone() }) } } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index a476b2f..88627d8 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1,10 +1,11 @@ +use std::cell::Cell; use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::str::FromStr; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; use anyhow::anyhow; -use common::model::{PhysicalKey, PhysicalShortcut, PluginId, SearchResultEntrypointAction, UiPropertyValue, UiPropertyValueToEnum, UiPropertyValueToStruct, UiWidgetId}; +use common::model::{PhysicalKey, PhysicalShortcut, PluginId, UiPropertyValue, UiPropertyValueToEnum, UiPropertyValueToStruct, UiWidgetId}; use common_ui::shortcut_to_text; use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; @@ -20,8 +21,10 @@ use iced_aw::{floating_element, GridRow}; use itertools::Itertools; use crate::model::UiViewEvent; +use crate::ui::AppMsg; use crate::ui::custom_widgets::loading_bar::LoadingBar; use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::state::PluginViewState; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; use crate::ui::theme::date_picker::DatePickerStyle; @@ -63,19 +66,15 @@ pub enum ComponentWidgetState { }, Detail { show_action_panel: bool, - focused_action_item: ScrollHandle, }, Form { show_action_panel: bool, - focused_action_item: ScrollHandle, }, List { show_action_panel: bool, - focused_action_item: ScrollHandle, }, Grid { show_action_panel: bool, - focused_action_item: ScrollHandle, }, None } @@ -110,19 +109,15 @@ impl ComponentWidgetState { }, ComponentWidget::Detail { .. } => ComponentWidgetState::Detail { show_action_panel: false, - focused_action_item: ScrollHandle::new(), }, ComponentWidget::Form { .. } => ComponentWidgetState::Form { show_action_panel: false, - focused_action_item: ScrollHandle::new(), }, ComponentWidget::List { .. } => ComponentWidgetState::List { show_action_panel: false, - focused_action_item: ScrollHandle::new(), }, ComponentWidget::Grid { .. } => ComponentWidgetState::Grid { show_action_panel: false, - focused_action_item: ScrollHandle::new(), }, _ => ComponentWidgetState::None } @@ -151,6 +146,7 @@ pub enum ComponentRenderContext { widget_id: UiWidgetId }, Root { + plugin_view_state: PluginViewState, entrypoint_name: String, action_shortcuts: HashMap, }, @@ -200,7 +196,7 @@ impl ComponentWidgetWrapper { .map(|widget| widget.clone()) } - pub fn show_action_panel(&self) { + pub fn toggle_action_panel(&self) { { let (_, ref mut state) = &mut *self.get_mut(); @@ -224,7 +220,31 @@ impl ComponentWidgetWrapper { self.get_children() .unwrap_or(vec![]) .iter() - .for_each(|child| child.show_action_panel()); + .for_each(|child| child.toggle_action_panel()); + } + + pub fn get_all_widgets(&self) -> Vec { + let mut result: Vec<_> = self.get_children() + .unwrap_or(vec![]) + .iter() + .flat_map(|component| component.get_all_widgets()) + .collect(); + + result.push(self.clone()); + + result + } + + pub fn get_action_ids(&self) -> Vec { + self.get_all_widgets() + .into_iter() + .filter(|component| { + let (widget, _) = &*component.get(); + + matches!(widget, ComponentWidget::Action { .. }) + }) + .map(|component| component.id) + .collect() } fn get(&self) -> RwLockReadGuard<'_, (ComponentWidget, ComponentWidgetState)> { @@ -419,7 +439,7 @@ impl ComponentWidgetWrapper { } } ComponentWidget::Detail { children, isLoading: is_loading } => { - let ComponentWidgetState::Detail { show_action_panel, ref focused_action_item } = *state else { + let ComponentWidgetState::Detail { show_action_panel } = *state else { panic!("unexpected state kind {:?}", state) }; @@ -489,7 +509,7 @@ impl ComponentWidgetWrapper { if is_in_list { content } else { - render_plugin_root(show_action_panel, &focused_action_item, widget_id, children, content, context, is_loading.unwrap_or(false)) + render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } } ComponentWidget::Root { children } => { @@ -600,7 +620,7 @@ impl ComponentWidgetWrapper { .into() } ComponentWidget::Form { children, isLoading: is_loading } => { - let ComponentWidgetState::Form { show_action_panel, ref focused_action_item } = *state else { + let ComponentWidgetState::Form { show_action_panel } = *state else { panic!("unexpected state kind {:?}", state) }; @@ -676,7 +696,7 @@ impl ComponentWidgetWrapper { .width(Length::Fill) .themed(ContainerStyle::Form); - render_plugin_root(show_action_panel, &focused_action_item, widget_id, children, content, context, is_loading.unwrap_or(false)) + render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } ComponentWidget::InlineSeparator { icon } => { match icon { @@ -953,7 +973,7 @@ impl ComponentWidgetWrapper { render_section(content, Some(title), subtitle, RowStyle::ListSectionTitle, TextStyle::ListSectionTitle, TextStyle::ListSectionSubtitle) } ComponentWidget::List { children, isLoading: is_loading } => { - let ComponentWidgetState::List { show_action_panel, ref focused_action_item } = *state else { + let ComponentWidgetState::List { show_action_panel } = *state else { panic!("unexpected state kind {:?}", state) }; @@ -1038,7 +1058,7 @@ impl ComponentWidgetWrapper { .height(Length::Fill) .into(); - render_plugin_root(show_action_panel, &focused_action_item, widget_id, children, content, context, is_loading.unwrap_or(false)) + render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } ComponentWidget::GridItem { children, title, subtitle } => { // TODO should be just one @@ -1101,7 +1121,7 @@ impl ComponentWidgetWrapper { render_section(content, Some(title), subtitle, RowStyle::GridSectionTitle, TextStyle::GridSectionTitle, TextStyle::GridSectionSubtitle) } ComponentWidget::Grid { children, columns, isLoading: is_loading } => { - let ComponentWidgetState::Grid { show_action_panel, ref focused_action_item } = *state else { + let ComponentWidgetState::Grid { show_action_panel } = *state else { panic!("unexpected state kind {:?}", state) }; @@ -1152,7 +1172,7 @@ impl ComponentWidgetWrapper { .width(Length::Fill) .themed(ContainerStyle::Grid); - render_plugin_root(show_action_panel, &focused_action_item, widget_id, children, content, context, is_loading.unwrap_or(false)) + render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } } } @@ -1284,16 +1304,15 @@ fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option< .into() } -fn render_plugin_root<'a, ACTION>( +fn render_plugin_root<'a>( show_action_panel: bool, - action_panel_scroll_handle: &ScrollHandle, widget_id: UiWidgetId, children: &[ComponentWidgetWrapper], content: Element<'a, ComponentWidgetEvent>, context: ComponentRenderContext, is_loading: bool ) -> Element<'a, ComponentWidgetEvent> { - let ComponentRenderContext::Root { entrypoint_name, action_shortcuts } = context else { + let ComponentRenderContext::Root { entrypoint_name, action_shortcuts, plugin_view_state } = context else { panic!("not supposed to be passed to root item: {:?}", context) }; @@ -1307,18 +1326,36 @@ fn render_plugin_root<'a, ACTION>( .into() }; - render_root( - show_action_panel, - top_panel, - top_separator, - content, - None, - convert_action_panel(children, action_shortcuts), - Some(action_panel_scroll_handle), - entrypoint_name, - || ComponentWidgetEvent::ToggleActionPanel { widget_id }, - |widget_id| ComponentWidgetEvent::ActionClick { widget_id } - ) + match plugin_view_state { + PluginViewState::None => { + render_root( + show_action_panel, + top_panel, + top_separator, + content, + None, // TODO + convert_action_panel(children, action_shortcuts), + None::<&ScrollHandle>, + entrypoint_name, + || ComponentWidgetEvent::ToggleActionPanel { widget_id }, + |widget_id| ComponentWidgetEvent::ActionClick { widget_id } + ) + } + PluginViewState::ActionPanel { focused_action_item } => { + render_root( + show_action_panel, + top_panel, + top_separator, + content, + None, // TODO + convert_action_panel(children, action_shortcuts), + Some(&focused_action_item), + entrypoint_name, + || ComponentWidgetEvent::ToggleActionPanel { widget_id }, + |widget_id| ComponentWidgetEvent::ActionClick { widget_id } + ) + } + } } pub struct ActionPanel { @@ -1405,7 +1442,8 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( title: Option, items: Vec, action_panel_scroll_handle: &ScrollHandle, - on_action: &dyn Fn(UiWidgetId) -> T + on_action: &dyn Fn(UiWidgetId) -> T, + index_counter: &Cell ) -> Vec> { let mut columns = vec![]; @@ -1455,13 +1493,14 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( .into() }; - // TODO broken for plugin views - let style = if action_panel_scroll_handle.index == widget_id { + let style = if action_panel_scroll_handle.index == index_counter.get() { ButtonStyle::ActionFocused } else { ButtonStyle::Action }; + index_counter.set(index_counter.get() + 1); + let content = button(content) .on_press(on_action(widget_id)) .width(Length::Fill) @@ -1475,7 +1514,7 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( columns.push(separator); - let content = render_action_panel_items(title, items, action_panel_scroll_handle, on_action); + let content = render_action_panel_items(title, items, action_panel_scroll_handle, on_action, index_counter); for content in content { columns.push(content); @@ -1494,7 +1533,7 @@ fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T, ACTION>( on_action: F, action_panel_scroll_handle: &ScrollHandle, ) -> Element<'a, T> { - let columns = render_action_panel_items(action_panel.title, action_panel.items, action_panel_scroll_handle, &on_action); + let columns = render_action_panel_items(action_panel.title, action_panel.items, action_panel_scroll_handle, &on_action, &Cell::new(0)); let actions: Element<_> = column(columns) .into(); @@ -1926,19 +1965,9 @@ impl ComponentWidgetEvent { Some(create_password_field_on_change_event(widget_id, Some(value))) } ComponentWidgetEvent::ToggleActionPanel { .. } => { - let (widget, ref mut state) = &mut *widget.get_mut(); - - let show_action_panel = match state { - ComponentWidgetState::Detail { show_action_panel, .. } => show_action_panel, - ComponentWidgetState::Form { show_action_panel, .. } => show_action_panel, - ComponentWidgetState::List { show_action_panel, .. } => show_action_panel, - ComponentWidgetState::Grid { show_action_panel, .. } => show_action_panel, - _ => panic!("unexpected state kind, widget: {:?} state: {:?}", widget, state) - }; - - *show_action_panel = !*show_action_panel; - - None + Some(UiViewEvent::AppEvent { + event: AppMsg::ToggleActionPanel + }) } ComponentWidgetEvent::ListItemClick { widget_id } => { Some(create_list_item_on_click_event(widget_id)) diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 4363141..0185148 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -1,4 +1,4 @@ -use common::model::{EntrypointId, PluginId, UiWidget}; +use common::model::{EntrypointId, PluginId, UiWidget, UiWidgetId}; use crate::model::UiViewEvent; use crate::ui::theme::Element; @@ -79,7 +79,11 @@ impl PluginWidgetContainer { event.handle(plugin_id, widget) } - pub fn show_action_panel(&self) { - self.root_widget.show_action_panel() + pub fn toggle_action_panel(&self) { + self.root_widget.toggle_action_panel() + } + + pub fn get_action_ids(&self) -> Vec { + self.root_widget.get_action_ids() } } From 1e2877b057208d81d8b5361fc8599992c2c3a4b6 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:26:21 +0200 Subject: [PATCH 095/540] Fix alt + k not working in main search view --- Cargo.lock | 24 +++++------ rust/client/src/ui/mod.rs | 88 +++++++++++++++++++-------------------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1c21a2b..593e9c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4061,7 +4061,7 @@ dependencies = [ [[package]] name = "iced" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#99c14c8f5dea9c4429d74fb4f6b5290c0f43fabc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" dependencies = [ "iced_core", "iced_futures", @@ -4090,7 +4090,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#99c14c8f5dea9c4429d74fb4f6b5290c0f43fabc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" dependencies = [ "bitflags 2.5.0", "glam", @@ -4108,7 +4108,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#99c14c8f5dea9c4429d74fb4f6b5290c0f43fabc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" dependencies = [ "futures", "iced_core", @@ -4121,7 +4121,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#99c14c8f5dea9c4429d74fb4f6b5290c0f43fabc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" dependencies = [ "bitflags 2.5.0", "bytemuck", @@ -4143,7 +4143,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#99c14c8f5dea9c4429d74fb4f6b5290c0f43fabc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -4155,7 +4155,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#99c14c8f5dea9c4429d74fb4f6b5290c0f43fabc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" dependencies = [ "iced_core", "iced_futures", @@ -4167,7 +4167,7 @@ dependencies = [ [[package]] name = "iced_sctk" version = "0.1.0" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#99c14c8f5dea9c4429d74fb4f6b5290c0f43fabc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" dependencies = [ "enum-repr", "float-cmp", @@ -4193,7 +4193,7 @@ dependencies = [ [[package]] name = "iced_style" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#99c14c8f5dea9c4429d74fb4f6b5290c0f43fabc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" dependencies = [ "iced_core", "once_cell", @@ -4213,7 +4213,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#99c14c8f5dea9c4429d74fb4f6b5290c0f43fabc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" dependencies = [ "bytemuck", "cosmic-text", @@ -4229,7 +4229,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#99c14c8f5dea9c4429d74fb4f6b5290c0f43fabc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" dependencies = [ "bitflags 2.5.0", "bytemuck", @@ -4246,7 +4246,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#99c14c8f5dea9c4429d74fb4f6b5290c0f43fabc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" dependencies = [ "iced_renderer", "iced_runtime", @@ -4260,7 +4260,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#99c14c8f5dea9c4429d74fb4f6b5290c0f43fabc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" dependencies = [ "iced_graphics", "iced_runtime", diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 2177ea1..d362a76 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -10,7 +10,7 @@ use iced::advanced::layout::Limits; use iced::multi_window::Application; use iced::futures::channel::mpsc::Sender; use iced::futures::SinkExt; -use iced::keyboard::Key; +use iced::keyboard::{Key, Modifiers}; use iced::keyboard::key::Named; use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, Space}; use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; @@ -478,27 +478,28 @@ impl Application for AppModel { // for main view, also fired in cases where main text field is not focused self.global_state.enter(&self.search_results) }, - Key::Named(Named::Backspace) => self.backspace_prompt(), + Key::Named(Named::Backspace) => { + match &mut self.global_state { + GlobalState::MainView { sub_state, search_field_id, prompt, .. } => { + match sub_state { + MainViewState::None => Self::backspace_prompt(prompt, search_field_id.clone()), + MainViewState::ActionPanel { .. } => Command::none() + } + } + GlobalState::ErrorView { .. } => Command::none(), + GlobalState::PluginView { .. } => Command::none() + } + }, _ => { - match &self.global_state { - GlobalState::MainView { sub_state, .. } => { + match &mut self.global_state { + GlobalState::MainView { sub_state, search_field_id, prompt, .. } => { match sub_state { MainViewState::None => { match physical_key_model(physical_key, modifiers) { Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { Command::perform(async {}, |_| AppMsg::ToggleActionPanel) } - _ => { - match text { - Some(text) => { - self.append_prompt(text.to_string()) - } - None => { - Command::none() - } - } - } - } + _ => Self::append_prompt(prompt, text, search_field_id.clone(), modifiers) } } MainViewState::ActionPanel { .. } => { match physical_key_model(physical_key, modifiers) { @@ -697,8 +698,10 @@ impl Application for AppModel { GlobalState::MainView { sub_state, focused_search_result, .. } => { match sub_state { MainViewState::None => { - if let Some(_) = focused_search_result.get(&self.search_results) { - MainViewState::action_panel(sub_state); + if let Some(search_item) = focused_search_result.get(&self.search_results) { + if search_item.entrypoint_actions.len() > 0 { + MainViewState::action_panel(sub_state); + } } } MainViewState::ActionPanel { .. } => { @@ -967,6 +970,7 @@ impl Application for AppModel { let input: Element<_> = text_input("Search...", prompt) .on_input(AppMsg::PromptChanged) .on_submit(AppMsg::PromptSubmit) + .ignore_with_modifiers(true) .id(search_field_id.clone()) .width(Length::Fill) .themed(TextInputStyle::MainSearch); @@ -1134,14 +1138,6 @@ impl Application for AppModel { event::Status::Ignored => Some(AppMsg::IcedEvent(event)), event::Status::Captured => match &event { Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Named(Named::Escape), .. }) => Some(AppMsg::IcedEvent(event)), - Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Character(char), modifiers, .. }) => { - if char == "k" && modifiers.alt() { - // TODO this still enters "k" into a search bar which is undesirable - Some(AppMsg::IcedEvent(event)) - } else { - None - } - }, _ => None } }); @@ -1377,31 +1373,35 @@ impl AppModel { Ok(()) }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } +} - fn append_prompt(&mut self, value: String) -> Command { - match &mut self.global_state { - GlobalState::MainView { search_field_id, prompt, .. } => { - *prompt = format!("{}{}", prompt, value); - - focus(search_field_id.clone()) +// these are needed to force focus the text_input in main search view when +// the window is opened but text_input not focused +impl AppModel { + fn append_prompt(prompt: &mut String, value: Option, search_field_id: text_input::Id, modifiers: Modifiers) -> Command { + if modifiers.control() || modifiers.alt() || modifiers.logo() { + Command::none() + } else { + match value { + Some(value) => { + if let Some(value) = value.chars().next().filter(|c| !c.is_control()) { + *prompt = format!("{}{}", prompt, value); + focus(search_field_id.clone()) + } else { + Command::none() + } + } + None => Command::none() } - GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { .. } => Command::none(), } } - fn backspace_prompt(&mut self) -> Command { - match &mut self.global_state { - GlobalState::MainView { search_field_id, prompt, .. } => { - let mut chars = prompt.chars(); - chars.next_back(); - *prompt = chars.as_str().to_owned(); + fn backspace_prompt(prompt: &mut String, search_field_id: text_input::Id) -> Command { + let mut chars = prompt.chars(); + chars.next_back(); + *prompt = chars.as_str().to_owned(); - focus(search_field_id.clone()) - } - GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { .. } => Command::none(), - } + focus(search_field_id.clone()) } } From 904a34a83d37e0c7f3480d31223ba785800be781 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:57:05 +0200 Subject: [PATCH 096/540] Implement running action via enter in plugin view --- rust/client/src/ui/mod.rs | 57 +++++++++++++++++++------ rust/client/src/ui/state/main_view.rs | 21 ++++++++- rust/client/src/ui/state/mod.rs | 45 ++++++------------- rust/client/src/ui/state/plugin_view.rs | 23 ++++++++-- rust/client/src/ui/widget.rs | 20 +++++---- 5 files changed, 108 insertions(+), 58 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index d362a76..6970140 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -729,22 +729,46 @@ impl Application for AppModel { Command::none() } AppMsg::OnEntrypointAction(widget_id) => { - match &self.global_state { - GlobalState::MainView { focused_search_result, .. } => { - if let Some(search_item) = focused_search_result.get(&self.search_results) { - let search_item = search_item.clone(); - if widget_id == 0 { - Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) - } else { - Command::perform(async {}, move |_| AppMsg::RunSearchItemAction(search_item, Some(widget_id - 1))) + match &mut self.global_state { + GlobalState::MainView { focused_search_result, sub_state, .. } => { + match sub_state { + MainViewState::None => Command::none(), + MainViewState::ActionPanel { .. } => { + if let Some(search_item) = focused_search_result.get(&self.search_results) { + MainViewState::initial(sub_state); + + let search_item = search_item.clone(); + if widget_id == 0 { + Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) + } else { + Command::perform(async {}, move |_| AppMsg::RunSearchItemAction(search_item, Some(widget_id - 1))) + } + } else { + Command::none() + } } - } else { - Command::none() } } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { .. } => { - todo!() + GlobalState::PluginView { client_context, sub_state, .. } => { + match sub_state { + PluginViewState::None => Command::none(), + PluginViewState::ActionPanel { .. } => { + let client_context = client_context.read().expect("lock is poisoned"); + + let plugin_id = client_context.get_view_plugin_id(); + + let widget_event = ComponentWidgetEvent::RunAction { + widget_id, + }; + let render_location = UiRenderLocation::View; + + Command::batch([ + Command::perform(async {}, |_| AppMsg::ToggleActionPanel), + Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + ]) + } + } } } } @@ -1308,16 +1332,21 @@ impl AppModel { Command::perform(async move { let event = { let client_context = client_context.read().expect("lock is poisoned"); - client_context.handle_event(render_location, &plugin_id, widget_event) + client_context.handle_event(render_location, &plugin_id, widget_event.clone()) }; if let Some(event) = event { match event { UiViewEvent::View { widget_id, event_name, event_arguments } => { + let msg = match widget_event { + ComponentWidgetEvent::ActionClick { .. } => AppMsg::ToggleActionPanel, + _ => AppMsg::Noop + }; + backend_client.send_view_event(plugin_id, widget_id, event_name, event_arguments) .await?; - Ok(AppMsg::Noop) + Ok(msg) } UiViewEvent::Open { href } => { backend_client.send_open_event(plugin_id, href) diff --git a/rust/client/src/ui/state/main_view.rs b/rust/client/src/ui/state/main_view.rs index 7882fa3..677d419 100644 --- a/rust/client/src/ui/state/main_view.rs +++ b/rust/client/src/ui/state/main_view.rs @@ -30,11 +30,28 @@ impl MainViewState { impl Focus for MainViewState { fn enter(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { - todo!() + match self { + MainViewState::None => { + panic!("invalid state") + } + MainViewState::ActionPanel { focused_action_item } => { + let widget_id = focused_action_item.index; + + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) + } + } } fn escape(&mut self) -> Command { - todo!() + match self { + MainViewState::None => { + Command::perform(async {}, |_| AppMsg::HideWindow) + } + MainViewState::ActionPanel { .. } => { + MainViewState::initial(self); + Command::none() + } + } } fn tab(&mut self) -> Command { diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 3a24786..723173d 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -133,35 +133,26 @@ impl Focus for GlobalState { fn enter(&mut self, focus_list: &[SearchResult]) -> Command { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { - match sub_state { - MainViewState::None => { - if let Some(search_item) = focused_search_result.get(focus_list) { + if let Some(search_item) = focused_search_result.get(focus_list) { + match sub_state { + MainViewState::None => { let search_item = search_item.clone(); Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) - } else { - Command::none() + } + MainViewState::ActionPanel { .. } => { + sub_state.enter(&search_item.entrypoint_actions) } } - MainViewState::ActionPanel { focused_action_item, .. } => { - let widget_id = focused_action_item.index; - - MainViewState::initial(sub_state); - - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) - } + } else { + Command::none() } } - GlobalState::PluginView { sub_state, .. } => { - match sub_state { - PluginViewState::None => Command::none(), - PluginViewState::ActionPanel { focused_action_item, .. } => { - let widget_id = focused_action_item.index; + GlobalState::PluginView { sub_state, client_context, .. } => { + let client_context = client_context.read().expect("lock is poisoned"); - PluginViewState::initial(sub_state); + let action_ids = client_context.get_action_ids(); - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) - } - } + sub_state.enter(&action_ids) } GlobalState::ErrorView { .. } => Command::none() } @@ -170,15 +161,7 @@ impl Focus for GlobalState { fn escape(&mut self) -> Command { match self { GlobalState::MainView { sub_state, .. } => { - match sub_state { - MainViewState::None => { - Command::perform(async {}, |_| AppMsg::HideWindow) - } - MainViewState::ActionPanel { .. } => { - MainViewState::initial(sub_state); - Command::none() - } - } + sub_state.escape() } GlobalState::PluginView { plugin_view_data: PluginViewData { @@ -206,7 +189,7 @@ impl Focus for GlobalState { } } PluginViewState::ActionPanel { .. } => { - Command::perform(async {}, |_| AppMsg::ToggleActionPanel) + sub_state.escape() } } } diff --git a/rust/client/src/ui/state/plugin_view.rs b/rust/client/src/ui/state/plugin_view.rs index 7aa7d34..a17db0b 100644 --- a/rust/client/src/ui/state/plugin_view.rs +++ b/rust/client/src/ui/state/plugin_view.rs @@ -30,12 +30,29 @@ impl PluginViewState { } impl Focus for PluginViewState { - fn enter(&mut self, _focus_list: &[UiWidgetId]) -> Command { - todo!() + fn enter(&mut self, focus_list: &[UiWidgetId]) -> Command { + match self { + PluginViewState::None => Command::none(), + PluginViewState::ActionPanel { focused_action_item, .. } => { + if let Some(widget_id) = focused_action_item.get(focus_list) { + let widget_id = *widget_id; + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) + } else { + Command::none() + } + } + } } fn escape(&mut self) -> Command { - todo!() + match self { + PluginViewState::None => { + panic!("invalid state") + } + PluginViewState::ActionPanel { .. } => { + Command::perform(async {}, |_| AppMsg::ToggleActionPanel) + } + } } fn tab(&mut self) -> Command { diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 88627d8..b50292c 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1442,7 +1442,7 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( title: Option, items: Vec, action_panel_scroll_handle: &ScrollHandle, - on_action: &dyn Fn(UiWidgetId) -> T, + on_action_click: &dyn Fn(UiWidgetId) -> T, index_counter: &Cell ) -> Vec> { let mut columns = vec![]; @@ -1502,7 +1502,7 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( index_counter.set(index_counter.get() + 1); let content = button(content) - .on_press(on_action(widget_id)) + .on_press(on_action_click(widget_id)) .width(Length::Fill) .themed(style); @@ -1514,7 +1514,7 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( columns.push(separator); - let content = render_action_panel_items(title, items, action_panel_scroll_handle, on_action, index_counter); + let content = render_action_panel_items(title, items, action_panel_scroll_handle, on_action_click, index_counter); for content in content { columns.push(content); @@ -1530,10 +1530,10 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T, ACTION>( action_panel: ActionPanel, - on_action: F, + on_action_click: F, action_panel_scroll_handle: &ScrollHandle, ) -> Element<'a, T> { - let columns = render_action_panel_items(action_panel.title, action_panel.items, action_panel_scroll_handle, &on_action, &Cell::new(0)); + let columns = render_action_panel_items(action_panel.title, action_panel.items, action_panel_scroll_handle, &on_action_click, &Cell::new(0)); let actions: Element<_> = column(columns) .into(); @@ -1557,7 +1557,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( action_panel_scroll_handle: Option<&ScrollHandle>, entrypoint_name: String, on_panel_toggle: impl Fn() -> T, - on_action: impl Fn(UiWidgetId) -> T, + on_action_click: impl Fn(UiWidgetId) -> T, ) -> Element<'a, T> { let entrypoint_name: Element<_> = text(entrypoint_name) .into(); @@ -1675,7 +1675,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( }; let action_panel_element = match (action_panel, action_panel_scroll_handle) { - (Some(action_panel), Some(action_panel_scroll_handle)) => render_action_panel(action_panel, on_action, action_panel_scroll_handle), + (Some(action_panel), Some(action_panel_scroll_handle)) => render_action_panel(action_panel, on_action_click, action_panel_scroll_handle), _ => Space::with_height(1).into(), }; @@ -1834,6 +1834,9 @@ pub enum ComponentWidgetEvent { ActionClick { widget_id: UiWidgetId, }, + RunAction { + widget_id: UiWidgetId + }, ToggleDatePicker { widget_id: UiWidgetId, }, @@ -1883,7 +1886,7 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::TagClick { widget_id } => { Some(create_metadata_tag_item_on_click_event(widget_id)) } - ComponentWidgetEvent::ActionClick { widget_id } => { + ComponentWidgetEvent::RunAction { widget_id } | ComponentWidgetEvent::ActionClick { widget_id } => { Some(create_action_on_action_event(widget_id)) } ComponentWidgetEvent::ToggleDatePicker { .. } => { @@ -1985,6 +1988,7 @@ impl ComponentWidgetEvent { match self { ComponentWidgetEvent::LinkClick { widget_id, .. } => widget_id, ComponentWidgetEvent::ActionClick { widget_id, .. } => widget_id, + ComponentWidgetEvent::RunAction { widget_id, .. } => widget_id, ComponentWidgetEvent::TagClick { widget_id, .. } => widget_id, ComponentWidgetEvent::ToggleDatePicker { widget_id, .. } => widget_id, ComponentWidgetEvent::SubmitDatePicker { widget_id, .. } => widget_id, From 71b110aa9ecf0f76ab573b621aec09e8e5e5404c Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:27:20 +0200 Subject: [PATCH 097/540] Implement visual part of primary and secondary actions --- rust/client/src/ui/mod.rs | 34 ++++++--- rust/client/src/ui/theme/container.rs | 6 +- rust/client/src/ui/theme/mod.rs | 4 +- rust/client/src/ui/theme/row.rs | 4 +- rust/client/src/ui/theme/rule.rs | 4 +- rust/client/src/ui/theme/text.rs | 6 +- rust/client/src/ui/widget.rs | 100 ++++++++++++++++++++------ 7 files changed, 113 insertions(+), 45 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 6970140..fdb41d9 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -38,7 +38,7 @@ use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::theme::container::ContainerStyle; use crate::ui::theme::text_input::TextInputStyle; use crate::ui::view_container::view_container; -use crate::ui::widget::{render_root, ActionPanel, ActionPanelItems, ComponentWidgetEvent}; +use crate::ui::widget::{render_root, ActionPanel, ActionPanelItem, ComponentWidgetEvent}; mod view_container; mod search_list; @@ -1031,7 +1031,7 @@ impl Application for AppModel { list, ]).into(); - let (default_action, action_panel) = if let Some(search_item) = focused_search_result.get(&self.search_results) { + let (primary_action, action_panel) = if let Some(search_item) = focused_search_result.get(&self.search_results) { let label = match search_item.entrypoint_type { SearchResultEntrypointType::Command => "Run Command", SearchResultEntrypointType::View => "Open View", @@ -1049,23 +1049,37 @@ impl Application for AppModel { let mut actions: Vec<_> = search_item.entrypoint_actions .iter() .enumerate() - .map(|(index, action)| ActionPanelItems::Action { - label: action.label.clone(), - widget_id: index + 1, - physical_shortcut: action.shortcut.clone(), + .map(|(index, action)| { + let physical_shortcut = if index == 0 { + Some(PhysicalShortcut { // secondary action + physical_key: PhysicalKey::Enter, + modifier_shift: true, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }) + } else { + action.shortcut.clone() + }; + + ActionPanelItem::Action { + label: action.label.clone(), + widget_id: index + 1, + physical_shortcut, + } }) .collect(); if actions.len() == 0 { (Some((label, default_shortcut)), None) } else { - let default_action = ActionPanelItems::Action { + let primary_action = ActionPanelItem::Action { label: label.clone(), widget_id: 0, physical_shortcut: Some(default_shortcut.clone()), }; - actions.insert(0, default_action); + actions.insert(0, primary_action); let action_panel = ActionPanel { title: Some(search_item.entrypoint_name.clone()), @@ -1085,7 +1099,7 @@ impl Application for AppModel { input, separator, content, - default_action, + primary_action, action_panel, None::<&ScrollHandle>, "".to_string(), @@ -1099,7 +1113,7 @@ impl Application for AppModel { input, separator, content, - default_action, + primary_action, action_panel, Some(focused_action_item), "".to_string(), diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 4f6e216..3c1bc1c 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -44,7 +44,7 @@ pub enum ContainerStyle { PreferenceRequiredViewDescription, Root, RootBottomPanel, - RootBottomPanelDefaultActionText, + RootBottomPanelPrimaryActionText, RootBottomPanelActionToggleText, RootInner, RootTopPanel, @@ -383,8 +383,8 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau ContainerStyle::RootBottomPanelActionToggleText => { self.padding(theme.root_bottom_panel_action_toggle_text.padding.to_iced()) } - ContainerStyle::RootBottomPanelDefaultActionText => { - self.padding(theme.root_bottom_panel_default_action_text.padding.to_iced()) + ContainerStyle::RootBottomPanelPrimaryActionText => { + self.padding(theme.root_bottom_panel_primary_action_text.padding.to_iced()) } ContainerStyle::TextAccessory => { self.padding(theme.text_accessory.padding.to_iced()) diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 2b480f6..eb55d1a 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -112,7 +112,7 @@ pub struct GauntletTheme { root_bottom_panel: ThemeBottomPanel, root_bottom_panel_action_toggle_button: ThemeButton, root_bottom_panel_action_toggle_text: ThemePaddingTextColor, - root_bottom_panel_default_action_text: ThemePaddingTextColor, + root_bottom_panel_primary_action_text: ThemePaddingTextColor, root_content: ThemePaddingOnly, root_top_panel: ThemePaddingOnly, root_top_panel_button: ThemeButton, @@ -407,7 +407,7 @@ impl GauntletTheme { padding: padding(0.0, 8.0, 0.0, 4.0), text_color: text_lighter_color }, - root_bottom_panel_default_action_text: ThemePaddingTextColor { + root_bottom_panel_primary_action_text: ThemePaddingTextColor { padding: padding(0.0, 8.0, 0.0, 4.0), text_color: text_lightest_color }, diff --git a/rust/client/src/ui/theme/row.rs b/rust/client/src/ui/theme/row.rs index 382f77d..4a88c5e 100644 --- a/rust/client/src/ui/theme/row.rs +++ b/rust/client/src/ui/theme/row.rs @@ -9,7 +9,7 @@ pub enum RowStyle { GridSectionTitle, GridItemTitle, RootBottomPanel, - RootBottomPanelDefaultAction, + RootBottomPanelPrimaryAction, } impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletTheme, Renderer> { @@ -39,7 +39,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletT RowStyle::RootBottomPanel => { self.spacing(theme.root_bottom_panel.spacing) } - RowStyle::RootBottomPanelDefaultAction => { + RowStyle::RootBottomPanelPrimaryAction => { self.padding(Padding::from([0.0, theme.root_bottom_panel.spacing, 0.0, 0.0])) } }.into() diff --git a/rust/client/src/ui/theme/rule.rs b/rust/client/src/ui/theme/rule.rs index 4a659a6..b316614 100644 --- a/rust/client/src/ui/theme/rule.rs +++ b/rust/client/src/ui/theme/rule.rs @@ -8,7 +8,7 @@ pub enum RuleStyle { #[default] Default, ActionPanel, - DefaultActionSeparator, + PrimaryActionSeparator, } impl rule::StyleSheet for GauntletTheme { @@ -34,7 +34,7 @@ impl rule::StyleSheet for GauntletTheme { fill_mode: rule::FillMode::Percent(96.0), } } - RuleStyle::DefaultActionSeparator => { + RuleStyle::PrimaryActionSeparator => { Appearance { color: theme.separator.color.to_iced(), width: 1, diff --git a/rust/client/src/ui/theme/text.rs b/rust/client/src/ui/theme/text.rs index c19748d..1615864 100644 --- a/rust/client/src/ui/theme/text.rs +++ b/rust/client/src/ui/theme/text.rs @@ -23,7 +23,7 @@ pub enum TextStyle { GridItemSubTitle, InlineName, InlineSeparator, - RootBottomPanelDefaultActionText, + RootBottomPanelPrimaryActionText, RootBottomPanelActionToggleText, } @@ -100,8 +100,8 @@ impl text::StyleSheet for GauntletTheme { TextStyle::InlineSeparator => Appearance { color: Some(self.inline_separator.text_color.to_iced()), }, - TextStyle::RootBottomPanelDefaultActionText => Appearance { - color: Some(self.root_bottom_panel_default_action_text.text_color.to_iced()), + TextStyle::RootBottomPanelPrimaryActionText => Appearance { + color: Some(self.root_bottom_panel_primary_action_text.text_color.to_iced()), }, TextStyle::RootBottomPanelActionToggleText => Appearance { color: Some(self.root_bottom_panel_action_toggle_text.text_color.to_iced()), diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index b50292c..ae5f26a 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1326,6 +1326,42 @@ fn render_plugin_root<'a>( .into() }; + let mut action_panel = convert_action_panel(children, action_shortcuts); + + let primary_action = action_panel.as_mut() + .map(|panel| { + fn find_first(items: &[ActionPanelItem]) -> Option { + for item in items { + match item { + ActionPanelItem::Action { label, .. } => { + return Some(label.to_string()) + } + ActionPanelItem::ActionSection { items, .. } => { + if let Some(item) = find_first(items) { + return Some(item) + } + } + } + } + + None + } + + find_first(&panel.items) + }) + .flatten() + .map(|label| { + let shortcut = PhysicalShortcut { + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false + }; + + (label.to_string(), shortcut) + }); + match plugin_view_state { PluginViewState::None => { render_root( @@ -1333,8 +1369,8 @@ fn render_plugin_root<'a>( top_panel, top_separator, content, - None, // TODO - convert_action_panel(children, action_shortcuts), + primary_action, + action_panel, None::<&ScrollHandle>, entrypoint_name, || ComponentWidgetEvent::ToggleActionPanel { widget_id }, @@ -1347,8 +1383,8 @@ fn render_plugin_root<'a>( top_panel, top_separator, content, - None, // TODO - convert_action_panel(children, action_shortcuts), + primary_action, + action_panel, Some(&focused_action_item), entrypoint_name, || ComponentWidgetEvent::ToggleActionPanel { widget_id }, @@ -1360,10 +1396,10 @@ fn render_plugin_root<'a>( pub struct ActionPanel { pub title: Option, - pub items: Vec + pub items: Vec } -pub enum ActionPanelItems { +pub enum ActionPanelItem { Action { label: String, widget_id: UiWidgetId, @@ -1371,7 +1407,7 @@ pub enum ActionPanelItems { }, ActionSection { title: Option, - items: Vec + items: Vec } } @@ -1394,7 +1430,7 @@ fn convert_action_panel(children: &[ComponentWidgetWrapper], action_shortcuts: H match action_panel { ComponentWidget::ActionPanel { children, title } => { - fn convert_to_items(children: &[ComponentWidgetWrapper], action_shortcuts: &HashMap) -> Vec { + fn convert_to_items(children: &[ComponentWidgetWrapper], action_shortcuts: &HashMap) -> Vec { let mut items = vec![]; for child in children { @@ -1408,14 +1444,14 @@ fn convert_action_panel(children: &[ComponentWidgetWrapper], action_shortcuts: H .flatten() .cloned(); - items.push(ActionPanelItems::Action { + items.push(ActionPanelItem::Action { label: label.clone(), widget_id, physical_shortcut, }); } ComponentWidget::ActionPanelSection { children, title } => { - items.push(ActionPanelItems::ActionSection { + items.push(ActionPanelItem::ActionSection { title: title.clone(), items: convert_to_items(children, action_shortcuts), }); @@ -1440,7 +1476,7 @@ fn convert_action_panel(children: &[ComponentWidgetWrapper], action_shortcuts: H fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( title: Option, - items: Vec, + items: Vec, action_panel_scroll_handle: &ScrollHandle, on_action_click: &dyn Fn(UiWidgetId) -> T, index_counter: &Cell @@ -1465,7 +1501,7 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( for item in items { match item { - ActionPanelItems::Action { label, widget_id, physical_shortcut } => { + ActionPanelItem::Action { label, widget_id, physical_shortcut } => { if place_separator { let separator: Element<_> = horizontal_rule(1) .themed(RuleStyle::ActionPanel); @@ -1475,6 +1511,24 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( place_separator = false; } + let physical_shortcut = match index_counter.get() { + 0 => Some(PhysicalShortcut { // primary + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }), + 1 => Some(PhysicalShortcut { // secondary + physical_key: PhysicalKey::Enter, + modifier_shift: true, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }), + _ => physical_shortcut + }; + let shortcut_element: Option> = physical_shortcut.as_ref() .map(|shortcut| render_shortcut(shortcut)); @@ -1508,7 +1562,7 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( columns.push(content); } - ActionPanelItems::ActionSection { title, items } => { + ActionPanelItem::ActionSection { title, items } => { let separator: Element<_> = horizontal_rule(1) .themed(RuleStyle::ActionPanel); @@ -1552,7 +1606,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( top_panel: Element<'a, T>, top_separator: Element<'a, T>, content: Element<'a, T>, - default_action: Option<(String, PhysicalShortcut)>, + primary_action: Option<(String, PhysicalShortcut)>, action_panel: Option, action_panel_scroll_handle: Option<&ScrollHandle>, entrypoint_name: String, @@ -1564,18 +1618,18 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( let panel_height = 16 + 8 + 2; // TODO get value from theme - let default_action = match default_action { + let primary_action = match primary_action { Some((label, shortcut)) => { let label: Element<_> = text(label) - .themed(TextStyle::RootBottomPanelDefaultActionText); + .themed(TextStyle::RootBottomPanelPrimaryActionText); let label: Element<_> = container(label) - .themed(ContainerStyle::RootBottomPanelDefaultActionText); + .themed(ContainerStyle::RootBottomPanelPrimaryActionText); let shortcut = render_shortcut(&shortcut); let content: Element<_> = row(vec![label, shortcut]) - .themed(RowStyle::RootBottomPanelDefaultAction); + .themed(RowStyle::RootBottomPanelPrimaryAction); Some(content) } @@ -1605,11 +1659,11 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( bottom_panel_content.push(space); - if let Some(default_action) = default_action { - bottom_panel_content.push(default_action); + if let Some(primary_action) = primary_action { + bottom_panel_content.push(primary_action); let rule: Element<_> = vertical_rule(1) - .style(RuleStyle::DefaultActionSeparator) + .style(RuleStyle::PrimaryActionSeparator) .into(); let rule: Element<_> = container(rule) @@ -1642,8 +1696,8 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( let mut bottom_panel_content = vec![entrypoint_name, space]; - if let Some(default_action) = default_action { - bottom_panel_content.push(default_action); + if let Some(primary_action) = primary_action { + bottom_panel_content.push(primary_action); } let bottom_panel: Element<_> = row(bottom_panel_content) From 08931e2c808a14b2445e0f955c0d7b56ac52be8f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 4 Oct 2024 19:31:10 +0200 Subject: [PATCH 098/540] Bundle openssl. Fixes crash when it is not present --- Cargo.lock | 18 ++++++++++++++---- rust/server/Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 593e9c6..dcb0447 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3429,9 +3429,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ "bitflags 2.5.0", "libc", @@ -4843,9 +4843,9 @@ dependencies = [ [[package]] name = "libgit2-sys" -version = "0.16.2+1.7.2" +version = "0.17.0+1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" dependencies = [ "cc", "libc", @@ -5855,6 +5855,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-src" +version = "300.3.2+3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.102" @@ -5863,6 +5872,7 @@ checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index e158b8f..2edf69c 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -13,7 +13,7 @@ tantivy = "0.20.2" zstd-sys = "=2.0.9" # TODO REMOVE https://github.com/gyscos/zstd-rs/issues/270 regex = "1.9.3" once_cell = "1.18.0" -git2 = "0.18.3" +git2 = { version = "0.19.0", features = ["vendored-libgit2", "vendored-openssl"] } tempfile = "3" async-stream = "0.3.5" anyhow = { version = "1", features = ["backtrace"] } From 947502e62861c362c28c273dc3e54a0160d5c921 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 4 Oct 2024 20:28:33 +0200 Subject: [PATCH 099/540] Remove not needed macOS entitlement --- assets/macos/entitlements.plist | 4 ---- 1 file changed, 4 deletions(-) diff --git a/assets/macos/entitlements.plist b/assets/macos/entitlements.plist index 1fd5a09..4422ae9 100644 --- a/assets/macos/entitlements.plist +++ b/assets/macos/entitlements.plist @@ -2,10 +2,6 @@ - - com.apple.security.cs.disable-library-validation - - com.apple.security.cs.allow-jit From f07795b76f9621e5b6d2b436cedbb6be8176b8e4 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 4 Oct 2024 21:10:41 +0200 Subject: [PATCH 100/540] Implement primary and secondary shortcuts --- Cargo.lock | 24 +++---- rust/client/src/ui/mod.rs | 88 ++++++++++++++++++------- rust/client/src/ui/state/main_view.rs | 21 +++--- rust/client/src/ui/state/mod.rs | 77 +++++++++++++++------- rust/client/src/ui/state/plugin_view.rs | 42 +++++++++--- 5 files changed, 175 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dcb0447..7246706 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4061,7 +4061,7 @@ dependencies = [ [[package]] name = "iced" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" dependencies = [ "iced_core", "iced_futures", @@ -4090,7 +4090,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" dependencies = [ "bitflags 2.5.0", "glam", @@ -4108,7 +4108,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" dependencies = [ "futures", "iced_core", @@ -4121,7 +4121,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" dependencies = [ "bitflags 2.5.0", "bytemuck", @@ -4143,7 +4143,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -4155,7 +4155,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" dependencies = [ "iced_core", "iced_futures", @@ -4167,7 +4167,7 @@ dependencies = [ [[package]] name = "iced_sctk" version = "0.1.0" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" dependencies = [ "enum-repr", "float-cmp", @@ -4193,7 +4193,7 @@ dependencies = [ [[package]] name = "iced_style" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" dependencies = [ "iced_core", "once_cell", @@ -4213,7 +4213,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" dependencies = [ "bytemuck", "cosmic-text", @@ -4229,7 +4229,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" dependencies = [ "bitflags 2.5.0", "bytemuck", @@ -4246,7 +4246,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" dependencies = [ "iced_renderer", "iced_runtime", @@ -4260,7 +4260,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#6d56c953989e9d6795f761f40e135842d60216d4" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" dependencies = [ "iced_graphics", "iced_runtime", diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index fdb41d9..a8a2771 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -434,7 +434,9 @@ impl Application for AppModel { _ => Command::none() } } - AppMsg::PromptSubmit => self.global_state.enter(&self.search_results), + AppMsg::PromptSubmit => { + self.global_state.primary(&self.search_results) + }, AppMsg::SetSearchResults(new_search_results) => { self.search_results = new_search_results; @@ -471,12 +473,20 @@ impl Application for AppModel { keyboard::Event::KeyPressed { key, modifiers, physical_key, text, .. } => { tracing::debug!("Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", key, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); match key { - Key::Named(Named::ArrowUp) => self.global_state.arrow_up(&self.search_results), - Key::Named(Named::ArrowDown) => self.global_state.arrow_down(&self.search_results), - Key::Named(Named::Escape) => self.global_state.escape(), + Key::Named(Named::ArrowUp) => self.global_state.up(&self.search_results), + Key::Named(Named::ArrowDown) => self.global_state.down(&self.search_results), + Key::Named(Named::Escape) => self.global_state.back(), Key::Named(Named::Enter) => { - // for main view, also fired in cases where main text field is not focused - self.global_state.enter(&self.search_results) + if modifiers.logo() || modifiers.alt() || modifiers.control() { + Command::none() // to avoid not wanted "enter" presses + } else { + if modifiers.shift() { + // for main view, also fired in cases where main text field is not focused + self.global_state.secondary(&self.search_results) + } else { + self.global_state.primary(&self.search_results) + } + } }, Key::Named(Named::Backspace) => { match &mut self.global_state { @@ -552,7 +562,7 @@ impl Application for AppModel { self.hide_window() } AppMsg::IcedEvent(_) => Command::none(), - AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => self.global_state.escape(), + AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => self.global_state.back(), AppMsg::WidgetEvent { widget_event, plugin_id, render_location } => { self.handle_plugin_event(widget_event, plugin_id, render_location) } @@ -619,25 +629,43 @@ impl Application for AppModel { Command::none() } AppMsg::RunSearchItemAction(search_result, action_index) => { - let event = match search_result.entrypoint_type { - SearchResultEntrypointType::Command => AppMsg::RunCommand { - entrypoint_id: search_result.entrypoint_id.clone(), - plugin_id: search_result.plugin_id.clone() + match search_result.entrypoint_type { + SearchResultEntrypointType::Command => { + match action_index { + None => { + let msg = AppMsg::RunCommand { + entrypoint_id: search_result.entrypoint_id.clone(), + plugin_id: search_result.plugin_id.clone() + }; + Command::perform(async {}, |_| msg) + } + Some(_) => Command::none() + } }, - SearchResultEntrypointType::View => AppMsg::OpenView { - plugin_id: search_result.plugin_id.clone(), - plugin_name: search_result.plugin_name.clone(), - entrypoint_id: search_result.entrypoint_id.clone(), - entrypoint_name: search_result.entrypoint_name.clone(), + SearchResultEntrypointType::View => { + match action_index { + None => { + let msg = AppMsg::OpenView { + plugin_id: search_result.plugin_id.clone(), + plugin_name: search_result.plugin_name.clone(), + entrypoint_id: search_result.entrypoint_id.clone(), + entrypoint_name: search_result.entrypoint_name.clone(), + }; + Command::perform(async {}, |_| msg) + } + Some(_) => Command::none() + } }, - SearchResultEntrypointType::GeneratedCommand => AppMsg::RunGeneratedCommandEvent { - entrypoint_id: search_result.entrypoint_id.clone(), - plugin_id: search_result.plugin_id.clone(), - action_index, - }, - }; + SearchResultEntrypointType::GeneratedCommand => { + let msg = AppMsg::RunGeneratedCommandEvent { + entrypoint_id: search_result.entrypoint_id.clone(), + plugin_id: search_result.plugin_id.clone(), + action_index, + }; - Command::perform(async {}, |_| event) + Command::perform(async {}, |_| msg) + }, + } } AppMsg::Screenshot { save_path } => { println!("Creating screenshot at: {}", save_path); @@ -752,7 +780,19 @@ impl Application for AppModel { GlobalState::ErrorView { .. } => Command::none(), GlobalState::PluginView { client_context, sub_state, .. } => { match sub_state { - PluginViewState::None => Command::none(), + PluginViewState::None => { + let client_context = client_context.read().expect("lock is poisoned"); + + let plugin_id = client_context.get_view_plugin_id(); + + let widget_event = ComponentWidgetEvent::RunAction { + widget_id, + }; + + let render_location = UiRenderLocation::View; + + Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + }, PluginViewState::ActionPanel { .. } => { let client_context = client_context.read().expect("lock is poisoned"); diff --git a/rust/client/src/ui/state/main_view.rs b/rust/client/src/ui/state/main_view.rs index 677d419..21f71b5 100644 --- a/rust/client/src/ui/state/main_view.rs +++ b/rust/client/src/ui/state/main_view.rs @@ -29,7 +29,7 @@ impl MainViewState { } impl Focus for MainViewState { - fn enter(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { + fn primary(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { match self { MainViewState::None => { panic!("invalid state") @@ -42,7 +42,12 @@ impl Focus for MainViewState { } } - fn escape(&mut self) -> Command { + fn secondary(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { + // secondary action doesn't do anything when action panel is open + panic!("invalid state") + } + + fn back(&mut self) -> Command { match self { MainViewState::None => { Command::perform(async {}, |_| AppMsg::HideWindow) @@ -54,15 +59,15 @@ impl Focus for MainViewState { } } - fn tab(&mut self) -> Command { + fn next(&mut self) -> Command { todo!() } - fn shift_tab(&mut self) -> Command { + fn previous(&mut self) -> Command { todo!() } - fn arrow_up(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { + fn up(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { match self { MainViewState::None => Command::none(), MainViewState::ActionPanel { focused_action_item, .. } => { @@ -71,7 +76,7 @@ impl Focus for MainViewState { } } - fn arrow_down(&mut self, focus_list: &[SearchResultEntrypointAction]) -> Command { + fn down(&mut self, focus_list: &[SearchResultEntrypointAction]) -> Command { match self { MainViewState::None => Command::none(), MainViewState::ActionPanel { focused_action_item } => { @@ -84,11 +89,11 @@ impl Focus for MainViewState { } } - fn arrow_left(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { + fn left(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { todo!() } - fn arrow_right(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { + fn right(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { todo!() } } diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 723173d..3c5a3f1 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -119,18 +119,19 @@ impl GlobalState { } pub trait Focus { - fn enter(&mut self, focus_list: &[T]) -> Command; - fn escape(&mut self) -> Command; - fn tab(&mut self) -> Command; - fn shift_tab(&mut self) -> Command; - fn arrow_up(&mut self, focus_list: &[T]) -> Command; - fn arrow_down(&mut self, focus_list: &[T]) -> Command; - fn arrow_left(&mut self, focus_list: &[T]) -> Command; - fn arrow_right(&mut self, focus_list: &[T]) -> Command; + fn primary(&mut self, focus_list: &[T]) -> Command; + fn secondary(&mut self, focus_list: &[T]) -> Command; + fn back(&mut self) -> Command; + fn next(&mut self) -> Command; + fn previous(&mut self) -> Command; + fn up(&mut self, focus_list: &[T]) -> Command; + fn down(&mut self, focus_list: &[T]) -> Command; + fn left(&mut self, focus_list: &[T]) -> Command; + fn right(&mut self, focus_list: &[T]) -> Command; } impl Focus for GlobalState { - fn enter(&mut self, focus_list: &[SearchResult]) -> Command { + fn primary(&mut self, focus_list: &[SearchResult]) -> Command { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { if let Some(search_item) = focused_search_result.get(focus_list) { @@ -140,7 +141,7 @@ impl Focus for GlobalState { Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) } MainViewState::ActionPanel { .. } => { - sub_state.enter(&search_item.entrypoint_actions) + sub_state.primary(&search_item.entrypoint_actions) } } } else { @@ -152,16 +153,44 @@ impl Focus for GlobalState { let action_ids = client_context.get_action_ids(); - sub_state.enter(&action_ids) + sub_state.primary(&action_ids) } GlobalState::ErrorView { .. } => Command::none() } } - fn escape(&mut self) -> Command { + fn secondary(&mut self, focus_list: &[SearchResult]) -> Command { + match self { + GlobalState::MainView { focused_search_result, sub_state, .. } => { + if let Some(search_item) = focused_search_result.get(focus_list) { + match sub_state { + MainViewState::None => { + let search_item = search_item.clone(); + Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, Some(0))) + } + MainViewState::ActionPanel { .. } => { + Command::none() + } + } + } else { + Command::none() + } + } + GlobalState::PluginView { sub_state, client_context, .. } => { + let client_context = client_context.read().expect("lock is poisoned"); + + let action_ids = client_context.get_action_ids(); + + sub_state.secondary(&action_ids) + } + GlobalState::ErrorView { .. } => Command::none() + } + } + + fn back(&mut self) -> Command { match self { GlobalState::MainView { sub_state, .. } => { - sub_state.escape() + sub_state.back() } GlobalState::PluginView { plugin_view_data: PluginViewData { @@ -189,7 +218,7 @@ impl Focus for GlobalState { } } PluginViewState::ActionPanel { .. } => { - sub_state.escape() + sub_state.back() } } } @@ -198,21 +227,21 @@ impl Focus for GlobalState { } } } - fn tab(&mut self) -> Command { + fn next(&mut self) -> Command { match self { GlobalState::MainView { .. } => Command::none(), GlobalState::PluginView { .. } => Command::none(), GlobalState::ErrorView { .. } => Command::none(), } } - fn shift_tab(&mut self) -> Command { + fn previous(&mut self) -> Command { match self { GlobalState::MainView { .. } => Command::none(), GlobalState::PluginView { .. } => Command::none(), GlobalState::ErrorView { .. } => Command::none(), } } - fn arrow_up(&mut self, focus_list: &[SearchResult]) -> Command { + fn up(&mut self, focus_list: &[SearchResult]) -> Command { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { @@ -221,7 +250,7 @@ impl Focus for GlobalState { } MainViewState::ActionPanel { .. } => { if let Some(search_item) = focused_search_result.get(focus_list) { - sub_state.arrow_up(&search_item.entrypoint_actions) + sub_state.up(&search_item.entrypoint_actions) } else { Command::none() } @@ -237,13 +266,13 @@ impl Focus for GlobalState { let action_ids = client_context.get_action_ids(); - sub_state.arrow_up(&action_ids) + sub_state.up(&action_ids) } } }, } } - fn arrow_down(&mut self, focus_list: &[SearchResult]) -> Command { + fn down(&mut self, focus_list: &[SearchResult]) -> Command { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { @@ -256,7 +285,7 @@ impl Focus for GlobalState { } MainViewState::ActionPanel { .. } => { if let Some(search_item) = focused_search_result.get(focus_list) { - sub_state.arrow_down(&search_item.entrypoint_actions) + sub_state.down(&search_item.entrypoint_actions) } else { Command::none() } @@ -272,20 +301,20 @@ impl Focus for GlobalState { let action_ids = client_context.get_action_ids(); - sub_state.arrow_down(&action_ids) + sub_state.down(&action_ids) } } } } } - fn arrow_left(&mut self, _focus_list: &[SearchResult]) -> Command { + fn left(&mut self, _focus_list: &[SearchResult]) -> Command { match self { GlobalState::MainView { .. } => Command::none(), GlobalState::PluginView { .. } => Command::none(), GlobalState::ErrorView { .. } => Command::none(), } } - fn arrow_right(&mut self, _focus_list: &[SearchResult]) -> Command { + fn right(&mut self, _focus_list: &[SearchResult]) -> Command { match self { GlobalState::MainView { .. } => Command::none(), GlobalState::PluginView { .. } => Command::none(), diff --git a/rust/client/src/ui/state/plugin_view.rs b/rust/client/src/ui/state/plugin_view.rs index a17db0b..21dc41a 100644 --- a/rust/client/src/ui/state/plugin_view.rs +++ b/rust/client/src/ui/state/plugin_view.rs @@ -30,9 +30,16 @@ impl PluginViewState { } impl Focus for PluginViewState { - fn enter(&mut self, focus_list: &[UiWidgetId]) -> Command { + fn primary(&mut self, focus_list: &[UiWidgetId]) -> Command { match self { - PluginViewState::None => Command::none(), + PluginViewState::None => { + if let Some(widget_id) = focus_list.get(0) { + let widget_id = *widget_id; + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) + } else { + Command::none() + } + }, PluginViewState::ActionPanel { focused_action_item, .. } => { if let Some(widget_id) = focused_action_item.get(focus_list) { let widget_id = *widget_id; @@ -44,7 +51,24 @@ impl Focus for PluginViewState { } } - fn escape(&mut self) -> Command { + fn secondary(&mut self, focus_list: &[UiWidgetId]) -> Command { + match self { + PluginViewState::None => { + if let Some(widget_id) = focus_list.get(1) { + let widget_id = *widget_id; + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) + } else { + Command::none() + } + }, + PluginViewState::ActionPanel { .. } => { + // secondary does nothing when action panel is opened + Command::none() + } + } + } + + fn back(&mut self) -> Command { match self { PluginViewState::None => { panic!("invalid state") @@ -55,15 +79,15 @@ impl Focus for PluginViewState { } } - fn tab(&mut self) -> Command { + fn next(&mut self) -> Command { todo!() } - fn shift_tab(&mut self) -> Command { + fn previous(&mut self) -> Command { todo!() } - fn arrow_up(&mut self, _focus_list: &[UiWidgetId]) -> Command { + fn up(&mut self, _focus_list: &[UiWidgetId]) -> Command { match self { PluginViewState::None => Command::none(), PluginViewState::ActionPanel { focused_action_item, .. } => { @@ -72,7 +96,7 @@ impl Focus for PluginViewState { } } - fn arrow_down(&mut self, focus_list: &[UiWidgetId]) -> Command { + fn down(&mut self, focus_list: &[UiWidgetId]) -> Command { match self { PluginViewState::None => Command::none(), PluginViewState::ActionPanel { focused_action_item } => { @@ -85,11 +109,11 @@ impl Focus for PluginViewState { } } - fn arrow_left(&mut self, _focus_list: &[UiWidgetId]) -> Command { + fn left(&mut self, _focus_list: &[UiWidgetId]) -> Command { todo!() } - fn arrow_right(&mut self, _focus_list: &[UiWidgetId]) -> Command { + fn right(&mut self, _focus_list: &[UiWidgetId]) -> Command { todo!() } } From 72db547a998b3a2655b728da7d929a837d742e2e Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 5 Oct 2024 18:59:03 +0200 Subject: [PATCH 101/540] Make patch part optional in macOS version parsing. Fixes Applications plugin not working on some systems --- rust/server/src/plugins/applications/macos.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/server/src/plugins/applications/macos.rs b/rust/server/src/plugins/applications/macos.rs index b504edd..27b2182 100644 --- a/rust/server/src/plugins/applications/macos.rs +++ b/rust/server/src/plugins/applications/macos.rs @@ -91,7 +91,7 @@ fn get_settings(file_manager: &FileManager) -> Vec { let system_version: SystemVersion = plist::from_file("/System/Library/CoreServices/SystemVersion.plist") .expect("SystemVersion.plist doesn't follow expected format"); - let regex = Regex::new(r"^(?\d+).\d+.\d+$") + let regex = Regex::new(r"^(?\d+).\d+(.\d+)?$") .expect("This regex cannot be invalid"); let captures = regex.captures(&system_version.product_version) From 12634f3d7d330885ea71325d65e91c2c2d5d26a4 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 5 Oct 2024 21:14:51 +0200 Subject: [PATCH 102/540] Implement action shortcuts on main view --- dev_plugin/gauntlet.toml | 6 +++ dev_plugin/src/command-generator.ts | 2 +- js/core/src/command-generator.ts | 17 ++++++- js/core/src/init.tsx | 25 +++++++--- js/typings/index.d.ts | 5 +- rust/client/src/ui/mod.rs | 65 +++++++++++++++++++++++--- rust/client/src/ui/widget_container.rs | 2 +- rust/common/src/model.rs | 7 +++ rust/common/src/rpc/backend_api.rs | 4 +- rust/server/src/lib.rs | 3 +- rust/server/src/model.rs | 10 +++- rust/server/src/plugins/js/mod.rs | 14 ++++-- rust/server/src/plugins/mod.rs | 7 +-- 13 files changed, 140 insertions(+), 27 deletions(-) diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index e41f919..dbf235e 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -136,6 +136,12 @@ path = 'src/command-generator.ts' type = 'command-generator' description = '' +[[entrypoint.actions]] +id = 'testGeneratedAction1' +description = "test action description 1" +shortcut = { key = 'a', kind = 'main'} + + [[entrypoint]] id = 'test-list-detail' name = 'Test List Detail' diff --git a/dev_plugin/src/command-generator.ts b/dev_plugin/src/command-generator.ts index 76ce1f1..2a15c4f 100644 --- a/dev_plugin/src/command-generator.ts +++ b/dev_plugin/src/command-generator.ts @@ -27,13 +27,13 @@ export default function CommandGenerator(): GeneratedCommand[] { }, actions: [ { - ref: "testAction1", label: "Test 1", fn: () => { console.log('generated-action-1') } }, { + ref: "testGeneratedAction1", label: "Test 2", fn: () => { console.log('generated-action-2') diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index 4cab0ee..3f83838 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -16,7 +16,7 @@ export interface GeneratedCommandAction { fn: () => void } -type ProcessedGeneratedCommand = GeneratedCommand & { lookupId: string, uuid: string }; +type ProcessedGeneratedCommand = GeneratedCommand & { generatorEntrypointId: string, lookupId: string, uuid: string }; let storedGeneratedCommands: ProcessedGeneratedCommand[] = [] @@ -33,6 +33,7 @@ export async function runCommandGenerators(): Promise { const generatedCommands = (await generator()) .map(value => { return { + generatorEntrypointId: generatorEntrypointId, lookupId: generatorEntrypointId + ":" + value.id, uuid: crypto.randomUUID(), ...value @@ -64,6 +65,20 @@ export function generatedCommandSearchIndex(): AdditionalSearchItem[] { })) } +export async function runGeneratedCommandAction(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean) { + const command = storedGeneratedCommands.find(value => value.lookupId == entrypointId); + + if (command) { + const id = await InternalApi.fetch_action_id_for_shortcut(command.generatorEntrypointId, key, modifierShift, modifierControl, modifierAlt, modifierMeta); + if (id) { + const action = command.actions?.find(value => value.ref == id); + if (action) { + action.fn() + } + } + } +} + export function runGeneratedCommand(entrypointId: string, action_index: number | undefined) { const generatedCommand = storedGeneratedCommands.find(value => value.lookupId === entrypointId); diff --git a/js/core/src/init.tsx b/js/core/src/init.tsx index bc312d3..71de6ee 100644 --- a/js/core/src/init.tsx +++ b/js/core/src/init.tsx @@ -1,5 +1,5 @@ import { FC } from "react"; -import { runCommandGenerators, runGeneratedCommand } from "./command-generator"; +import { runCommandGenerators, runGeneratedCommand, runGeneratedCommandAction } from "./command-generator"; import { reloadSearchIndex } from "./search-index"; import { clearRenderer } from "gauntlet:renderer"; @@ -91,15 +91,26 @@ function handleEvent(event: ViewEvent) { async function handleKeyboardEvent(event: NotReactsKeyboardEvent) { InternalApi.op_log_trace("plugin_event_handler", `Handling keyboard event: ${Deno.inspect(event)}`); - if (latestRootUiWidget) { - const actionHandlers = findAllActionHandlers(latestRootUiWidget); + switch (event.origin) { + case "MainView": { + runGeneratedCommandAction(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta) + break; + } + case "PluginView": { + if (latestRootUiWidget) { + const actionHandlers = findAllActionHandlers(latestRootUiWidget); - const id = await InternalApi.fetch_action_id_for_shortcut(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta); + const id = await InternalApi.fetch_action_id_for_shortcut(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta); - const actionHandler = actionHandlers.find(value => value.id === id); + if (id) { + const actionHandler = actionHandlers.find(value => value.id === id); - if (actionHandler) { - actionHandler.onAction() + if (actionHandler) { + actionHandler.onAction() + } + } + } + break; } } } diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 65b2c1b..003721d 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -19,10 +19,13 @@ type ViewEvent = { eventArguments: PropertyValue[] } +type KeyboardEventOrigin = "MainView" | "PluginView" + // naming to avoid collision type NotReactsKeyboardEvent = { type: "KeyboardEvent" entrypointId: string + origin: KeyboardEventOrigin key: string modifierShift: boolean modifierControl: boolean @@ -118,7 +121,7 @@ interface InternalApi { op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, entrypoint_id: string, container: UiWidget): void; show_plugin_error_view(entrypoint_id: string, render_location: RenderLocation): void; - fetch_action_id_for_shortcut(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean): Promise; + fetch_action_id_for_shortcut(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean): Promise; clipboard_read(): Promise<{ text_data?: string, png_data?: Blob }>; clipboard_read_text(): Promise; diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index a8a2771..a0caf62 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -24,7 +24,7 @@ use tokio::sync::RwLock as TokioRwLock; use tonic::transport::Server; use client_context::ClientContext; -use common::model::{BackendRequestData, BackendResponseData, EntrypointId, PhysicalKey, PhysicalShortcut, PluginId, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; +use common::model::{BackendRequestData, BackendResponseData, EntrypointId, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; use common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFrontendApiError}; use common::scenario_convert::{ui_render_location_from_scenario, ui_widget_from_scenario}; use common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; @@ -502,20 +502,59 @@ impl Application for AppModel { }, _ => { match &mut self.global_state { - GlobalState::MainView { sub_state, search_field_id, prompt, .. } => { + GlobalState::MainView { sub_state, search_field_id, prompt, focused_search_result, .. } => { match sub_state { MainViewState::None => { match physical_key_model(physical_key, modifiers) { Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { Command::perform(async {}, |_| AppMsg::ToggleActionPanel) } - _ => Self::append_prompt(prompt, text, search_field_id.clone(), modifiers) } + Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { + if let Some(search_item) = focused_search_result.get(&self.search_results) { + if search_item.entrypoint_actions.len() > 0 { + self.handle_main_view_keyboard_event( + search_item.plugin_id.clone(), + search_item.entrypoint_id.clone(), + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + ) + } else { + Command::none() + } + } else { + Command::none() + } + } + _ => Self::append_prompt(prompt, text, search_field_id.clone(), modifiers) + } } MainViewState::ActionPanel { .. } => { match physical_key_model(physical_key, modifiers) { Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { Command::perform(async {}, |_| AppMsg::ToggleActionPanel) } + Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { + if let Some(search_item) = focused_search_result.get(&self.search_results) { + if search_item.entrypoint_actions.len() > 0 { + self.handle_main_view_keyboard_event( + search_item.plugin_id.clone(), + search_item.entrypoint_id.clone(), + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + ) + } else { + Command::none() + } + } else { + Command::none() + } + } _ => Command::none() } } @@ -528,7 +567,7 @@ impl Application for AppModel { Command::perform(async {}, |_| AppMsg::ToggleActionPanel) } Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - self.handle_plugin_keyboard_event(physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + self.handle_plugin_view_keyboard_event(physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) } _ => Command::none() } @@ -1416,7 +1455,21 @@ impl AppModel { }, |result| handle_backend_error(result, |msg| msg)) } - fn handle_plugin_keyboard_event(&self, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Command { + fn handle_main_view_keyboard_event(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Command { + let mut backend_client = self.backend_api.clone(); + + Command::perform( + async move { + backend_client.send_keyboard_event(plugin_id, entrypoint_id, KeyboardEventOrigin::MainView, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + .await?; + + Ok(()) + }, + |result| handle_backend_error(result, |()| AppMsg::Noop), + ) + } + + fn handle_plugin_view_keyboard_event(&self, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Command { let mut backend_client = self.backend_api.clone(); let (plugin_id, entrypoint_id) = { @@ -1426,7 +1479,7 @@ impl AppModel { Command::perform( async move { - backend_client.send_keyboard_event(plugin_id, entrypoint_id, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + backend_client.send_keyboard_event(plugin_id, entrypoint_id, KeyboardEventOrigin::PluginView, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) .await?; Ok(()) diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 0185148..29c10f5 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -28,7 +28,7 @@ impl PluginWidgetContainer { } pub fn get_plugin_name(&self) -> String { - self.plugin_name.clone().expect("plugin id should always exist after render") + self.plugin_name.clone().expect("plugin name should always exist after render") } pub fn get_entrypoint_id(&self) -> EntrypointId { diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 0c733de..ef023ef 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -191,6 +191,7 @@ pub enum BackendRequestData { SendKeyboardEvent { plugin_id: PluginId, entrypoint_id: EntrypointId, + origin: KeyboardEventOrigin, key: PhysicalKey, modifier_shift: bool, modifier_control: bool, @@ -208,6 +209,12 @@ pub enum BackendRequestData { }, } +#[derive(Debug, Clone)] +pub enum KeyboardEventOrigin { + MainView, + PluginView, +} + #[derive(Debug, Clone)] pub struct UiWidget { pub widget_id: UiWidgetId, diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index c419399..493ea11 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -6,7 +6,7 @@ use tonic::transport::Channel; use utils::channel::{RequestError, RequestSender}; -use crate::model::{BackendRequestData, BackendResponseData, DownloadStatus, EntrypointId, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, UiPropertyValue, UiWidgetId}; +use crate::model::{BackendRequestData, BackendResponseData, DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, UiPropertyValue, UiWidgetId}; use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadStatus, RpcDownloadStatusRequest, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcPingRequest, RpcPluginsRequest, RpcRemovePluginRequest, RpcSaveLocalPluginRequest, RpcSetEntrypointStateRequest, RpcSetGlobalShortcutRequest, RpcSetPluginStateRequest, RpcSetPreferenceValueRequest, RpcShowSettingsWindowRequest, RpcShowWindowRequest}; use crate::rpc::grpc::rpc_backend_client::RpcBackendClient; use crate::rpc::grpc_convert::{plugin_preference_from_rpc, plugin_preference_user_data_from_rpc, plugin_preference_user_data_to_rpc}; @@ -127,6 +127,7 @@ impl BackendForFrontendApi { &mut self, plugin_id: PluginId, entrypoint_id: EntrypointId, + origin: KeyboardEventOrigin, key: PhysicalKey, modifier_shift: bool, modifier_control: bool, @@ -136,6 +137,7 @@ impl BackendForFrontendApi { let request = BackendRequestData::SendKeyboardEvent { plugin_id, entrypoint_id, + origin, key, modifier_shift, modifier_control, diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index 78626b3..2d88988 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -195,10 +195,11 @@ async fn handle_request(application_manager: Arc, request_da BackendResponseData::Nothing } - BackendRequestData::SendKeyboardEvent { plugin_id, entrypoint_id, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { + BackendRequestData::SendKeyboardEvent { plugin_id, entrypoint_id, origin, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { application_manager.handle_keyboard_event( plugin_id, entrypoint_id, + origin, key, modifier_shift, modifier_control, diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 92a855c..2ee6c08 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use deno_core::serde_v8; use serde::{Deserialize, Serialize}; -use common::model::{EntrypointId, PhysicalKey, UiPropertyValue, UiWidget, UiWidgetId}; +use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, UiPropertyValue, UiWidget, UiWidgetId}; #[derive(Debug)] pub enum JsUiResponseData { @@ -66,6 +66,7 @@ pub enum JsUiEvent { KeyboardEvent { #[serde(rename = "entrypointId")] entrypoint_id: String, + origin: JsKeyboardEventOrigin, key: String, #[serde(rename = "modifierShift")] modifier_shift: bool, @@ -84,6 +85,12 @@ pub enum JsUiEvent { RefreshSearchIndex, } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum JsKeyboardEventOrigin { + MainView, + PluginView, +} + // FIXME this could have been serde_v8::AnyValue but it doesn't support undefined, make a pr? #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type")] @@ -133,6 +140,7 @@ pub enum IntermediateUiEvent { HandleKeyboardEvent { entrypoint_id: EntrypointId, key: PhysicalKey, + origin: KeyboardEventOrigin, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 498060b..6080451 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -28,11 +28,11 @@ use tokio::net::TcpStream; use tokio_util::sync::CancellationToken; use common::dirs::Dirs; -use common::model::{EntrypointId, PhysicalKey, PluginId, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidget, UiWidgetId}; +use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidget, UiWidgetId}; use common::rpc::frontend_api::FrontendApi; use component_model::{create_component_model, Children, Component, Property, PropertyType, SharedType}; -use crate::model::{IntermediateUiEvent, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData, JsUiWidget, PreferenceUserData}; +use crate::model::{IntermediateUiEvent, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData, JsUiWidget, JsKeyboardEventOrigin, PreferenceUserData}; use crate::plugins::applications::{get_apps, DesktopEntry}; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; @@ -116,6 +116,7 @@ pub enum OnePluginCommandData { }, HandleKeyboardEvent { entrypoint_id: EntrypointId, + origin: KeyboardEventOrigin, key: PhysicalKey, modifier_shift: bool, modifier_control: bool, @@ -180,9 +181,10 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run event_arguments, }) } - OnePluginCommandData::HandleKeyboardEvent { entrypoint_id, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { + OnePluginCommandData::HandleKeyboardEvent { entrypoint_id, origin, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { Some(IntermediateUiEvent::HandleKeyboardEvent { entrypoint_id, + origin, key, modifier_shift, modifier_control, @@ -667,9 +669,13 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { event_arguments, } } - IntermediateUiEvent::HandleKeyboardEvent { entrypoint_id, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { + IntermediateUiEvent::HandleKeyboardEvent { entrypoint_id, origin, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { JsUiEvent::KeyboardEvent { entrypoint_id: entrypoint_id.to_string(), + origin: match origin { + KeyboardEventOrigin::MainView => JsKeyboardEventOrigin::MainView, + KeyboardEventOrigin::PluginView => JsKeyboardEventOrigin::PluginView, + }, key: key.to_value(), modifier_shift, modifier_control, diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 7fe80b9..aff2f9c 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -10,12 +10,12 @@ use global_hotkey::hotkey::HotKey; use include_dir::{Dir, include_dir}; use tokio::runtime::Handle; -use common::model::{DownloadStatus, EntrypointId, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, UiPropertyValue, UiRequestData, UiResponseData, UiWidgetId}; +use common::model::{DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, UiPropertyValue, UiRequestData, UiResponseData, UiWidgetId}; use common::rpc::frontend_api::FrontendApi; use common::{settings_env_data_to_string, SettingsEnvData}; use utils::channel::RequestSender; use common::dirs::Dirs; -use crate::model::ActionShortcutKey; +use crate::model::{ActionShortcutKey, JsKeyboardEventOrigin}; use crate::plugins::config_reader::ConfigReader; use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions}; use crate::plugins::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; @@ -404,11 +404,12 @@ impl ApplicationManager { }) } - pub fn handle_keyboard_event(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) { + pub fn handle_keyboard_event(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, origin: KeyboardEventOrigin, key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) { self.send_command(PluginCommand::One { id: plugin_id, data: OnePluginCommandData::HandleKeyboardEvent { entrypoint_id, + origin, key, modifier_shift, modifier_control, From a334ba2052bed9c9d3601bc9040b1270868c4f79 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 5 Oct 2024 21:31:51 +0200 Subject: [PATCH 103/540] Fix action shortcut not being shown in main search view action panel --- js/core/src/command-generator.ts | 1 + rust/server/src/plugins/js/search.rs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index 3f83838..fe99d57 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -53,6 +53,7 @@ export async function runCommandGenerators(): Promise { export function generatedCommandSearchIndex(): AdditionalSearchItem[] { return storedGeneratedCommands.map(value => ({ + generator_entrypoint_id: value.generatorEntrypointId, entrypoint_id: value.lookupId, entrypoint_uuid: value.uuid, entrypoint_name: value.name, diff --git a/rust/server/src/plugins/js/search.rs b/rust/server/src/plugins/js/search.rs index ee82b06..66e772f 100644 --- a/rust/server/src/plugins/js/search.rs +++ b/rust/server/src/plugins/js/search.rs @@ -3,7 +3,7 @@ use crate::plugins::icon_cache::IconCache; use crate::plugins::js::PluginData; use crate::search::{SearchIndex, SearchIndexItem, SearchIndexItemAction}; use anyhow::Context; -use common::model::{EntrypointId, PhysicalShortcut, SearchResultEntrypointType}; +use common::model::{EntrypointId, SearchResultEntrypointType}; use deno_core::{op, OpState}; use serde::Deserialize; use std::cell::RefCell; @@ -72,7 +72,7 @@ async fn reload_search_index(state: Rc>, generated_commands: Ve let entrypoint_frecency = frecency_map.get(&item.entrypoint_id).cloned().unwrap_or(0.0); let shortcuts = shortcuts - .get(&item.entrypoint_id); + .get(&item.generator_entrypoint_id); let entrypoint_actions = item.entrypoint_actions.iter() .map(|action| { @@ -176,6 +176,7 @@ async fn reload_search_index(state: Rc>, generated_commands: Ve #[derive(Debug, Deserialize)] struct AdditionalSearchItem { entrypoint_name: String, + generator_entrypoint_id: String, entrypoint_id: String, entrypoint_uuid: String, entrypoint_icon: Option>, From c021fbab7e54836d915539183d578d34b7a95294 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 5 Oct 2024 21:47:16 +0200 Subject: [PATCH 104/540] Gracefully handle parse errors of system extension plist in macOS Applications plugin --- rust/server/src/plugins/applications/macos.rs | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/rust/server/src/plugins/applications/macos.rs b/rust/server/src/plugins/applications/macos.rs index 27b2182..33ce416 100644 --- a/rust/server/src/plugins/applications/macos.rs +++ b/rust/server/src/plugins/applications/macos.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::error::Error; use std::ffi::OsStr; use std::path::{Path, PathBuf}; +use anyhow::Context; use cacao::filesystem::{FileManager, SearchPathDirectory, SearchPathDomainMask}; use cacao::url::Url; use deno_runtime::deno_http::compressible::is_content_compressible; @@ -116,23 +117,29 @@ fn get_settings(file_manager: &FileManager) -> Vec { let extensions: HashMap<_, _> = get_extensions_in_dir(PathBuf::from("/System/Library/ExtensionKit/Extensions")) .into_iter() - .map(|path| { - let name = path.file_stem() - .expect(&format!("invalid path: {:?}", path)) - .to_string_lossy() - .to_string(); + .filter_map(|path| { + fn read_plist(path: &Path) -> anyhow::Result<(String, String)> { + let name = path.file_stem() + .expect(&format!("invalid path: {:?}", path)) + .to_string_lossy() + .to_string(); - let info_path = path.join("Contents").join("Info.plist"); + let info_path = path.join("Contents").join("Info.plist"); - let info = plist::from_file::(info_path) - .expect("Unexpected Info.plist for System Extensions"); + let info = plist::from_file::<_, Info>(info_path.as_path()) + .context(format!("Unexpected Info.plist for System Extensions: {}", &info_path.display()))?; - let name = info.bundle_display_name - .clone() - .or_else(|| info.bundle_name.clone()) - .unwrap_or(name); + let name = info.bundle_display_name + .clone() + .or_else(|| info.bundle_name.clone()) + .unwrap_or(name); - (info.bundle_id, name) + Ok((info.bundle_id, name)) + } + + read_plist(&path) + .inspect_err(|err| tracing::error!("error while reading system extension Info.plist {:?}: {:?}", path, err)) + .ok() }) .collect(); From 628523302e31ac8785ad767ed66ec0ec1d0e3b6e Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 5 Oct 2024 21:48:08 +0200 Subject: [PATCH 105/540] Move settings to the bottom of macOS Applications search list --- rust/server/src/plugins/applications/macos.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/server/src/plugins/applications/macos.rs b/rust/server/src/plugins/applications/macos.rs index 33ce416..29cb38d 100644 --- a/rust/server/src/plugins/applications/macos.rs +++ b/rust/server/src/plugins/applications/macos.rs @@ -15,8 +15,8 @@ pub fn get_apps() -> Vec { let file_manager = FileManager::default(); let all_items = [ + get_applications(&file_manager), get_settings(&file_manager), - get_applications(&file_manager) ]; all_items From 43e7cf919d2a646c17c268244ea69527cd09d50b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 6 Oct 2024 09:29:55 +0200 Subject: [PATCH 106/540] Fix log printing wrong thing --- rust/server/src/plugins/applications/macos.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/server/src/plugins/applications/macos.rs b/rust/server/src/plugins/applications/macos.rs index 29cb38d..0db67e5 100644 --- a/rust/server/src/plugins/applications/macos.rs +++ b/rust/server/src/plugins/applications/macos.rs @@ -143,7 +143,7 @@ fn get_settings(file_manager: &FileManager) -> Vec { }) .collect(); - tracing::debug!("Found following macOS setting extensions: {:?}", &preferences_ids); + tracing::debug!("Found following macOS setting extensions: {:?}", &extensions); preferences_ids.into_iter() .filter_map(|preferences_id| { From ff23ea4d76e9fefe3e837e223375a108435b9f49 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 6 Oct 2024 09:53:50 +0200 Subject: [PATCH 107/540] Slightly improve logging --- rust/common/src/model.rs | 1 + rust/server/src/plugins/mod.rs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index ef023ef..1046e16 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -343,6 +343,7 @@ pub enum PluginPreferenceUserData { ListOfEnums { value: Option>, }, + // TODO be careful about exposing secrets to logs when adding password type } #[derive(Debug, Clone)] diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index aff2f9c..5e830db 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -96,6 +96,8 @@ impl ApplicationManager { } pub fn clear_all_icon_cache_dir(&self) -> anyhow::Result<()> { + tracing::debug!("clearing all icon cache"); + self.icon_cache.clear_all_icon_cache_dir() } @@ -248,6 +250,8 @@ impl ApplicationManager { } pub async fn set_entrypoint_state(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, enabled: bool) -> anyhow::Result<()> { + tracing::debug!(target = "plugin", "Setting entrypoint state for plugin id: {:?}, entrypoint_id: {:?}, enabled: {}", plugin_id, entrypoint_id, enabled); + self.db_repository.set_plugin_entrypoint_enabled(&plugin_id.to_string(), &entrypoint_id.to_string(), enabled) .await?; @@ -271,6 +275,8 @@ impl ApplicationManager { } pub async fn set_preference_value(&self, plugin_id: PluginId, entrypoint_id: Option, preference_id: String, preference_value: PluginPreferenceUserData) -> anyhow::Result<()> { + tracing::debug!(target = "plugin", "Setting preference value for plugin id: {:?}, entrypoint_id: {:?}, preference_id: {}", plugin_id, entrypoint_id, preference_id); + let user_data = plugin_preference_user_data_to_db(preference_value); self.db_repository.set_preference_value(plugin_id.to_string(), entrypoint_id.map(|id| id.to_string()), preference_id, user_data) From f26baeaecb4219e96a5d8037476e6461bba9be14 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 6 Oct 2024 11:19:22 +0200 Subject: [PATCH 108/540] Add build information shown on startup --- Cargo.lock | 198 ++++++++++++++++++++++++++++++++++++++--- rust/server/Cargo.toml | 4 + rust/server/build.rs | 19 +++- rust/server/src/lib.rs | 8 ++ 4 files changed, 215 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7246706..17ca186 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,9 +240,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" dependencies = [ "backtrace", ] @@ -909,6 +909,38 @@ dependencies = [ "wayland-client 0.31.2", ] +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.22", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cbc" version = "0.1.2" @@ -1615,6 +1647,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2 1.0.80", + "quote 1.0.36", + "strsim", + "syn 2.0.59", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote 1.0.36", + "syn 2.0.59", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -2309,6 +2376,37 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_builder" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" +dependencies = [ + "darling", + "proc-macro2 1.0.80", + "quote 1.0.36", + "syn 2.0.59", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" +dependencies = [ + "derive_builder_core", + "syn 2.0.59", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -4295,6 +4393,12 @@ dependencies = [ "objc2 0.4.1", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -5605,6 +5709,15 @@ dependencies = [ "syn 2.0.59", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "numbat" version = "1.13.0" @@ -6980,14 +7093,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -7001,13 +7114,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.5", ] [[package]] @@ -7018,9 +7131,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "renderdoc-sys" @@ -7425,9 +7538,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "rustybuzz" @@ -7635,6 +7748,9 @@ name = "semver" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +dependencies = [ + "serde", +] [[package]] name = "semver-parser" @@ -7776,6 +7892,8 @@ dependencies = [ "ureq", "utils", "uuid", + "vergen-gitcl", + "vergen-pretty", "walkdir", "zstd-sys", ] @@ -9198,7 +9316,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -9988,6 +10108,58 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vergen" +version = "9.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349ed9e45296a581f455bc18039878f409992999bc1d5da12a6800eb18c8752f" +dependencies = [ + "anyhow", + "cargo_metadata", + "derive_builder", + "regex", + "rustversion", + "time 0.3.36", + "vergen-lib", +] + +[[package]] +name = "vergen-gitcl" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3a7f91caabecefc3c249fd864b11d4abe315c166fbdb568964421bccfd2b7a" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "time 0.3.36", + "vergen", + "vergen-lib", +] + +[[package]] +name = "vergen-lib" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229eaddb0050920816cf051e619affaf18caa3dd512de8de5839ccbc8e53abb0" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + +[[package]] +name = "vergen-pretty" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9579e7d75e528471646aa8c9bf11392154f3f8e0b02d94211bc5d57701d010f0" +dependencies = [ + "anyhow", + "convert_case 0.6.0", + "derive_builder", + "rustversion", +] + [[package]] name = "version-compare" version = "0.2.0" diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 2edf69c..905a6e6 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -42,6 +42,7 @@ typed-path = "0.9" scenario_runner = { path = "../scenario_runner", optional = true } itertools = "0.10.5" +vergen-pretty = "0.3.5" [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] libc = "0.2.153" @@ -58,3 +59,6 @@ icns = "0.3.1" [features] release = ["common/release"] scenario_runner = ["dep:scenario_runner", "common/scenario_runner"] + +[build-dependencies] +vergen-gitcl = { version = "1.0.1", features = ["build", "cargo"] } diff --git a/rust/server/build.rs b/rust/server/build.rs index de8c9a1..954e2a9 100644 --- a/rust/server/build.rs +++ b/rust/server/build.rs @@ -1,3 +1,20 @@ -fn main() { +use vergen_gitcl::{CargoBuilder, Emitter, GitclBuilder}; + +fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=src/db_migrations"); + + let gitcl = GitclBuilder::all_git()?; + let cargo = CargoBuilder::default() + .debug(true) + .features(true) + .opt_level(true) + .target_triple(true) + .build()?; + + Emitter::default() + .add_instructions(&gitcl)? + .add_instructions(&cargo)? + .emit()?; + + Ok(()) } diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index 2d88988..f996081 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -1,5 +1,6 @@ use std::rc::Rc; use std::sync::Arc; +use vergen_pretty::vergen_pretty_env; use client::{open_window, start_client}; use common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; use common::rpc::backend_api::BackendApi; @@ -18,6 +19,13 @@ pub(in crate) mod model; const SETTINGS_ENV: &'static str = "GAUNTLET_INTERNAL_SETTINGS"; pub fn start(minimized: bool) { + tracing::info!("Gauntlet Build Information:"); + for (name, value) in vergen_pretty_env!() { + if let Some(value) = value { + tracing::info!("{}: {}", name, value); + } + } + #[cfg(feature = "scenario_runner")] run_scenario_runner(); From 68f4aadc6a28f9303b710f9165acdf3d1322889f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 6 Oct 2024 11:22:35 +0200 Subject: [PATCH 109/540] Add log about macOS version used for indexing settings --- rust/server/src/plugins/applications/macos.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rust/server/src/plugins/applications/macos.rs b/rust/server/src/plugins/applications/macos.rs index 0db67e5..e03e5f6 100644 --- a/rust/server/src/plugins/applications/macos.rs +++ b/rust/server/src/plugins/applications/macos.rs @@ -102,6 +102,8 @@ fn get_settings(file_manager: &FileManager) -> Vec { .parse() .expect("SystemVersion.plist ProductVersion major doesn't match expected format"); + tracing::debug!("Indexing settings for macOS version: {}", major_version); + if major_version >= 13 { // Ventura and higher let sidebar: Vec = plist::from_file("/System/Applications/System Settings.app/Contents/Resources/Sidebar.plist") .expect("Sidebar.plist doesn't follow expected format"); From 39451890a88c680fcb179806fdfd0469cf8f8d8b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 6 Oct 2024 12:27:15 +0200 Subject: [PATCH 110/540] Fix previous inline view still being shown after main window is closed and opened again --- rust/client/src/ui/client_context.rs | 4 ++++ rust/client/src/ui/mod.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index 07b1bb7..b1613c6 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -62,6 +62,10 @@ impl ClientContext { } } + pub fn clear_all_inline_views(&mut self) { + self.inline_views.clear() + } + pub fn clear_inline_view(&mut self, plugin_id: &PluginId) { if let Some(index) = self.inline_views.iter().position(|(id, _)| id == plugin_id) { self.inline_views.remove(index); diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index a0caf62..f8c4449 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -1326,6 +1326,10 @@ impl AppModel { GlobalState::initial(&mut self.global_state) ); + let mut client_context = self.client_context.write().expect("lock is poisoned"); + + client_context.clear_all_inline_views(); + Command::batch(commands) } From e02e99394919ddcc07cadd3c1a639126d7a3fa57 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 6 Oct 2024 14:00:46 +0200 Subject: [PATCH 111/540] Implement ability for list to be unfocused. Action panel list is unfocused by default if panel is opened using mouse click --- rust/client/src/ui/mod.rs | 35 +++++++------ rust/client/src/ui/scroll_handle.rs | 67 ++++++++++++++++++------- rust/client/src/ui/search_list.rs | 17 ++++--- rust/client/src/ui/state/main_view.rs | 13 +++-- rust/client/src/ui/state/mod.rs | 2 +- rust/client/src/ui/state/plugin_view.rs | 12 ++--- rust/client/src/ui/widget.rs | 21 +++++--- 7 files changed, 107 insertions(+), 60 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index f8c4449..1436c77 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -108,8 +108,13 @@ pub enum AppMsg { FontLoaded(Result<(), font::Error>), ShowWindow, HideWindow, - ToggleActionPanel, - OnEntrypointAction(UiWidgetId), + ToggleActionPanel { + keyboard: bool + }, + OnEntrypointAction { + keyboard: bool, + widget_id: UiWidgetId + }, ShowPreferenceRequiredView { plugin_id: PluginId, entrypoint_id: EntrypointId, @@ -507,7 +512,7 @@ impl Application for AppModel { MainViewState::None => { match physical_key_model(physical_key, modifiers) { Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - Command::perform(async {}, |_| AppMsg::ToggleActionPanel) + Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) } Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { if let Some(search_item) = focused_search_result.get(&self.search_results) { @@ -534,7 +539,7 @@ impl Application for AppModel { MainViewState::ActionPanel { .. } => { match physical_key_model(physical_key, modifiers) { Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - Command::perform(async {}, |_| AppMsg::ToggleActionPanel) + Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) } Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { if let Some(search_item) = focused_search_result.get(&self.search_results) { @@ -564,7 +569,7 @@ impl Application for AppModel { GlobalState::PluginView { .. } => { match physical_key_model(physical_key, modifiers) { Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - Command::perform(async {}, |_| AppMsg::ToggleActionPanel) + Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) } Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { self.handle_plugin_view_keyboard_event(physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) @@ -760,14 +765,14 @@ impl Application for AppModel { #[cfg(not(target_os = "linux"))] window::close(window::Id::MAIN) } - AppMsg::ToggleActionPanel => { + AppMsg::ToggleActionPanel { keyboard } => { match &mut self.global_state { GlobalState::MainView { sub_state, focused_search_result, .. } => { match sub_state { MainViewState::None => { if let Some(search_item) = focused_search_result.get(&self.search_results) { if search_item.entrypoint_actions.len() > 0 { - MainViewState::action_panel(sub_state); + MainViewState::action_panel(sub_state, keyboard); } } } @@ -784,7 +789,7 @@ impl Application for AppModel { match sub_state { PluginViewState::None => { - PluginViewState::action_panel(sub_state) + PluginViewState::action_panel(sub_state, keyboard) } PluginViewState::ActionPanel { .. } => { PluginViewState::initial(sub_state) @@ -795,7 +800,7 @@ impl Application for AppModel { Command::none() } - AppMsg::OnEntrypointAction(widget_id) => { + AppMsg::OnEntrypointAction { keyboard, widget_id} => { match &mut self.global_state { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { @@ -843,7 +848,7 @@ impl Application for AppModel { let render_location = UiRenderLocation::View; Command::batch([ - Command::perform(async {}, |_| AppMsg::ToggleActionPanel), + Command::perform(async {}, move |_| AppMsg::ToggleActionPanel { keyboard }), Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) ]) } @@ -1182,8 +1187,8 @@ impl Application for AppModel { action_panel, None::<&ScrollHandle>, "".to_string(), - || AppMsg::ToggleActionPanel, - AppMsg::OnEntrypointAction + || AppMsg::ToggleActionPanel { keyboard: false }, + |widget_id| AppMsg::OnEntrypointAction { widget_id, keyboard: false } ) } MainViewState::ActionPanel { focused_action_item, .. } => { @@ -1196,8 +1201,8 @@ impl Application for AppModel { action_panel, Some(focused_action_item), "".to_string(), - || AppMsg::ToggleActionPanel, - AppMsg::OnEntrypointAction + || AppMsg::ToggleActionPanel { keyboard: false }, + |widget_id| AppMsg::OnEntrypointAction { widget_id, keyboard: false } ) } }; @@ -1436,7 +1441,7 @@ impl AppModel { match event { UiViewEvent::View { widget_id, event_name, event_arguments } => { let msg = match widget_event { - ComponentWidgetEvent::ActionClick { .. } => AppMsg::ToggleActionPanel, + ComponentWidgetEvent::ActionClick { .. } => AppMsg::ToggleActionPanel { keyboard: false }, _ => AppMsg::Noop }; diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs index d291061..228a441 100644 --- a/rust/client/src/ui/scroll_handle.rs +++ b/rust/client/src/ui/scroll_handle.rs @@ -4,50 +4,68 @@ use iced::widget::scrollable; use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; use crate::ui::AppMsg; +// TODO this size of item for main view list, incorrect for actions, +// but amount of actions is usually small so it is not that noticeable const ESTIMATED_ITEM_SIZE: f32 = 38.8; #[derive(Clone, Debug)] pub struct ScrollHandle { phantom: PhantomData, pub scrollable_id: scrollable::Id, - pub index: usize, - pub offset: usize, + pub index: Option, + offset: usize, } impl ScrollHandle { - pub fn new() -> ScrollHandle { + pub fn new(first_focused: bool) -> ScrollHandle { ScrollHandle { phantom: PhantomData, scrollable_id: scrollable::Id::unique(), - index: 0, + index: if first_focused { Some(0) } else { None }, offset: 0, } } pub fn reset(&mut self) { - self.index = 0; + self.index = None; self.offset = 0; } pub fn get<'a>(&self, search_results: &'a [T]) -> Option<&'a T> { - search_results.get(self.index) + match self.index { + None => None, + Some(index) => search_results.get(index) + } } - pub fn focus_next(&mut self, item_amount: usize) -> Command { + pub fn focus_next(&mut self, total_item_amount: usize) -> Command { self.offset = if self.offset < 8 { self.offset + 1 } else { 8 }; - if self.index < item_amount - 1 { - self.index = self.index + 1; + match self.index.as_mut() { + None => { + if total_item_amount > 0 { + self.index = Some(0); - let pos_y = self.index as f32 * ESTIMATED_ITEM_SIZE - (self.offset as f32 * ESTIMATED_ITEM_SIZE); + self.scroll_to(0) + } else { + Command::none() + } + } + Some(index) => { + if *index < total_item_amount - 1 { + *index = *index + 1; - scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) - } else { - Command::none() + let index = *index; + + self.scroll_to(index) + } else { + Command::none() + } + } } } @@ -58,14 +76,25 @@ impl ScrollHandle { 1 }; - if self.index > 0 { - self.index = self.index - 1; + match self.index.as_mut() { + None => Command::none(), + Some(index) => { + if *index > 0 { + *index = *index - 1; - let pos_y = self.index as f32 * ESTIMATED_ITEM_SIZE - (self.offset as f32 * ESTIMATED_ITEM_SIZE); + let index = *index; - scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) - } else { - Command::none() + self.scroll_to(index) + } else { + Command::none() + } + } } } + + fn scroll_to(&self, index: usize) -> Command { + let pos_y = index as f32 * ESTIMATED_ITEM_SIZE - (self.offset as f32 * ESTIMATED_ITEM_SIZE); + + scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) + } } \ No newline at end of file diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index b111eeb..44a685f 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -17,7 +17,7 @@ use crate::ui::theme::text::TextStyle; pub struct SearchList<'a, Message> { on_select: Box Message>, - focused_search_result: usize, + focused_search_result: Option, search_results: &'a[SearchResult], } @@ -35,7 +35,7 @@ pub struct SelectItemEvent(SearchResult); impl<'a, Message> SearchList<'a, Message> { pub fn new( search_results: &'a[SearchResult], - focused_search_result: usize, + focused_search_result: Option, on_open_view: impl Fn(SearchResult) -> Message + 'static, ) -> Self { Self { @@ -105,10 +105,15 @@ impl<'a, Message> Component for SearchList<'a, Message> .align_items(Alignment::Center) .into(); - let style = if self.focused_search_result == index { - ButtonStyle::MainListItemFocused - } else { - ButtonStyle::MainListItem + let style = match self.focused_search_result { + None => ButtonStyle::MainListItem, + Some(focused_index) => { + if focused_index == index { + ButtonStyle::MainListItemFocused + } else { + ButtonStyle::MainListItem + } + } }; button(button_content) diff --git a/rust/client/src/ui/state/main_view.rs b/rust/client/src/ui/state/main_view.rs index 21f71b5..9da06d9 100644 --- a/rust/client/src/ui/state/main_view.rs +++ b/rust/client/src/ui/state/main_view.rs @@ -21,9 +21,9 @@ impl MainViewState { *prev_state = Self::None } - pub fn action_panel(prev_state: &mut MainViewState) { + pub fn action_panel(prev_state: &mut MainViewState, focus_first: bool) { *prev_state = Self::ActionPanel { - focused_action_item: ScrollHandle::new(), + focused_action_item: ScrollHandle::new(focus_first), } } } @@ -35,9 +35,12 @@ impl Focus for MainViewState { panic!("invalid state") } MainViewState::ActionPanel { focused_action_item } => { - let widget_id = focused_action_item.index; - - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) + match focused_action_item.index { + None => Command::none(), + Some(widget_id) => { + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + } + } } } } diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 3c5a3f1..855023d 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -68,7 +68,7 @@ impl GlobalState { GlobalState::MainView { search_field_id, prompt: "".to_string(), - focused_search_result: ScrollHandle::new(), + focused_search_result: ScrollHandle::new(true), sub_state: MainViewState::new(), pending_plugin_view_data: None, } diff --git a/rust/client/src/ui/state/plugin_view.rs b/rust/client/src/ui/state/plugin_view.rs index 21dc41a..89adc38 100644 --- a/rust/client/src/ui/state/plugin_view.rs +++ b/rust/client/src/ui/state/plugin_view.rs @@ -22,9 +22,9 @@ impl PluginViewState { *prev_state = Self::None } - pub fn action_panel(prev_state: &mut PluginViewState) { + pub fn action_panel(prev_state: &mut PluginViewState, focus_first: bool) { *prev_state = Self::ActionPanel { - focused_action_item: ScrollHandle::new(), + focused_action_item: ScrollHandle::new(focus_first), } } } @@ -35,7 +35,7 @@ impl Focus for PluginViewState { PluginViewState::None => { if let Some(widget_id) = focus_list.get(0) { let widget_id = *widget_id; - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) } else { Command::none() } @@ -43,7 +43,7 @@ impl Focus for PluginViewState { PluginViewState::ActionPanel { focused_action_item, .. } => { if let Some(widget_id) = focused_action_item.get(focus_list) { let widget_id = *widget_id; - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) } else { Command::none() } @@ -56,7 +56,7 @@ impl Focus for PluginViewState { PluginViewState::None => { if let Some(widget_id) = focus_list.get(1) { let widget_id = *widget_id; - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction(widget_id)) + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) } else { Command::none() } @@ -74,7 +74,7 @@ impl Focus for PluginViewState { panic!("invalid state") } PluginViewState::ActionPanel { .. } => { - Command::perform(async {}, |_| AppMsg::ToggleActionPanel) + Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) } } } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index ae5f26a..fff5df5 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1547,10 +1547,15 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( .into() }; - let style = if action_panel_scroll_handle.index == index_counter.get() { - ButtonStyle::ActionFocused - } else { - ButtonStyle::Action + let style = match action_panel_scroll_handle.index { + None => ButtonStyle::Action, + Some(focused_index) => { + if focused_index == index_counter.get() { + ButtonStyle::ActionFocused + } else { + ButtonStyle::Action + } + } }; index_counter.set(index_counter.get() + 1); @@ -1610,7 +1615,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( action_panel: Option, action_panel_scroll_handle: Option<&ScrollHandle>, entrypoint_name: String, - on_panel_toggle: impl Fn() -> T, + on_panel_toggle_click: impl Fn() -> T, on_action_click: impl Fn(UiWidgetId) -> T, ) -> Element<'a, T> { let entrypoint_name: Element<_> = text(entrypoint_name) @@ -1679,7 +1684,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( .into(); let action_panel_toggle: Element<_> = button(action_panel_toggle_content) - .on_press(on_panel_toggle()) + .on_press(on_panel_toggle_click()) .themed(ButtonStyle::RootBottomPanelActionToggleButton); bottom_panel_content.push(action_panel_toggle); @@ -1724,7 +1729,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( content } else { mouse_area(content) - .on_press(on_panel_toggle()) + .on_press(on_panel_toggle_click()) .into() }; @@ -2023,7 +2028,7 @@ impl ComponentWidgetEvent { } ComponentWidgetEvent::ToggleActionPanel { .. } => { Some(UiViewEvent::AppEvent { - event: AppMsg::ToggleActionPanel + event: AppMsg::ToggleActionPanel { keyboard: false } }) } ComponentWidgetEvent::ListItemClick { widget_id } => { From 068cc0ddf0753c947325085ad13964675044f049 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 6 Oct 2024 14:59:26 +0200 Subject: [PATCH 112/540] Unfocus main search list if inline view is rendered --- dev_plugin/gauntlet.toml | 7 ++++++ dev_plugin/src/empty.tsx | 5 ++++ rust/client/src/ui/mod.rs | 36 +++++++++++++++++++++-------- rust/client/src/ui/scroll_handle.rs | 8 +++++-- 4 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 dev_plugin/src/empty.tsx diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index dbf235e..6ba082f 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -129,6 +129,13 @@ path = 'src/command-a.ts' type = 'command' description = '' +[[entrypoint]] +id = 'empty-entrypoint' +name = 'Empty Entrypoint' +path = 'src/empty.tsx' +type = 'view' +description = '' + [[entrypoint]] id = 'command-generator' name = 'Command generator' diff --git a/dev_plugin/src/empty.tsx b/dev_plugin/src/empty.tsx new file mode 100644 index 0000000..ce41a64 --- /dev/null +++ b/dev_plugin/src/empty.tsx @@ -0,0 +1,5 @@ +import { ReactElement } from "react"; + +export default function DetailView(): ReactElement { + return <> +} diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 1436c77..f231f22 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -97,6 +97,8 @@ pub enum AppMsg { SetSearchResults(Vec), ReplaceView { top_level_view: bool, + has_children: bool, + render_location: UiRenderLocation, }, IcedEvent(Event), WidgetEvent { @@ -297,9 +299,14 @@ impl Application for AppModel { let entrypoint_id = EntrypointId::from_string(entrypoint_id); let mut context = ClientContext::new(); + + let render_location = ui_render_location_from_scenario(render_location); + let ui_widget = ui_widget_from_scenario(container); + let has_children = ui_widget.widget_children.len() != 0; + context.replace_view( - ui_render_location_from_scenario(render_location), - ui_widget_from_scenario(container), + render_location, + ui_widget, &plugin_id, "Screenshot Plugin", &entrypoint_id, @@ -308,11 +315,11 @@ impl Application for AppModel { let context = Arc::new(StdRwLock::new(context)); - commands.push(Command::perform(async move { top_level_view }, |top_level_view| AppMsg::ReplaceView { top_level_view })); + commands.push(Command::perform(async { }, move |_| AppMsg::ReplaceView { top_level_view, has_children, render_location })); let state= match render_location { - ScenarioUiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique()), - ScenarioUiRenderLocation::View => GlobalState::new_plugin( + UiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique()), + UiRenderLocation::View => GlobalState::new_plugin( PluginViewData { top_level_view, plugin_id, @@ -420,7 +427,7 @@ impl Application for AppModel { *prompt = new_prompt.clone(); - focused_search_result.reset(); + focused_search_result.reset(true); MainViewState::initial(sub_state); } @@ -447,9 +454,16 @@ impl Application for AppModel { Command::none() } - AppMsg::ReplaceView { top_level_view } => { + AppMsg::ReplaceView { top_level_view, render_location, has_children } => { match &mut self.global_state { - GlobalState::MainView { pending_plugin_view_data, .. } => { + GlobalState::MainView { pending_plugin_view_data, focused_search_result, .. } => { + + if has_children { + if let UiRenderLocation::InlineView = render_location { + focused_search_result.unfocus(); + } + } + match pending_plugin_view_data { None => Command::none(), Some(pending_plugin_view_data) => { @@ -1579,6 +1593,8 @@ async fn request_loop( top_level_view, container } => { + let has_children = container.widget_children.len() != 0; + client_context.replace_view( render_location, container, @@ -1591,7 +1607,9 @@ async fn request_loop( responder.respond(UiResponseData::Nothing); AppMsg::ReplaceView { - top_level_view + top_level_view, + has_children, + render_location } } UiRequestData::ClearInlineView { plugin_id } => { diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs index 228a441..ca0dea7 100644 --- a/rust/client/src/ui/scroll_handle.rs +++ b/rust/client/src/ui/scroll_handle.rs @@ -26,11 +26,15 @@ impl ScrollHandle { } } - pub fn reset(&mut self) { - self.index = None; + pub fn reset(&mut self, first_focused: bool) { + self.index = if first_focused { Some(0) } else { None }; self.offset = 0; } + pub fn unfocus(&mut self) { + self.index = None; + } + pub fn get<'a>(&self, search_results: &'a [T]) -> Option<&'a T> { match self.index { None => None, From 763d7b0ac40203a8314a1ed31580879fbb049375 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:32:42 +0200 Subject: [PATCH 113/540] Implement inline view action panel. Shortcuts still todo --- dev_plugin/src/inline-view.tsx | 29 ++- docs/js/components/inline/props/actions.md | 2 + js/api/src/gen/components.tsx | 5 +- rust/client/src/ui/client_context.rs | 7 +- rust/client/src/ui/inline_view_container.rs | 85 +++++---- rust/client/src/ui/mod.rs | 192 ++++++++++++++++---- rust/client/src/ui/state/main_view.rs | 94 ++-------- rust/client/src/ui/state/mod.rs | 178 +++++++++++++----- rust/client/src/ui/state/plugin_view.rs | 92 ---------- rust/client/src/ui/widget.rs | 96 +++++++--- rust/client/src/ui/widget_container.rs | 11 +- rust/component_model/src/lib.rs | 4 +- 12 files changed, 467 insertions(+), 328 deletions(-) create mode 100644 docs/js/components/inline/props/actions.md diff --git a/dev_plugin/src/inline-view.tsx b/dev_plugin/src/inline-view.tsx index f8b4e22..2ced9b9 100644 --- a/dev_plugin/src/inline-view.tsx +++ b/dev_plugin/src/inline-view.tsx @@ -1,5 +1,6 @@ -import { Content, Icons, Inline } from "@project-gauntlet/api/components"; +import { Action, ActionPanel, Content, Icons, Inline } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Clipboard } from "@project-gauntlet/api/helpers"; export default function InlineView(props: { text: string }): ReactNode | undefined { const text = props.text; @@ -8,7 +9,31 @@ export default function InlineView(props: { text: string }): ReactNode | undefin } return ( - + + { + console.log("action test 1") + await Clipboard.writeText("Test Content") + }} + /> + { + console.log("action test 2") + }} + /> + { + console.log("action test 3") + }} + /> + + } + > Testing inline view left {text} diff --git a/docs/js/components/inline/props/actions.md b/docs/js/components/inline/props/actions.md new file mode 100644 index 0000000..d28c707 --- /dev/null +++ b/docs/js/components/inline/props/actions.md @@ -0,0 +1,2 @@ +Allows to define an Action Panel for this view +Every root component has such property \ No newline at end of file diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index e04fa92..4f4dae1 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -116,7 +116,7 @@ declare global { icon?: Icons; }; ["gauntlet:inline"]: { - children?: ElementComponent; + children?: ElementComponent; }; ["gauntlet:empty_view"]: { title: string; @@ -623,6 +623,7 @@ export const InlineSeparator: FC = (props: InlineSeparator }; export interface InlineProps { children?: ElementComponent; + actions?: ElementComponent; } export const Inline: FC & { Left: typeof Content; @@ -630,7 +631,7 @@ export const Inline: FC & { Right: typeof Content; Center: typeof Content; } = (props: InlineProps): ReactNode => { - return {props.children}; + return {props.actions as any}{props.children}; }; Inline.Left = Content; Inline.Separator = InlineSeparator; diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index b1613c6..7524753 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -5,7 +5,7 @@ use crate::ui::widget::ComponentWidgetEvent; use crate::ui::widget_container::PluginWidgetContainer; pub struct ClientContext { - inline_views: Vec<(PluginId, PluginWidgetContainer)>, + inline_views: Vec<(PluginId, PluginWidgetContainer)>, // Vec to have stable ordering view: PluginWidgetContainer, } @@ -21,6 +21,11 @@ impl ClientContext { &self.inline_views } + pub fn get_first_inline_view_container(&self) -> Option<&PluginWidgetContainer> { + self.inline_views.first() + .map(|(_, container)| container) + } + pub fn get_inline_view_container(&self, plugin_id: &PluginId) -> &PluginWidgetContainer { self.inline_views.iter() .find(|(id, _)| id == plugin_id) diff --git a/rust/client/src/ui/inline_view_container.rs b/rust/client/src/ui/inline_view_container.rs index 275bfd4..375d4fd 100644 --- a/rust/client/src/ui/inline_view_container.rs +++ b/rust/client/src/ui/inline_view_container.rs @@ -1,14 +1,15 @@ +use std::collections::HashMap; use std::sync::{Arc, RwLock}; -use iced::widget::{Component, horizontal_space}; use iced::widget::component; +use iced::widget::{horizontal_space, Component}; use common::model::UiRenderLocation; -use crate::ui::AppMsg; use crate::ui::client_context::ClientContext; use crate::ui::theme::{Element, GauntletTheme}; -use crate::ui::widget::{ComponentRenderContext, ComponentWidgetEvent}; +use crate::ui::widget::{ActionPanel, ComponentRenderContext, ComponentWidgetEvent}; +use crate::ui::AppMsg; pub struct InlineViewContainer { client_context: Arc>, @@ -20,51 +21,43 @@ pub fn inline_view_container(client_context: Arc>) -> Inli } } -#[derive(Default)] -pub struct PluginContainerState { - current_plugin: usize -} - -pub enum InlineViewContainerEvent { - WidgetEvent(ComponentWidgetEvent), -} - impl Component for InlineViewContainer { - type State = PluginContainerState; - type Event = InlineViewContainerEvent; + type State = (); + type Event = ComponentWidgetEvent; fn update( &mut self, - state: &mut Self::State, + _state: &mut Self::State, event: Self::Event, ) -> Option { - match event { - InlineViewContainerEvent::WidgetEvent(event) => { - let client_context = self.client_context.read().expect("lock is poisoned"); - let containers = client_context.get_all_inline_view_containers(); - let (plugin_id, _) = &containers[state.current_plugin]; - - Some(AppMsg::WidgetEvent { - plugin_id: plugin_id.to_owned(), - render_location: UiRenderLocation::InlineView, - widget_event: event, - }) - } - } - } - - fn view(&self, state: &Self::State) -> Element { let client_context = self.client_context.read().expect("lock is poisoned"); let containers = client_context.get_all_inline_view_containers(); - if let Some((_, container)) = &containers.get(state.current_plugin) { - container.render_widget(ComponentRenderContext::InlineRoot { - plugin_name: container.get_plugin_name(), - entrypoint_name: container.get_entrypoint_name(), - }).map(InlineViewContainerEvent::WidgetEvent) - } else { - horizontal_space() - .into() + match containers.first() { + Some((plugin_id, _)) => Some(AppMsg::WidgetEvent { + plugin_id: plugin_id.clone(), + render_location: UiRenderLocation::InlineView, + widget_event: event, + }), + None => None, + } + } + + fn view(&self, _state: &Self::State) -> Element { + let client_context = self.client_context.read().expect("lock is poisoned"); + let containers = client_context.get_all_inline_view_containers(); + + match containers.first() { + Some((_, container)) => { + container.render_widget(ComponentRenderContext::InlineRoot { + plugin_name: container.get_plugin_name(), + entrypoint_name: container.get_entrypoint_name(), + }) + } + None => { + horizontal_space() + .into() + } } } } @@ -73,4 +66,16 @@ impl<'a> From for Element<'a, AppMsg> { fn from(container: InlineViewContainer) -> Self { component(container) } -} \ No newline at end of file +} + +pub fn inline_view_action_panel(client_context: Arc>) -> Option { + let client_context = client_context.read().expect("lock is poisoned"); + let containers = client_context.get_first_inline_view_container(); + + if let Some(container) = containers { + container.get_action_panel(HashMap::new()) + } else { + None + } +} + diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index f231f22..b358456 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -32,13 +32,13 @@ use common_ui::physical_key_model; use utils::channel::{channel, RequestReceiver, RequestSender}; use crate::model::UiViewEvent; -use crate::ui::inline_view_container::inline_view_container; +use crate::ui::inline_view_container::{inline_view_action_panel, inline_view_container}; use crate::ui::search_list::search_list; use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::theme::container::ContainerStyle; use crate::ui::theme::text_input::TextInputStyle; use crate::ui::view_container::view_container; -use crate::ui::widget::{render_root, ActionPanel, ActionPanelItem, ComponentWidgetEvent}; +use crate::ui::widget::{render_root, ActionPanel, ActionPanelItem, ComponentRenderContext, ComponentWidgetEvent}; mod view_container; mod search_list; @@ -56,6 +56,7 @@ mod state; pub use theme::GauntletTheme; use crate::ui::scroll_handle::ScrollHandle; use crate::ui::state::{ErrorViewData, Focus, GlobalState, MainViewState, PluginViewData, PluginViewState}; +use crate::ui::widget_container::PluginWidgetContainer; pub struct AppModel { // logic @@ -318,7 +319,7 @@ impl Application for AppModel { commands.push(Command::perform(async { }, move |_| AppMsg::ReplaceView { top_level_view, has_children, render_location })); let state= match render_location { - UiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique()), + UiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique(), context.clone()), UiRenderLocation::View => GlobalState::new_plugin( PluginViewData { top_level_view, @@ -354,7 +355,8 @@ impl Application for AppModel { } } } else { - (Arc::new(StdRwLock::new(ClientContext::new())), GlobalState::new(text_input::Id::unique())) + let context = Arc::new(StdRwLock::new(ClientContext::new())); + (context.clone(), GlobalState::new(text_input::Id::unique(), context.clone())) }; ( @@ -512,7 +514,8 @@ impl Application for AppModel { GlobalState::MainView { sub_state, search_field_id, prompt, .. } => { match sub_state { MainViewState::None => Self::backspace_prompt(prompt, search_field_id.clone()), - MainViewState::ActionPanel { .. } => Command::none() + MainViewState::SearchResultActionPanel { .. } => Command::none(), + MainViewState::InlineViewActionPanel { .. } => Command::none() } } GlobalState::ErrorView { .. } => Command::none(), @@ -529,17 +532,58 @@ impl Application for AppModel { Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) } Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - if let Some(search_item) = focused_search_result.get(&self.search_results) { - if search_item.entrypoint_actions.len() > 0 { - self.handle_main_view_keyboard_event( - search_item.plugin_id.clone(), - search_item.entrypoint_id.clone(), + if modifier_shift || modifier_control || modifier_alt || modifier_meta { + if let Some(search_item) = focused_search_result.get(&self.search_results) { + if search_item.entrypoint_actions.len() > 0 { + self.handle_main_view_keyboard_event( + search_item.plugin_id.clone(), + search_item.entrypoint_id.clone(), + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + ) + } else { + Command::none() + } + } else { + self.handle_inline_plugin_view_keyboard_event( physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta ) + } + } else { + Self::append_prompt(prompt, text, search_field_id.clone(), modifiers) + } + } + _ => Self::append_prompt(prompt, text, search_field_id.clone(), modifiers) + } + } + MainViewState::SearchResultActionPanel { .. } => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { + Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) + } + Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { + if modifier_shift || modifier_control || modifier_alt || modifier_meta { + if let Some(search_item) = focused_search_result.get(&self.search_results) { + if search_item.entrypoint_actions.len() > 0 { + self.handle_main_view_keyboard_event( + search_item.plugin_id.clone(), + search_item.entrypoint_id.clone(), + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + ) + } else { + Command::none() + } } else { Command::none() } @@ -547,29 +591,18 @@ impl Application for AppModel { Command::none() } } - _ => Self::append_prompt(prompt, text, search_field_id.clone(), modifiers) + _ => Command::none() } } - MainViewState::ActionPanel { .. } => { + MainViewState::InlineViewActionPanel { .. } => { match physical_key_model(physical_key, modifiers) { Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) } Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - if let Some(search_item) = focused_search_result.get(&self.search_results) { - if search_item.entrypoint_actions.len() > 0 { - self.handle_main_view_keyboard_event( - search_item.plugin_id.clone(), - search_item.entrypoint_id.clone(), - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta - ) - } else { - Command::none() - } + if modifier_shift || modifier_control || modifier_alt || modifier_meta { + // TODO + Command::none() } else { Command::none() } @@ -586,7 +619,11 @@ impl Application for AppModel { Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) } Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - self.handle_plugin_view_keyboard_event(physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + if modifier_shift || modifier_control || modifier_alt || modifier_meta { + self.handle_plugin_view_keyboard_event(physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + } else { + Command::none() + } } _ => Command::none() } @@ -786,11 +823,18 @@ impl Application for AppModel { MainViewState::None => { if let Some(search_item) = focused_search_result.get(&self.search_results) { if search_item.entrypoint_actions.len() > 0 { - MainViewState::action_panel(sub_state, keyboard); + MainViewState::search_result_action_panel(sub_state, keyboard); + } + } else { + if let Some(_) = inline_view_action_panel(self.client_context.clone()) { + MainViewState::inline_result_action_panel(sub_state, keyboard); } } } - MainViewState::ActionPanel { .. } => { + MainViewState::SearchResultActionPanel { .. } => { + MainViewState::initial(sub_state); + } + MainViewState::InlineViewActionPanel { .. } => { MainViewState::initial(sub_state); } } @@ -819,7 +863,7 @@ impl Application for AppModel { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { MainViewState::None => Command::none(), - MainViewState::ActionPanel { .. } => { + MainViewState::SearchResultActionPanel { .. } => { if let Some(search_item) = focused_search_result.get(&self.search_results) { MainViewState::initial(sub_state); @@ -833,6 +877,30 @@ impl Application for AppModel { Command::none() } } + MainViewState::InlineViewActionPanel { .. } => { + let client_context = self.client_context.read().expect("lock is poisoned"); + + match client_context.get_first_inline_view_container() { + Some(container) => { + let plugin_id = container.get_plugin_id(); + + let action_ids = container.get_action_ids(); + + let widget_id = action_ids[widget_id]; + + let widget_event = ComponentWidgetEvent::RunAction { + widget_id, + }; + let render_location = UiRenderLocation::InlineView; + + Command::batch([ + Command::perform(async {}, move |_| AppMsg::ToggleActionPanel { keyboard }), + Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + ]) + } + None => Command::none() + } + } } } GlobalState::ErrorView { .. } => Command::none(), @@ -1187,7 +1255,25 @@ impl Application for AppModel { (Some((label, default_shortcut)), Some(action_panel)) } } else { - (None, None) + match inline_view_action_panel(self.client_context.clone()) { + None => (None, None), + Some(action_panel) => { + match action_panel.find_first() { + None => (None, None), + Some(action) => { + let shortcut = PhysicalShortcut { + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false + }; + + (Some((action, shortcut)), Some(action_panel)) + } + } + } + } }; let root = match sub_state { @@ -1205,7 +1291,21 @@ impl Application for AppModel { |widget_id| AppMsg::OnEntrypointAction { widget_id, keyboard: false } ) } - MainViewState::ActionPanel { focused_action_item, .. } => { + MainViewState::SearchResultActionPanel { focused_action_item, .. } => { + render_root( + true, + input, + separator, + content, + primary_action, + action_panel, + Some(focused_action_item), + "".to_string(), + || AppMsg::ToggleActionPanel { keyboard: false }, + |widget_id| AppMsg::OnEntrypointAction { widget_id, keyboard: false } + ) + } + MainViewState::InlineViewActionPanel { focused_action_item, .. } => { render_root( true, input, @@ -1342,7 +1442,7 @@ impl AppModel { } commands.push( - GlobalState::initial(&mut self.global_state) + GlobalState::initial(&mut self.global_state, self.client_context.clone()) ); let mut client_context = self.client_context.write().expect("lock is poisoned"); @@ -1385,7 +1485,7 @@ impl AppModel { fn reset_window_state(&mut self) -> Command { let mut commands = vec![ - GlobalState::initial(&mut self.global_state), + GlobalState::initial(&mut self.global_state, self.client_context.clone()), ]; if !self.wayland { @@ -1511,6 +1611,30 @@ impl AppModel { ) } + fn handle_inline_plugin_view_keyboard_event(&self, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Command { + let mut backend_client = self.backend_api.clone(); + + let (plugin_id, entrypoint_id) = { + let client_context = self.client_context.read().expect("lock is poisoned"); + match client_context.get_first_inline_view_container() { + None => { + return Command::none() + }, + Some(container) => (container.get_plugin_id(), container.get_entrypoint_id()) + } + }; + + Command::perform( + async move { + backend_client.send_keyboard_event(plugin_id, entrypoint_id, KeyboardEventOrigin::PluginView, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + .await?; + + Ok(()) + }, + |result| handle_backend_error(result, |()| AppMsg::Noop), + ) + } + fn search(&self, new_prompt: String, render_inline_view: bool) -> Command { let mut backend_api = self.backend_api.clone(); diff --git a/rust/client/src/ui/state/main_view.rs b/rust/client/src/ui/state/main_view.rs index 9da06d9..c72e080 100644 --- a/rust/client/src/ui/state/main_view.rs +++ b/rust/client/src/ui/state/main_view.rs @@ -1,14 +1,15 @@ use crate::ui::scroll_handle::ScrollHandle; -use crate::ui::state::Focus; -use crate::ui::AppMsg; -use common::model::SearchResultEntrypointAction; -use iced::Command; +use common::model::{SearchResultEntrypointAction, UiWidgetId}; pub enum MainViewState { None, - ActionPanel { + SearchResultActionPanel { // ephemeral state focused_action_item: ScrollHandle, + }, + InlineViewActionPanel { + // ephemeral state + focused_action_item: ScrollHandle, } } @@ -21,82 +22,15 @@ impl MainViewState { *prev_state = Self::None } - pub fn action_panel(prev_state: &mut MainViewState, focus_first: bool) { - *prev_state = Self::ActionPanel { + pub fn search_result_action_panel(prev_state: &mut MainViewState, focus_first: bool) { + *prev_state = Self::SearchResultActionPanel { + focused_action_item: ScrollHandle::new(focus_first), + } + } + + pub fn inline_result_action_panel(prev_state: &mut MainViewState, focus_first: bool) { + *prev_state = Self::InlineViewActionPanel { focused_action_item: ScrollHandle::new(focus_first), } } } - -impl Focus for MainViewState { - fn primary(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { - match self { - MainViewState::None => { - panic!("invalid state") - } - MainViewState::ActionPanel { focused_action_item } => { - match focused_action_item.index { - None => Command::none(), - Some(widget_id) => { - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) - } - } - } - } - } - - fn secondary(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { - // secondary action doesn't do anything when action panel is open - panic!("invalid state") - } - - fn back(&mut self) -> Command { - match self { - MainViewState::None => { - Command::perform(async {}, |_| AppMsg::HideWindow) - } - MainViewState::ActionPanel { .. } => { - MainViewState::initial(self); - Command::none() - } - } - } - - fn next(&mut self) -> Command { - todo!() - } - - fn previous(&mut self) -> Command { - todo!() - } - - fn up(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { - match self { - MainViewState::None => Command::none(), - MainViewState::ActionPanel { focused_action_item, .. } => { - focused_action_item.focus_previous() - } - } - } - - fn down(&mut self, focus_list: &[SearchResultEntrypointAction]) -> Command { - match self { - MainViewState::None => Command::none(), - MainViewState::ActionPanel { focused_action_item } => { - if focus_list.len() != 0 { - focused_action_item.focus_next(focus_list.len() + 1) - } else { - Command::none() - } - } - } - } - - fn left(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { - todo!() - } - - fn right(&mut self, _focus_list: &[SearchResultEntrypointAction]) -> Command { - todo!() - } -} diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 855023d..66f9bd0 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -12,6 +12,7 @@ use iced::widget::text_input::focus; use iced::Command; use std::collections::HashMap; use std::sync::{Arc, RwLock as StdRwLock}; +use crate::ui::inline_view_container::inline_view_action_panel; pub enum GlobalState { MainView { @@ -23,6 +24,7 @@ pub enum GlobalState { focused_search_result: ScrollHandle, // state + client_context: Arc>, sub_state: MainViewState, pending_plugin_view_data: Option, }, @@ -64,13 +66,14 @@ pub enum ErrorViewData { } impl GlobalState { - pub fn new(search_field_id: text_input::Id) -> GlobalState { + pub fn new(search_field_id: text_input::Id, client_context: Arc>) -> GlobalState { GlobalState::MainView { search_field_id, prompt: "".to_string(), focused_search_result: ScrollHandle::new(true), sub_state: MainViewState::new(), pending_plugin_view_data: None, + client_context, } } @@ -88,10 +91,10 @@ impl GlobalState { } } - pub fn initial(prev_global_state: &mut GlobalState) -> Command { + pub fn initial(prev_global_state: &mut GlobalState, client_context: Arc>) -> Command { let search_field_id = text_input::Id::unique(); - *prev_global_state = GlobalState::new(search_field_id.clone()); + *prev_global_state = GlobalState::new(search_field_id.clone(), client_context); Command::batch([ focus(search_field_id), @@ -133,19 +136,37 @@ pub trait Focus { impl Focus for GlobalState { fn primary(&mut self, focus_list: &[SearchResult]) -> Command { match self { - GlobalState::MainView { focused_search_result, sub_state, .. } => { - if let Some(search_item) = focused_search_result.get(focus_list) { - match sub_state { - MainViewState::None => { + GlobalState::MainView { focused_search_result, sub_state, client_context, .. } => { + match sub_state { + MainViewState::None => { + if let Some(search_item) = focused_search_result.get(focus_list) { let search_item = search_item.clone(); Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) - } - MainViewState::ActionPanel { .. } => { - sub_state.primary(&search_item.entrypoint_actions) + } else { + if let Some(_) = inline_view_action_panel(client_context.clone()) { + // TODO + Command::none() + } else { + Command::none() + } + } + } + MainViewState::SearchResultActionPanel { focused_action_item, .. } => { + match focused_action_item.index { + None => Command::none(), + Some(widget_id) => { + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + } + } + } + MainViewState::InlineViewActionPanel { focused_action_item } => { + match focused_action_item.index { + None => Command::none(), + Some(widget_id) => { + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + } } } - } else { - Command::none() } } GlobalState::PluginView { sub_state, client_context, .. } => { @@ -153,7 +174,24 @@ impl Focus for GlobalState { let action_ids = client_context.get_action_ids(); - sub_state.primary(&action_ids) + match sub_state { + PluginViewState::None => { + if let Some(widget_id) = action_ids.get(0) { + let widget_id = *widget_id; + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + } else { + Command::none() + } + }, + PluginViewState::ActionPanel { focused_action_item, .. } => { + if let Some(widget_id) = focused_action_item.get(&action_ids) { + let widget_id = *widget_id; + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + } else { + Command::none() + } + } + } } GlobalState::ErrorView { .. } => Command::none() } @@ -161,19 +199,25 @@ impl Focus for GlobalState { fn secondary(&mut self, focus_list: &[SearchResult]) -> Command { match self { - GlobalState::MainView { focused_search_result, sub_state, .. } => { - if let Some(search_item) = focused_search_result.get(focus_list) { - match sub_state { - MainViewState::None => { + GlobalState::MainView { focused_search_result, sub_state, client_context, .. } => { + match sub_state { + MainViewState::None => { + if let Some(search_item) = focused_search_result.get(focus_list) { let search_item = search_item.clone(); Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, Some(0))) - } - MainViewState::ActionPanel { .. } => { - Command::none() + } else { + if let Some(_) = inline_view_action_panel(client_context.clone()) { + // TODO + Command::none() + } else { + Command::none() + } } } - } else { - Command::none() + MainViewState::SearchResultActionPanel { .. } | MainViewState::InlineViewActionPanel { .. } => { + // secondary does nothing when action panel is opened + Command::none() + } } } GlobalState::PluginView { sub_state, client_context, .. } => { @@ -181,7 +225,20 @@ impl Focus for GlobalState { let action_ids = client_context.get_action_ids(); - sub_state.secondary(&action_ids) + match sub_state { + PluginViewState::None => { + if let Some(widget_id) = action_ids.get(1) { + let widget_id = *widget_id; + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + } else { + Command::none() + } + }, + PluginViewState::ActionPanel { .. } => { + // secondary does nothing when action panel is opened + Command::none() + } + } } GlobalState::ErrorView { .. } => Command::none() } @@ -190,7 +247,19 @@ impl Focus for GlobalState { fn back(&mut self) -> Command { match self { GlobalState::MainView { sub_state, .. } => { - sub_state.back() + match sub_state { + MainViewState::None => { + Command::perform(async {}, |_| AppMsg::HideWindow) + } + MainViewState::SearchResultActionPanel { .. } => { + MainViewState::initial(sub_state); + Command::none() + } + MainViewState::InlineViewActionPanel { .. } => { + MainViewState::initial(sub_state); + Command::none() + } + } } GlobalState::PluginView { plugin_view_data: PluginViewData { @@ -200,16 +269,18 @@ impl Focus for GlobalState { .. }, sub_state, - .. + client_context } => { match sub_state { PluginViewState::None => { if *top_level_view { let plugin_id = plugin_id.clone(); + let client_context = client_context.clone(); + Command::batch([ Command::perform(async {}, |_| AppMsg::ClosePluginView(plugin_id)), - GlobalState::initial(self) + GlobalState::initial(self, client_context) ]) } else { let plugin_id = plugin_id.clone(); @@ -218,7 +289,7 @@ impl Focus for GlobalState { } } PluginViewState::ActionPanel { .. } => { - sub_state.back() + Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) } } } @@ -241,32 +312,27 @@ impl Focus for GlobalState { GlobalState::ErrorView { .. } => Command::none(), } } - fn up(&mut self, focus_list: &[SearchResult]) -> Command { + fn up(&mut self, _focus_list: &[SearchResult]) -> Command { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { MainViewState::None => { focused_search_result.focus_previous() } - MainViewState::ActionPanel { .. } => { - if let Some(search_item) = focused_search_result.get(focus_list) { - sub_state.up(&search_item.entrypoint_actions) - } else { - Command::none() - } + MainViewState::SearchResultActionPanel { focused_action_item } => { + focused_action_item.focus_previous() + } + MainViewState::InlineViewActionPanel { focused_action_item } => { + focused_action_item.focus_previous() } } } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { sub_state, client_context, .. } => { + GlobalState::PluginView { sub_state, .. } => { match sub_state { PluginViewState::None => Command::none(), - PluginViewState::ActionPanel { .. } => { - let client_context = client_context.read().expect("lock is poisoned"); - - let action_ids = client_context.get_action_ids(); - - sub_state.up(&action_ids) + PluginViewState::ActionPanel { focused_action_item } => { + focused_action_item.focus_previous() } } }, @@ -274,7 +340,7 @@ impl Focus for GlobalState { } fn down(&mut self, focus_list: &[SearchResult]) -> Command { match self { - GlobalState::MainView { focused_search_result, sub_state, .. } => { + GlobalState::MainView { focused_search_result, sub_state, client_context, .. } => { match sub_state { MainViewState::None => { if focus_list.len() != 0 { @@ -283,25 +349,45 @@ impl Focus for GlobalState { Command::none() } } - MainViewState::ActionPanel { .. } => { + MainViewState::SearchResultActionPanel { focused_action_item } => { if let Some(search_item) = focused_search_result.get(focus_list) { - sub_state.down(&search_item.entrypoint_actions) + if search_item.entrypoint_actions.len() != 0 { + focused_action_item.focus_next(search_item.entrypoint_actions.len() + 1) + } else { + Command::none() + } } else { Command::none() } } + MainViewState::InlineViewActionPanel { focused_action_item } => { + match inline_view_action_panel(client_context.clone()) { + Some(action_panel) => { + if action_panel.action_count() != 0 { + focused_action_item.focus_next(action_panel.action_count()) + } else { + Command::none() + } + } + None => Command::none() + } + } } } GlobalState::ErrorView { .. } => Command::none(), GlobalState::PluginView { sub_state, client_context, .. } => { match sub_state { PluginViewState::None => Command::none(), - PluginViewState::ActionPanel { .. } => { + PluginViewState::ActionPanel { focused_action_item } => { let client_context = client_context.read().expect("lock is poisoned"); let action_ids = client_context.get_action_ids(); - sub_state.down(&action_ids) + if action_ids.len() != 0 { + focused_action_item.focus_next(action_ids.len()) + } else { + Command::none() + } } } } diff --git a/rust/client/src/ui/state/plugin_view.rs b/rust/client/src/ui/state/plugin_view.rs index 89adc38..72e8044 100644 --- a/rust/client/src/ui/state/plugin_view.rs +++ b/rust/client/src/ui/state/plugin_view.rs @@ -1,8 +1,5 @@ use crate::ui::scroll_handle::ScrollHandle; -use crate::ui::state::Focus; -use crate::ui::AppMsg; use common::model::UiWidgetId; -use iced::Command; #[derive(Debug, Clone)] pub enum PluginViewState { @@ -28,92 +25,3 @@ impl PluginViewState { } } } - -impl Focus for PluginViewState { - fn primary(&mut self, focus_list: &[UiWidgetId]) -> Command { - match self { - PluginViewState::None => { - if let Some(widget_id) = focus_list.get(0) { - let widget_id = *widget_id; - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) - } else { - Command::none() - } - }, - PluginViewState::ActionPanel { focused_action_item, .. } => { - if let Some(widget_id) = focused_action_item.get(focus_list) { - let widget_id = *widget_id; - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) - } else { - Command::none() - } - } - } - } - - fn secondary(&mut self, focus_list: &[UiWidgetId]) -> Command { - match self { - PluginViewState::None => { - if let Some(widget_id) = focus_list.get(1) { - let widget_id = *widget_id; - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) - } else { - Command::none() - } - }, - PluginViewState::ActionPanel { .. } => { - // secondary does nothing when action panel is opened - Command::none() - } - } - } - - fn back(&mut self) -> Command { - match self { - PluginViewState::None => { - panic!("invalid state") - } - PluginViewState::ActionPanel { .. } => { - Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) - } - } - } - - fn next(&mut self) -> Command { - todo!() - } - - fn previous(&mut self) -> Command { - todo!() - } - - fn up(&mut self, _focus_list: &[UiWidgetId]) -> Command { - match self { - PluginViewState::None => Command::none(), - PluginViewState::ActionPanel { focused_action_item, .. } => { - focused_action_item.focus_previous() - } - } - } - - fn down(&mut self, focus_list: &[UiWidgetId]) -> Command { - match self { - PluginViewState::None => Command::none(), - PluginViewState::ActionPanel { focused_action_item } => { - if focus_list.len() != 0 { - focused_action_item.focus_next(focus_list.len()) - } else { - Command::none() - } - } - } - } - - fn left(&mut self, _focus_list: &[UiWidgetId]) -> Command { - todo!() - } - - fn right(&mut self, _focus_list: &[UiWidgetId]) -> Command { - todo!() - } -} diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index fff5df5..906d73a 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -191,9 +191,22 @@ impl ComponentWidgetWrapper { self.get_children() .unwrap_or(vec![]) - .iter() + .into_iter() .find_map(|child| child.find_child_with_id(widget_id)) - .map(|widget| widget.clone()) + } + + pub fn find_child_by_type(&self, predicate: &dyn Fn(&ComponentWidget) -> bool) -> Option { + self.get_children() + .unwrap_or(vec![]) + .into_iter() + .find_map(|child| { + let (widget, _) = &*child.get(); + if predicate(widget) { + Some(child.clone()) + } else { + child.find_child_by_type(&predicate) + } + }) } pub fn toggle_action_panel(&self) { @@ -255,6 +268,12 @@ impl ComponentWidgetWrapper { self.inner.write().expect("lock is poisoned") } + pub fn get_action_panel(&self, action_shortcuts: HashMap) -> Option { + self.find_child_by_type(&|widget| matches!(widget, ComponentWidget::ActionPanel { .. })) + .map(|widget| convert_action_panel(&[widget], action_shortcuts)) + .flatten() + } + pub fn render_widget<'a>(&self, context: ComponentRenderContext) -> Element<'a, ComponentWidgetEvent> { let widget_id = self.id; let (widget, state) = &*self.get(); @@ -740,21 +759,23 @@ impl ComponentWidgetWrapper { let content: Vec> = children .into_iter() - .map(|child| { + .filter_map(|child| { let (widget, _) = &*child.get(); match widget { ComponentWidget::InlineSeparator { .. } => { - child.render_widget(ComponentRenderContext::None) + Some(child.render_widget(ComponentRenderContext::None)) } ComponentWidget::Content { .. } => { let element = child.render_widget(ComponentRenderContext::Inline); - container(element) + let container = container(element) .width(Length::Fill) - .into() + .into(); + + Some(container) } - _ => panic!("unexpected widget kind {:?}", widget) + _ => None } }) .collect(); @@ -1329,26 +1350,7 @@ fn render_plugin_root<'a>( let mut action_panel = convert_action_panel(children, action_shortcuts); let primary_action = action_panel.as_mut() - .map(|panel| { - fn find_first(items: &[ActionPanelItem]) -> Option { - for item in items { - match item { - ActionPanelItem::Action { label, .. } => { - return Some(label.to_string()) - } - ActionPanelItem::ActionSection { items, .. } => { - if let Some(item) = find_first(items) { - return Some(item) - } - } - } - } - - None - } - - find_first(&panel.items) - }) + .map(|panel| panel.find_first()) .flatten() .map(|label| { let shortcut = PhysicalShortcut { @@ -1394,11 +1396,23 @@ fn render_plugin_root<'a>( } } +#[derive(Debug)] pub struct ActionPanel { pub title: Option, pub items: Vec } +impl ActionPanel { + pub fn action_count(&self) -> usize { + self.items.iter().map(|item| item.action_count()).sum() + } + + pub fn find_first(&self) -> Option { + ActionPanelItem::find_first(&self.items) + } +} + +#[derive(Debug)] pub enum ActionPanelItem { Action { label: String, @@ -1411,6 +1425,34 @@ pub enum ActionPanelItem { } } +impl ActionPanelItem { + fn action_count(&self) -> usize { + match self { + ActionPanelItem::Action { .. } => 1, + ActionPanelItem::ActionSection { items, .. } => { + items.iter().map(|item| item.action_count()).sum() + } + } + } + + fn find_first(items: &[ActionPanelItem]) -> Option { + for item in items { + match item { + ActionPanelItem::Action { label, .. } => { + return Some(label.to_string()) + } + ActionPanelItem::ActionSection { items, .. } => { + if let Some(item) = Self::find_first(items) { + return Some(item) + } + } + } + } + + None + } +} + fn convert_action_panel(children: &[ComponentWidgetWrapper], action_shortcuts: HashMap) -> Option { let action_panel: Vec<_> = children .into_iter() diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 29c10f5..691c84f 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -1,8 +1,9 @@ -use common::model::{EntrypointId, PluginId, UiWidget, UiWidgetId}; +use std::collections::HashMap; +use common::model::{EntrypointId, PhysicalShortcut, PluginId, UiWidget, UiWidgetId}; use crate::model::UiViewEvent; - +use crate::ui::scroll_handle::ScrollHandle; use crate::ui::theme::Element; -use crate::ui::widget::{ComponentRenderContext, ComponentWidgetEvent, ComponentWidgetWrapper}; +use crate::ui::widget::{ActionPanel, ComponentRenderContext, ComponentWidgetEvent, ComponentWidgetWrapper}; pub struct PluginWidgetContainer { root_widget: ComponentWidgetWrapper, @@ -39,6 +40,10 @@ impl PluginWidgetContainer { self.entrypoint_name.clone().expect("entrypoint id should always exist after render") } + pub fn get_action_panel(&self, action_shortcuts: HashMap) -> Option { + self.root_widget.get_action_panel(action_shortcuts) + } + pub fn render_widget<'a>(&self, context: ComponentRenderContext) -> Element<'a, ComponentWidgetEvent> { self.root_widget.render_widget(context) } diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index 3849612..04161ad 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -965,7 +965,9 @@ pub fn create_component_model() -> Vec { "inline", mark_doc!("/inline/description.md"), "Inline", - [], + [ + property("actions", mark_doc!("/inline/props/actions.md"), true, component_ref(&action_panel_component)), + ], children_members([ member("Left", &content_component), member("Separator", &inline_separator_component), From 2cac027a2618951e8e089243b488651a1b8b7137 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:04:46 +0200 Subject: [PATCH 114/540] Implement primary and secondary shortcuts for inline view actions --- rust/client/src/ui/mod.rs | 24 ++++++++++++++++++----- rust/client/src/ui/state/mod.rs | 34 ++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index b358456..273fed1 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -862,7 +862,25 @@ impl Application for AppModel { match &mut self.global_state { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { - MainViewState::None => Command::none(), + MainViewState::None => { + let client_context = self.client_context.read().expect("lock is poisoned"); + + match client_context.get_first_inline_view_container() { + Some(container) => { + let plugin_id = container.get_plugin_id(); + + let widget_event = ComponentWidgetEvent::RunAction { + widget_id, + }; + let render_location = UiRenderLocation::InlineView; + + Command::batch([ + Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + ]) + } + None => Command::none() + } + }, MainViewState::SearchResultActionPanel { .. } => { if let Some(search_item) = focused_search_result.get(&self.search_results) { MainViewState::initial(sub_state); @@ -884,10 +902,6 @@ impl Application for AppModel { Some(container) => { let plugin_id = container.get_plugin_id(); - let action_ids = container.get_action_ids(); - - let widget_id = action_ids[widget_id]; - let widget_event = ComponentWidgetEvent::RunAction { widget_id, }; diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 66f9bd0..f6835b5 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -2,6 +2,7 @@ mod main_view; mod plugin_view; use crate::ui::client_context::ClientContext; +use crate::ui::inline_view_container::inline_view_action_panel; use crate::ui::scroll_handle::ScrollHandle; pub use crate::ui::state::main_view::MainViewState; pub use crate::ui::state::plugin_view::PluginViewState; @@ -12,7 +13,6 @@ use iced::widget::text_input::focus; use iced::Command; use std::collections::HashMap; use std::sync::{Arc, RwLock as StdRwLock}; -use crate::ui::inline_view_container::inline_view_action_panel; pub enum GlobalState { MainView { @@ -143,9 +143,19 @@ impl Focus for GlobalState { let search_item = search_item.clone(); Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) } else { - if let Some(_) = inline_view_action_panel(client_context.clone()) { - // TODO - Command::none() + let client_context = client_context.read().expect("lock is poisoned"); + + if let Some(container) = client_context.get_first_inline_view_container() { + let action_ids = container.get_action_ids(); + + match action_ids.get(0) { + Some(widget_id) => { + let widget_id = *widget_id; + + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + } + None => Command::none() + } } else { Command::none() } @@ -206,9 +216,19 @@ impl Focus for GlobalState { let search_item = search_item.clone(); Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, Some(0))) } else { - if let Some(_) = inline_view_action_panel(client_context.clone()) { - // TODO - Command::none() + let client_context = client_context.read().expect("lock is poisoned"); + + if let Some(container) = client_context.get_first_inline_view_container() { + let action_ids = container.get_action_ids(); + + match action_ids.get(1) { + Some(widget_id) => { + let widget_id = *widget_id; + + Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + } + None => Command::none() + } } else { Command::none() } From 0a92457bd4bca69b673e3ba1cea9c3ff6e44045e Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 7 Oct 2024 19:55:23 +0200 Subject: [PATCH 115/540] Implement inline view shortcut display in action panel --- dev_plugin/gauntlet.toml | 7 +++- dev_plugin/src/inline-view.tsx | 1 + rust/client/src/ui/client_context.rs | 22 +++++++++- rust/client/src/ui/inline_view_container.rs | 8 +--- rust/client/src/ui/mod.rs | 41 +++++++++++++++++-- rust/client/src/ui/widget.rs | 8 ++-- rust/client/src/ui/widget_container.rs | 2 +- rust/common/src/model.rs | 4 ++ rust/common/src/rpc/backend_api.rs | 10 +++++ rust/server/src/lib.rs | 6 +++ rust/server/src/plugins/data_db_repository.rs | 22 +++++++++- rust/server/src/plugins/mod.rs | 10 +++++ 12 files changed, 121 insertions(+), 20 deletions(-) diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 6ba082f..a684898 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -101,6 +101,11 @@ path = 'src/inline-view.tsx' type = 'inline-view' description = '' +[[entrypoint.actions]] +id = 'testInlineAction' +description = "test action description 1" +shortcut = { key = 'b', kind = 'main'} + [[entrypoint]] id = 'grid-view' name = 'Grid view' @@ -146,7 +151,7 @@ description = '' [[entrypoint.actions]] id = 'testGeneratedAction1' description = "test action description 1" -shortcut = { key = 'a', kind = 'main'} +shortcut = { key = 'b', kind = 'main'} [[entrypoint]] diff --git a/dev_plugin/src/inline-view.tsx b/dev_plugin/src/inline-view.tsx index 2ced9b9..a112fee 100644 --- a/dev_plugin/src/inline-view.tsx +++ b/dev_plugin/src/inline-view.tsx @@ -26,6 +26,7 @@ export default function InlineView(props: { text: string }): ReactNode | undefin }} /> { console.log("action test 3") diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index 7524753..ab6b7d3 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -1,11 +1,13 @@ -use common::model::{EntrypointId, PluginId, UiRenderLocation, UiWidget, UiWidgetId}; +use std::collections::HashMap; +use common::model::{EntrypointId, PhysicalShortcut, PluginId, UiRenderLocation, UiWidget, UiWidgetId}; use crate::model::UiViewEvent; -use crate::ui::widget::ComponentWidgetEvent; +use crate::ui::widget::{ActionPanel, ComponentWidgetEvent}; use crate::ui::widget_container::PluginWidgetContainer; pub struct ClientContext { inline_views: Vec<(PluginId, PluginWidgetContainer)>, // Vec to have stable ordering + inline_view_shortcuts: HashMap>, view: PluginWidgetContainer, } @@ -13,6 +15,7 @@ impl ClientContext { pub fn new() -> Self { Self { inline_views: vec![], + inline_view_shortcuts: HashMap::new(), view: PluginWidgetContainer::new(), } } @@ -26,6 +29,17 @@ impl ClientContext { .map(|(_, container)| container) } + pub fn get_first_inline_view_action_panel(&self) -> Option { + self.get_first_inline_view_container() + .map(|container| { + match self.inline_view_shortcuts.get(&container.get_plugin_id()) { + None => container.get_action_panel(&HashMap::new()), + Some(shortcuts) => container.get_action_panel(shortcuts), + } + }) + .flatten() + } + pub fn get_inline_view_container(&self, plugin_id: &PluginId) -> &PluginWidgetContainer { self.inline_views.iter() .find(|(id, _)| id == plugin_id) @@ -67,6 +81,10 @@ impl ClientContext { } } + pub fn set_inline_view_shortcuts(&mut self, shortcuts: HashMap>) { + self.inline_view_shortcuts = shortcuts; + } + pub fn clear_all_inline_views(&mut self) { self.inline_views.clear() } diff --git a/rust/client/src/ui/inline_view_container.rs b/rust/client/src/ui/inline_view_container.rs index 375d4fd..ae41272 100644 --- a/rust/client/src/ui/inline_view_container.rs +++ b/rust/client/src/ui/inline_view_container.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::sync::{Arc, RwLock}; use iced::widget::component; @@ -70,12 +69,7 @@ impl<'a> From for Element<'a, AppMsg> { pub fn inline_view_action_panel(client_context: Arc>) -> Option { let client_context = client_context.read().expect("lock is poisoned"); - let containers = client_context.get_first_inline_view_container(); - if let Some(container) = containers { - container.get_action_panel(HashMap::new()) - } else { - None - } + client_context.get_first_inline_view_action_panel() } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 273fed1..4701acd 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -149,6 +149,9 @@ pub enum AppMsg { ShowBackendError(BackendForFrontendApiError), ClosePluginView(PluginId), OpenPluginView(PluginId, EntrypointId), + InlineViewShortcuts { + shortcuts: HashMap> + }, } pub struct AppFlags { @@ -466,7 +469,7 @@ impl Application for AppModel { } } - match pending_plugin_view_data { + let command = match pending_plugin_view_data { None => Command::none(), Some(pending_plugin_view_data) => { let pending_plugin_view_data = pending_plugin_view_data.clone(); @@ -479,6 +482,15 @@ impl Application for AppModel { self.client_context.clone() ) } + }; + + if let UiRenderLocation::InlineView = render_location { + Command::batch([ + command, + self.inline_view_shortcuts() + ]) + } else { + command } } GlobalState::ErrorView { .. } => Command::none(), @@ -601,8 +613,13 @@ impl Application for AppModel { } Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { if modifier_shift || modifier_control || modifier_alt || modifier_meta { - // TODO - Command::none() + self.handle_inline_plugin_view_keyboard_event( + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + ) } else { Command::none() } @@ -826,7 +843,8 @@ impl Application for AppModel { MainViewState::search_result_action_panel(sub_state, keyboard); } } else { - if let Some(_) = inline_view_action_panel(self.client_context.clone()) { + let client_context = self.client_context.read().expect("lock is poisoned"); + if let Some(_) = client_context.get_first_inline_view_container() { MainViewState::inline_result_action_panel(sub_state, keyboard); } } @@ -958,6 +976,13 @@ impl Application for AppModel { AppMsg::ClosePluginView(plugin_id) => { self.close_plugin_view(plugin_id) } + AppMsg::InlineViewShortcuts { shortcuts } => { + let mut client_context = self.client_context.write().expect("lock is poisoned"); + + client_context.set_inline_view_shortcuts(shortcuts); + + Command::none() + } } } @@ -1670,6 +1695,14 @@ impl AppModel { Ok(()) }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } + + fn inline_view_shortcuts(&self) -> Command { + let mut backend_api = self.backend_api.clone(); + + Command::perform(async move { + backend_api.inline_view_shortcuts().await + }, |result| handle_backend_error(result, |shortcuts| AppMsg::InlineViewShortcuts { shortcuts })) + } } // these are needed to force focus the text_input in main search view when diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 906d73a..2c31c8e 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -268,7 +268,7 @@ impl ComponentWidgetWrapper { self.inner.write().expect("lock is poisoned") } - pub fn get_action_panel(&self, action_shortcuts: HashMap) -> Option { + pub fn get_action_panel(&self, action_shortcuts: &HashMap) -> Option { self.find_child_by_type(&|widget| matches!(widget, ComponentWidget::ActionPanel { .. })) .map(|widget| convert_action_panel(&[widget], action_shortcuts)) .flatten() @@ -1347,7 +1347,7 @@ fn render_plugin_root<'a>( .into() }; - let mut action_panel = convert_action_panel(children, action_shortcuts); + let mut action_panel = convert_action_panel(children, &action_shortcuts); let primary_action = action_panel.as_mut() .map(|panel| panel.find_first()) @@ -1453,7 +1453,7 @@ impl ActionPanelItem { } } -fn convert_action_panel(children: &[ComponentWidgetWrapper], action_shortcuts: HashMap) -> Option { +fn convert_action_panel(children: &[ComponentWidgetWrapper], action_shortcuts: &HashMap) -> Option { let action_panel: Vec<_> = children .into_iter() .filter(|child| { @@ -1509,7 +1509,7 @@ fn convert_action_panel(children: &[ComponentWidgetWrapper], action_shortcuts: H Some(ActionPanel { title: title.clone(), - items: convert_to_items(children, &action_shortcuts), + items: convert_to_items(children, action_shortcuts), }) } _ => None diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 691c84f..00ddc01 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -40,7 +40,7 @@ impl PluginWidgetContainer { self.entrypoint_name.clone().expect("entrypoint id should always exist after render") } - pub fn get_action_panel(&self, action_shortcuts: HashMap) -> Option { + pub fn get_action_panel(&self, action_shortcuts: &HashMap) -> Option { self.root_widget.get_action_panel(action_shortcuts) } diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 1046e16..23ca912 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -158,6 +158,9 @@ pub enum BackendResponseData { RequestViewRender { shortcuts: HashMap }, + InlineViewShortcuts { + shortcuts: HashMap> + }, } #[derive(Debug)] @@ -207,6 +210,7 @@ pub enum BackendRequestData { plugin_id: PluginId, entrypoint_id: Option }, + InlineViewShortcuts, } #[derive(Debug, Clone)] diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index 493ea11..d81ed0d 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -187,6 +187,16 @@ impl BackendForFrontendApi { Ok(()) } + + pub async fn inline_view_shortcuts(&self) -> Result>, BackendForFrontendApiError> { + let request = BackendRequestData::InlineViewShortcuts; + + let BackendResponseData::InlineViewShortcuts { shortcuts } = self.backend_sender.send_receive(request).await? else { + unreachable!() + }; + + Ok(shortcuts) + } } #[derive(Error, Debug, Clone)] diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index f996081..f7ee16c 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -232,6 +232,12 @@ async fn handle_request(application_manager: Arc, request_da BackendResponseData::Nothing } + BackendRequestData::InlineViewShortcuts => { + let shortcuts = application_manager.inline_view_shortcuts() + .await?; + + BackendResponseData::InlineViewShortcuts { shortcuts } + } }; Ok(response_data) diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 69df4c9..89bdc8c 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -5,6 +5,7 @@ use anyhow::{anyhow, Context}; use deno_core::error::AnyError; use deno_core::futures; use deno_core::futures::{StreamExt, TryStreamExt}; +use deno_core::futures::future::join_all; use serde::{Deserialize, Serialize}; use sqlx::{Error, Executor, Pool, Row, Sqlite, SqlitePool}; use sqlx::migrate::Migrator; @@ -12,7 +13,7 @@ use sqlx::sqlite::SqliteConnectOptions; use sqlx::types::Json; use typed_path::TypedPathBuf; use uuid::Uuid; -use common::model::{PhysicalKey, PhysicalShortcut}; +use common::model::{PhysicalKey, PhysicalShortcut, PluginId}; use common::dirs::Dirs; use crate::model::ActionShortcutKey; use crate::plugins::frecency::{FrecencyItemStats, FrecencyMetaParams}; @@ -695,6 +696,25 @@ impl DataDbRepository { Ok(result) } + pub async fn inline_view_shortcuts(&self) -> anyhow::Result>> { + // language=SQLite + let shortcuts: Vec<_> = sqlx::query_as::<_, (String, String)>("SELECT id, plugin_id FROM plugin_entrypoint WHERE type = 'inline-view'") + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|(entrypoint_id, plugin_id)| async move { + let shortcuts = self.action_shortcuts(&plugin_id, &entrypoint_id).await?; + + Ok((plugin_id, shortcuts)) + }) + .collect(); + + join_all(shortcuts) + .await + .into_iter() + .collect() + } + pub async fn mark_entrypoint_frecency(&self, plugin_id: &str, entrypoint_id: &str) -> anyhow::Result<()> { let mut tx = self.pool.begin().await?; diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 5e830db..55625a4 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -605,6 +605,16 @@ impl ApplicationManager { self.request_search_index_refresh(plugin_id); } + + pub async fn inline_view_shortcuts(&self) -> anyhow::Result>> { + let result: HashMap<_, _> = self.db_repository.inline_view_shortcuts() + .await? + .into_iter() + .map(|(plugin_id, shortcuts)| (PluginId::from_string(plugin_id), shortcuts)) + .collect(); + + Ok(result) + } } fn plugin_preference_from_db(id: &str, value: DbPluginPreference) -> PluginPreference { From b0112a4e6813e4f7731edf79eed194d6cf8d28ac Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:02:57 +0200 Subject: [PATCH 116/540] Add copy functionality for Calculator plugin output --- bundled_plugins/calculator/gauntlet.toml | 1 + bundled_plugins/calculator/src/default.tsx | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/bundled_plugins/calculator/gauntlet.toml b/bundled_plugins/calculator/gauntlet.toml index 410c557..84f90b7 100644 --- a/bundled_plugins/calculator/gauntlet.toml +++ b/bundled_plugins/calculator/gauntlet.toml @@ -11,3 +11,4 @@ description = '' [permissions] main_search_bar = ["read"] +clipboard = ["write"] diff --git a/bundled_plugins/calculator/src/default.tsx b/bundled_plugins/calculator/src/default.tsx index c93ad89..e85d10b 100644 --- a/bundled_plugins/calculator/src/default.tsx +++ b/bundled_plugins/calculator/src/default.tsx @@ -1,5 +1,6 @@ -import { Content, Icons, Inline } from "@project-gauntlet/api/components"; +import { Action, ActionPanel, Content, Icons, Inline } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Clipboard } from "@project-gauntlet/api/helpers"; // @ts-expect-error const denoCore: DenoCore = Deno[Deno.internal].core; @@ -24,7 +25,18 @@ export default function Default(props: { text: string }): ReactNode | undefined } return ( - + + { + await Clipboard.writeText(right) + }} + /> + + } + > {left} From f970c9b26ce478ec885378029d02d2082e09bc2d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:21:57 +0200 Subject: [PATCH 117/540] Make on hover search result background color in main view distinct from focused background color --- rust/client/src/ui/theme/button.rs | 4 ++-- rust/client/src/ui/theme/mod.rs | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/rust/client/src/ui/theme/button.rs b/rust/client/src/ui/theme/button.rs index e31cbee..4ee88f4 100644 --- a/rust/client/src/ui/theme/button.rs +++ b/rust/client/src/ui/theme/button.rs @@ -99,7 +99,7 @@ impl ButtonStyle { } ButtonStyle::ActionFocused => { let theme = &theme.action; - (Some(&theme.background_color_hovered), Some(&theme.background_color_hovered), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color_focused), Some(&theme.background_color_focused), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::ListItem => { let theme = &theme.list_item; @@ -111,7 +111,7 @@ impl ButtonStyle { } ButtonStyle::MainListItemFocused => { let theme = &theme.main_list_item; - (Some(&theme.background_color_hovered), Some(&theme.background_color_hovered), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color_focused), Some(&theme.background_color_focused), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::MetadataLink => { let theme = &theme.metadata_link; diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index eb55d1a..5a37303 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -259,6 +259,7 @@ impl GauntletTheme { action: ThemeButton { padding: padding_all(8.0), background_color: TRANSPARENT, + background_color_focused: background_lighter_color, background_color_hovered: background_lighter_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, @@ -286,6 +287,7 @@ impl GauntletTheme { metadata_tag_item_button: ThemeButton { padding: padding_axis(2.0, 8.0), background_color: primary_color, + background_color_focused: primary_hovered_color, background_color_hovered: primary_hovered_color, text_color: text_darkest_color, text_color_hovered: text_darkest_color, @@ -348,6 +350,7 @@ impl GauntletTheme { grid_item: ThemeButton { padding: padding_all(8.0), background_color: background_lighter_color, + background_color_focused: background_lightest_color, background_color_hovered: background_lightest_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, @@ -381,6 +384,7 @@ impl GauntletTheme { root_top_panel_button: ThemeButton { padding: padding_axis(3.0, 5.0), background_color: background_lighter_color, + background_color_focused: background_lightest_color, background_color_hovered: background_lightest_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, @@ -396,6 +400,7 @@ impl GauntletTheme { root_bottom_panel_action_toggle_button: ThemeButton { padding: padding_axis(3.0, 5.0), background_color: TRANSPARENT, + background_color_focused: background_lighter_color, background_color_hovered: background_lighter_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, @@ -414,6 +419,7 @@ impl GauntletTheme { list_item: ThemeButton { padding: padding_all(5.0), background_color: TRANSPARENT, + background_color_focused: background_lighter_color, background_color_hovered: background_lighter_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, @@ -480,7 +486,8 @@ impl GauntletTheme { main_list_item: ThemeButton { padding: padding_all(5.0), background_color: TRANSPARENT, - background_color_hovered: background_lighter_color, + background_color_focused: background_lighter_color, + background_color_hovered: background_darker_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, border_radius: BUTTON_BORDER_RADIUS, @@ -538,6 +545,7 @@ impl GauntletTheme { form_input_date_picker_buttons: ThemeButton { padding: padding_all(8.0), background_color: primary_color, + background_color_focused: primary_hovered_color, background_color_hovered: primary_hovered_color, text_color: text_darkest_color, text_color_hovered: text_darkest_color, @@ -668,6 +676,7 @@ const fn padding_axis(vertical: f32, horizontal: f32) -> ThemePadding { pub struct ThemeButton { padding: ThemePadding, background_color: ThemeColor, + background_color_focused: ThemeColor, background_color_hovered: ThemeColor, text_color: ThemeColor, text_color_hovered: ThemeColor, From 07aeea6ebb3ebce3f49c2f0404174f27ad106863 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:52:16 +0200 Subject: [PATCH 118/540] Make entrypoint icons a little bit bigger --- rust/client/src/ui/theme/image.rs | 4 ++-- rust/client/src/ui/theme/space.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/client/src/ui/theme/image.rs b/rust/client/src/ui/theme/image.rs index 4fab907..c83a99b 100644 --- a/rust/client/src/ui/theme/image.rs +++ b/rust/client/src/ui/theme/image.rs @@ -18,8 +18,8 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Image { - self.width(16) - .height(16) + self.width(18) + .height(18) } }.into() } diff --git a/rust/client/src/ui/theme/space.rs b/rust/client/src/ui/theme/space.rs index 3a766ce..da22396 100644 --- a/rust/client/src/ui/theme/space.rs +++ b/rust/client/src/ui/theme/space.rs @@ -12,8 +12,8 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Space { fn themed(self, kind: ThemeKindSpace) -> Element<'a, Message> { match kind { ThemeKindSpace::MainListItemIcon => { - self.width(16) - .height(16) + self.width(18) + .height(18) } }.into() } From 6781a65e18c6fd5642d0968b911cf24119a0be88 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 8 Oct 2024 19:49:46 +0200 Subject: [PATCH 119/540] Show plugin id in settings --- rust/management_client/src/views/plugins.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index 8beca8d..46f0645 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -313,8 +313,17 @@ impl ManagementAppPluginsState { .padding(Padding::new(8.0)) .into(); + let id: Element<_> = text(&plugin.plugin_id.to_string()) + .style(TextStyle::Subtitle) + .into(); + + let id = container(id) + .padding(Padding::from([0.0, 8.0, 8.0, 8.0])) + .into(); + let mut column_content = vec![ name, + id, ]; if !plugin.plugin_description.is_empty() { From 0c01048d3237d58cf878c418b7e95898322ee84f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 8 Oct 2024 19:56:06 +0200 Subject: [PATCH 120/540] Merge all bundled plugins into one --- bundled_plugins/applications/gauntlet.toml | 19 ---------- bundled_plugins/applications/package.json | 17 --------- bundled_plugins/calculator/.gitignore | 2 - bundled_plugins/calculator/gauntlet.toml | 14 ------- bundled_plugins/calculator/package.json | 17 --------- bundled_plugins/calculator/tsconfig.json | 12 ------ .../{applications => gauntlet}/.gitignore | 0 bundled_plugins/gauntlet/gauntlet.toml | 37 +++++++++++++++++++ .../{settings => gauntlet}/package.json | 2 +- .../src/applications.ts} | 2 +- .../src/calculator.tsx} | 2 +- .../default.tsx => gauntlet/src/settings.tsx} | 2 +- .../{applications => gauntlet}/tsconfig.json | 0 bundled_plugins/settings/.gitignore | 2 - bundled_plugins/settings/gauntlet.toml | 11 ------ bundled_plugins/settings/tsconfig.json | 12 ------ rust/server/src/lib.rs | 2 +- rust/server/src/plugins/data_db_repository.rs | 9 +++++ rust/server/src/plugins/loader.rs | 4 +- rust/server/src/plugins/mod.rs | 12 +++--- 20 files changed, 58 insertions(+), 120 deletions(-) delete mode 100644 bundled_plugins/applications/gauntlet.toml delete mode 100644 bundled_plugins/applications/package.json delete mode 100644 bundled_plugins/calculator/.gitignore delete mode 100644 bundled_plugins/calculator/gauntlet.toml delete mode 100644 bundled_plugins/calculator/package.json delete mode 100644 bundled_plugins/calculator/tsconfig.json rename bundled_plugins/{applications => gauntlet}/.gitignore (100%) create mode 100644 bundled_plugins/gauntlet/gauntlet.toml rename bundled_plugins/{settings => gauntlet}/package.json (86%) rename bundled_plugins/{applications/src/default.ts => gauntlet/src/applications.ts} (90%) rename bundled_plugins/{calculator/src/default.tsx => gauntlet/src/calculator.tsx} (94%) rename bundled_plugins/{settings/src/default.tsx => gauntlet/src/settings.tsx} (82%) rename bundled_plugins/{applications => gauntlet}/tsconfig.json (100%) delete mode 100644 bundled_plugins/settings/.gitignore delete mode 100644 bundled_plugins/settings/gauntlet.toml delete mode 100644 bundled_plugins/settings/tsconfig.json diff --git a/bundled_plugins/applications/gauntlet.toml b/bundled_plugins/applications/gauntlet.toml deleted file mode 100644 index 5fe50a6..0000000 --- a/bundled_plugins/applications/gauntlet.toml +++ /dev/null @@ -1,19 +0,0 @@ -[gauntlet] -name = 'Applications' -description = 'Run applications from your system as commands' - -[[entrypoint]] -id = 'default' -name = 'Default' -path = 'src/default.ts' -type = 'command-generator' -description = '' - -[[supported_system]] -os = 'linux' - -[[supported_system]] -os = 'macos' - -[[supported_system]] -os = 'windows' diff --git a/bundled_plugins/applications/package.json b/bundled_plugins/applications/package.json deleted file mode 100644 index 1a165b0..0000000 --- a/bundled_plugins/applications/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@project-gauntlet/bundled-plugin-application", - "private": true, - "scripts": { - "build": "gauntlet build", - "dev": "gauntlet dev" - }, - "dependencies": { - "@project-gauntlet/api": "file:../../js/api" - }, - "devDependencies": { - "@types/react": "^18.2.14", - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../tools", - "typescript": "^5.3.3" - } -} diff --git a/bundled_plugins/calculator/.gitignore b/bundled_plugins/calculator/.gitignore deleted file mode 100644 index de4d1f0..0000000 --- a/bundled_plugins/calculator/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/bundled_plugins/calculator/gauntlet.toml b/bundled_plugins/calculator/gauntlet.toml deleted file mode 100644 index 84f90b7..0000000 --- a/bundled_plugins/calculator/gauntlet.toml +++ /dev/null @@ -1,14 +0,0 @@ -[gauntlet] -name = 'Calculator' -description = '' - -[[entrypoint]] -id = 'default' -name = 'Default' -path = 'src/default.tsx' -type = 'inline-view' -description = '' - -[permissions] -main_search_bar = ["read"] -clipboard = ["write"] diff --git a/bundled_plugins/calculator/package.json b/bundled_plugins/calculator/package.json deleted file mode 100644 index 5d4f41d..0000000 --- a/bundled_plugins/calculator/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@project-gauntlet/bundled-plugin-calculator", - "private": true, - "scripts": { - "build": "gauntlet build", - "dev": "gauntlet dev" - }, - "dependencies": { - "@project-gauntlet/api": "file:../../js/api" - }, - "devDependencies": { - "@types/react": "^18.2.14", - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../tools", - "typescript": "^5.3.3" - } -} diff --git a/bundled_plugins/calculator/tsconfig.json b/bundled_plugins/calculator/tsconfig.json deleted file mode 100644 index cbe7961..0000000 --- a/bundled_plugins/calculator/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "module": "ES2022", - "esModuleInterop": true, - "target": "ES2022", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "types": ["@project-gauntlet/deno"] - }, - "lib": ["ES2020"] -} \ No newline at end of file diff --git a/bundled_plugins/applications/.gitignore b/bundled_plugins/gauntlet/.gitignore similarity index 100% rename from bundled_plugins/applications/.gitignore rename to bundled_plugins/gauntlet/.gitignore diff --git a/bundled_plugins/gauntlet/gauntlet.toml b/bundled_plugins/gauntlet/gauntlet.toml new file mode 100644 index 0000000..734f0f7 --- /dev/null +++ b/bundled_plugins/gauntlet/gauntlet.toml @@ -0,0 +1,37 @@ +[gauntlet] +name = 'Gauntlet' +description = 'Default Gauntlet functionality as a bundled plugin' + +[[entrypoint]] +id = 'applications' +name = 'Applications' +path = 'src/applications.ts' +type = 'command-generator' +description = 'Run installed applications from your system' + +[[entrypoint]] +id = 'settings' +name = 'Gauntlet Settings' +path = 'src/settings.tsx' +type = 'command' +description = 'Open Gauntlet Settings' + +[[entrypoint]] +id = 'calculator' +name = 'Calculator' +path = 'src/calculator.tsx' +type = 'inline-view' +description = 'Calculator right under search bar' + +[permissions] +main_search_bar = ["read"] +clipboard = ["write"] + +[[supported_system]] +os = 'linux' + +[[supported_system]] +os = 'macos' + +[[supported_system]] +os = 'windows' diff --git a/bundled_plugins/settings/package.json b/bundled_plugins/gauntlet/package.json similarity index 86% rename from bundled_plugins/settings/package.json rename to bundled_plugins/gauntlet/package.json index f39626f..e212687 100644 --- a/bundled_plugins/settings/package.json +++ b/bundled_plugins/gauntlet/package.json @@ -1,5 +1,5 @@ { - "name": "@project-gauntlet/bundled-plugin-settings", + "name": "@project-gauntlet/bundled-plugin", "private": true, "scripts": { "build": "gauntlet build", diff --git a/bundled_plugins/applications/src/default.ts b/bundled_plugins/gauntlet/src/applications.ts similarity index 90% rename from bundled_plugins/applications/src/default.ts rename to bundled_plugins/gauntlet/src/applications.ts index 44b64fd..3c5c260 100644 --- a/bundled_plugins/applications/src/default.ts +++ b/bundled_plugins/gauntlet/src/applications.ts @@ -15,7 +15,7 @@ interface InternalApi { open_application(command: string[]): void } -export default async function Default(): Promise { +export default async function Applications(): Promise { return (await InternalApi.list_applications()) .map(value => ({ id: `${value.name}-${value.command.join("-")}`, diff --git a/bundled_plugins/calculator/src/default.tsx b/bundled_plugins/gauntlet/src/calculator.tsx similarity index 94% rename from bundled_plugins/calculator/src/default.tsx rename to bundled_plugins/gauntlet/src/calculator.tsx index e85d10b..a8c84de 100644 --- a/bundled_plugins/calculator/src/default.tsx +++ b/bundled_plugins/gauntlet/src/calculator.tsx @@ -10,7 +10,7 @@ interface InternalApi { run_numbat(input: string): { left: string, right: string } } -export default function Default(props: { text: string }): ReactNode | undefined { +export default function Calculator(props: { text: string }): ReactNode | undefined { const text = props.text; try { diff --git a/bundled_plugins/settings/src/default.tsx b/bundled_plugins/gauntlet/src/settings.tsx similarity index 82% rename from bundled_plugins/settings/src/default.tsx rename to bundled_plugins/gauntlet/src/settings.tsx index 8d0c053..9df14d6 100644 --- a/bundled_plugins/settings/src/default.tsx +++ b/bundled_plugins/gauntlet/src/settings.tsx @@ -6,6 +6,6 @@ interface InternalApi { open_settings(): void } -export default function Default(): void { +export default function Settings(): void { InternalApi.open_settings() } diff --git a/bundled_plugins/applications/tsconfig.json b/bundled_plugins/gauntlet/tsconfig.json similarity index 100% rename from bundled_plugins/applications/tsconfig.json rename to bundled_plugins/gauntlet/tsconfig.json diff --git a/bundled_plugins/settings/.gitignore b/bundled_plugins/settings/.gitignore deleted file mode 100644 index de4d1f0..0000000 --- a/bundled_plugins/settings/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/bundled_plugins/settings/gauntlet.toml b/bundled_plugins/settings/gauntlet.toml deleted file mode 100644 index 63334ce..0000000 --- a/bundled_plugins/settings/gauntlet.toml +++ /dev/null @@ -1,11 +0,0 @@ -[gauntlet] -name = 'Settings' -description = '' - -[[entrypoint]] -id = 'default' -name = 'Open Gauntlet Settings' -path = 'src/default.tsx' -type = 'command' -description = '' - diff --git a/bundled_plugins/settings/tsconfig.json b/bundled_plugins/settings/tsconfig.json deleted file mode 100644 index cbe7961..0000000 --- a/bundled_plugins/settings/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "module": "ES2022", - "esModuleInterop": true, - "target": "ES2022", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "types": ["@project-gauntlet/deno"] - }, - "lib": ["ES2020"] -} \ No newline at end of file diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index f7ee16c..ca77828 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -128,7 +128,7 @@ async fn run_server(frontend_sender: RequestSender anyhow::Result<()> { + self.remove_plugin("builtin://applications").await?; + self.remove_plugin("builtin://calculator").await?; + self.remove_plugin("builtin://settings").await?; + + Ok(()) + } + pub async fn list_plugins(&self) -> anyhow::Result> { // language=SQLite let plugins = sqlx::query_as::<_, DbReadPlugin>("SELECT * FROM plugin") diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index 61886b7..8e0e0b2 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -115,8 +115,8 @@ impl PluginLoader { Ok(plugin_id) } - pub async fn save_builtin_plugin(&self, id: &str, dir: &Dir<'_>) -> anyhow::Result { - let plugin_id = PluginId::from_string(format!("builtin://{id}")); + pub async fn save_bundled_plugin(&self, id: &str, dir: &Dir<'_>) -> anyhow::Result { + let plugin_id = PluginId::from_string(format!("bundled://{id}")); let temp_dir = tempfile::tempdir()?; dir.extract(&temp_dir)?; diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 55625a4..64a5242 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -38,10 +38,8 @@ mod icon_cache; pub(super) mod frecency; mod global_shortcut; -static BUILTIN_PLUGINS: [(&str, Dir); 3] = [ - ("applications", include_dir!("$CARGO_MANIFEST_DIR/../../bundled_plugins/applications/dist")), - ("calculator", include_dir!("$CARGO_MANIFEST_DIR/../../bundled_plugins/calculator/dist")), - ("settings", include_dir!("$CARGO_MANIFEST_DIR/../../bundled_plugins/settings/dist")), +static BUNDLED_PLUGINS: [(&str, Dir); 1] = [ + ("gauntlet", include_dir!("$CARGO_MANIFEST_DIR/../../bundled_plugins/gauntlet/dist")), ]; pub struct ApplicationManager { @@ -146,11 +144,11 @@ impl ApplicationManager { }) } - pub async fn load_builtin_plugins(&self) -> anyhow::Result<()> { - for (id, dir) in &BUILTIN_PLUGINS { + pub async fn load_bundled_plugins(&self) -> anyhow::Result<()> { + for (id, dir) in &BUNDLED_PLUGINS { tracing::info!(target = "plugin", "Saving builtin plugin with id: {:?}", id); - let plugin_id = self.plugin_downloader.save_builtin_plugin(id, dir).await?; + let plugin_id = self.plugin_downloader.save_bundled_plugin(id, dir).await?; self.reload_plugin(plugin_id).await?; } From 1f2fa6e3b2986f75b550f4b54ada24b9657c2ba2 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:01:35 +0200 Subject: [PATCH 121/540] Update package-lock.json --- package-lock.json | 40 ++++------------------------------------ 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index e881d53..3c3fbd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,32 +21,8 @@ "js/scenario_runner_cli" ] }, - "bundled_plugins/applications": { - "name": "@project-gauntlet/bundled-plugin-application", - "dependencies": { - "@project-gauntlet/api": "file:../../js/api" - }, - "devDependencies": { - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../tools", - "@types/react": "^18.2.14", - "typescript": "^5.3.3" - } - }, - "bundled_plugins/calculator": { - "name": "@project-gauntlet/bundled-plugin-calculator", - "dependencies": { - "@project-gauntlet/api": "file:../../js/api" - }, - "devDependencies": { - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../tools", - "@types/react": "^18.2.14", - "typescript": "^5.3.3" - } - }, - "bundled_plugins/settings": { - "name": "@project-gauntlet/bundled-plugin-settings", + "bundled_plugins/gauntlet": { + "name": "@project-gauntlet/bundled-plugin", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -1027,16 +1003,8 @@ "resolved": "js/build", "link": true }, - "node_modules/@project-gauntlet/bundled-plugin-application": { - "resolved": "bundled_plugins/applications", - "link": true - }, - "node_modules/@project-gauntlet/bundled-plugin-calculator": { - "resolved": "bundled_plugins/calculator", - "link": true - }, - "node_modules/@project-gauntlet/bundled-plugin-settings": { - "resolved": "bundled_plugins/settings", + "node_modules/@project-gauntlet/bundled-plugin": { + "resolved": "bundled_plugins/gauntlet", "link": true }, "node_modules/@project-gauntlet/core": { From 5051312323e3c90b18f971cef42bfa34ab160a01 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:48:18 +0200 Subject: [PATCH 122/540] Fix some macOS applications not having icons, also speedup icon cache generation --- Cargo.lock | 3 + rust/server/Cargo.toml | 3 + rust/server/src/plugins/applications/macos.rs | 99 +++++++++++++------ 3 files changed, 77 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17ca186..6cf3970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7871,6 +7871,9 @@ dependencies = [ "itertools 0.10.5", "libc", "numbat", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", "once_cell", "open", "plist", diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 905a6e6..e781b64 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -55,6 +55,9 @@ freedesktop-icons = "0.2" cacao = "0.3.2" plist = "1.6.1" icns = "0.3.1" +objc2-app-kit = { version = "0.2.2", features = ["NSWorkspace", "NSImage", "NSImageRep", "NSBitmapImageRep", "NSGraphics", "NSGraphicsContext"] } +objc2-foundation = { version = "0.2.2", features = ["NSString"] } +objc2 = "0.5.2" [features] release = ["common/release"] diff --git a/rust/server/src/plugins/applications/macos.rs b/rust/server/src/plugins/applications/macos.rs index e03e5f6..2751771 100644 --- a/rust/server/src/plugins/applications/macos.rs +++ b/rust/server/src/plugins/applications/macos.rs @@ -2,13 +2,17 @@ use std::collections::HashMap; use std::error::Error; use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use anyhow::Context; + +use anyhow::{anyhow, Context}; use cacao::filesystem::{FileManager, SearchPathDirectory, SearchPathDomainMask}; use cacao::url::Url; -use deno_runtime::deno_http::compressible::is_content_compressible; +use objc2::ClassType; +use objc2_app_kit::{NSBitmapImageRep, NSCalibratedWhiteColorSpace, NSCompositeCopy, NSDeviceRGBColorSpace, NSGraphicsContext, NSImage, NSPNGFileType, NSWorkspace}; +use objc2_foundation::{CGFloat, CGPoint, CGRect, NSDictionary, NSInteger, NSPoint, NSRect, NSSize, NSString, NSZeroRect}; use plist::Dictionary; use regex::Regex; use serde::Deserialize; + use crate::plugins::applications::{DesktopEntry, resize_icon}; pub fn get_apps() -> Vec { @@ -77,9 +81,13 @@ fn get_applications(file_manager: &FileManager) -> Vec { .and_then(|info| info.bundle_display_name.clone().or_else(|| info.bundle_name.clone())) .unwrap_or(name); + let icon = get_application_icon(&path) + .inspect_err(|err| tracing::error!("error while reading application icon for {:?}: {:?}", path, err)) + .ok(); + DesktopEntry { name, - icon: get_application_icon(&path, &info), + icon, command: vec!["open".to_string(), path.to_string_lossy().to_string()], } }) @@ -120,7 +128,7 @@ fn get_settings(file_manager: &FileManager) -> Vec { let extensions: HashMap<_, _> = get_extensions_in_dir(PathBuf::from("/System/Library/ExtensionKit/Extensions")) .into_iter() .filter_map(|path| { - fn read_plist(path: &Path) -> anyhow::Result<(String, String)> { + fn read_plist(path: &Path) -> anyhow::Result<(String, (String, PathBuf))> { let name = path.file_stem() .expect(&format!("invalid path: {:?}", path)) .to_string_lossy() @@ -136,7 +144,7 @@ fn get_settings(file_manager: &FileManager) -> Vec { .or_else(|| info.bundle_name.clone()) .unwrap_or(name); - Ok((info.bundle_id, name)) + Ok((info.bundle_id, (name, path.to_path_buf()))) } read_plist(&path) @@ -156,11 +164,15 @@ fn get_settings(file_manager: &FileManager) -> Vec { None } - Some(name) => { + Some((name, path)) => { + let icon = get_application_icon(&path) + .inspect_err(|err| tracing::error!("error while reading application icon for {:?}: {:?}", path, err)) + .ok(); + Some( DesktopEntry { name: name.to_string(), - icon: None, + icon, command: vec![ "open".to_string(), format!("x-apple.systempreferences:{}", preferences_id) @@ -288,29 +300,60 @@ fn get_items_in_dir(path: PathBuf, extension: &str) -> Vec { } } -fn get_application_icon(app_path: &Path, info: &Option) -> Option> { - if let Some(info) = info { - info.bundle_icon_name - .clone() - .or(info.bundle_icon_file.clone()) - .map(|icon| { - match PathBuf::from(&icon).extension() { - None => format!("{}.icns", icon), - Some(_) => icon - } - }) - .and_then(|icon_filename| { - let icon_path = app_path - .join("Contents") - .join("Resources") - .join(icon_filename); +// from https://stackoverflow.com/a/38442746 and https://stackoverflow.com/a/29162536 +unsafe fn resize_ns_image(source_image: &NSImage, width: NSInteger, height: NSInteger) -> Option> { + let new_size = NSSize::new(width as CGFloat, height as CGFloat); - tracing::debug!("Derived icon location for app {:?}: {:?}", &info.bundle_name, &icon_path); + let bitmap_image_rep = NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel( + NSBitmapImageRep::alloc(), + std::ptr::null_mut::<*mut _>(), + width, + height, + 8, + 4, + true, + false, + NSDeviceRGBColorSpace, + 0, + 0, + )?; - get_png_from_icon_path(icon_path) - }) - } else { - None + bitmap_image_rep.setSize(new_size); + + NSGraphicsContext::saveGraphicsState_class(); + + let context = NSGraphicsContext::graphicsContextWithBitmapImageRep(&bitmap_image_rep) + .expect("should be present because just saved"); + + NSGraphicsContext::setCurrentContext(Some(&context)); + + let rect = NSRect::new(NSPoint::new(0.0, 0.0), new_size); + source_image.drawInRect_fromRect_operation_fraction(rect, NSZeroRect, NSCompositeCopy, 1.0); + + NSGraphicsContext::restoreGraphicsState_class(); + + // TODO i guess this doesn't work for 2x image + + let data = bitmap_image_rep.representationUsingType_properties(NSPNGFileType, &NSDictionary::dictionary())?; + + Some(data.bytes().to_vec()) +} + +fn get_application_icon(app_path: &Path) -> anyhow::Result> { + unsafe { + let workspace = NSWorkspace::sharedWorkspace(); + + let app_path = app_path.to_str() + .context(format!("Application path is not a utf-8 string: {:?}", &app_path))?; + + let app_path = NSString::from_str(app_path); + + let image = workspace.iconForFile(&app_path); + + let bytes = resize_ns_image(&image, 40, 40) + .ok_or(anyhow!("Unable to resize the image"))?; + + Ok(bytes) } } From d94563512dfa836d8e625dd7ab18112066b0c9cc Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:25:04 +0200 Subject: [PATCH 123/540] Move "remove plugin" button in settings ui to the bottom of the view --- dev_plugin/gauntlet.toml | 21 +++++++++++++++++ rust/management_client/src/views/plugins.rs | 24 ++++++++++++-------- rust/server/src/plugins/js/mod.rs | 2 +- rust/server/src/plugins/js/plugins/numbat.rs | 2 +- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index a684898..f1c82af 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -11,6 +11,27 @@ type = 'bool' default = true description = "test bool description" +[[preferences]] +id = 'testBool2' +name = 'Test Boolean 2' +type = 'bool' +default = true +description = "test bool description" + +[[preferences]] +id = 'testBool3' +name = 'Test Boolean 3' +type = 'bool' +default = true +description = "test bool description" + +[[preferences]] +id = 'testBool4' +name = 'Test Boolean 4' +type = 'bool' +default = true +description = "test bool description" + [[entrypoint]] id = 'detail-view' diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index 46f0645..5f954b7 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -349,7 +349,18 @@ impl ManagementAppPluginsState { .map(|msg| ManagementAppPluginMsgIn::PluginPreferenceMsg(msg)) ); - if !plugin.plugin_id.to_string().starts_with("builtin://") { + let content: Element<_> = column(column_content) + .padding(Padding::from([0.0, 4.0, 0.0, 0.0])) + .into(); + + let content: Element<_> = scrollable(content) + .height(Length::Fill) + .width(Length::Fill) + .into(); + + let mut column_content = vec![content]; + + if !plugin.plugin_id.to_string().starts_with("bundled://") { let remove_text: Element<_> = text("Remove plugin") .into(); @@ -368,18 +379,13 @@ impl ManagementAppPluginsState { column_content.push(remove_button); } - let column: Element<_> = column(column_content) - .padding(Padding::from([0.0, 4.0, 0.0, 0.0])) + let content: Element<_> = column(column_content) .into(); - let column: Element<_> = scrollable(column) - .width(Length::Fill) - .into(); - - container(column) + container(content) .width(Length::Fill) .height(Length::Fill) - .padding(Padding::from([4.0, 0.0])) + .padding(Padding::from([4.0, 0.0, 0.0, 0.0])) .into() } } diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 6080451..cf939df 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -324,7 +324,7 @@ async fn start_js_runtime( let core_url = "gauntlet:core".parse().expect("should be valid"); let unused_url = "gauntlet:unused".parse().expect("should be valid"); - let numbat_context = if plugin_id.to_string() == "builtin://calculator" { + let numbat_context = if plugin_id.to_string() == "bundled://calculator" { Some(NumbatContext::new()) } else { None diff --git a/rust/server/src/plugins/js/plugins/numbat.rs b/rust/server/src/plugins/js/plugins/numbat.rs index 66c89d5..b1b127f 100644 --- a/rust/server/src/plugins/js/plugins/numbat.rs +++ b/rust/server/src/plugins/js/plugins/numbat.rs @@ -47,7 +47,7 @@ fn run_numbat(state: Rc>, input: String) -> anyhow::Result Date: Thu, 10 Oct 2024 18:33:59 +0200 Subject: [PATCH 124/540] Add "Check for updates" button in settings ui --- rust/management_client/src/views/plugins.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index 5f954b7..69d8f1e 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -361,6 +361,23 @@ impl ManagementAppPluginsState { let mut column_content = vec![content]; if !plugin.plugin_id.to_string().starts_with("bundled://") { + let check_for_updates_text: Element<_> = text("Check for updates") + .into(); + + let check_for_updates_text_container: Element<_> = container(check_for_updates_text) + .width(Length::Fill) + .center_y() + .center_x() + .into(); + + let check_for_updates_button: Element<_> = button(check_for_updates_text_container) + .width(Length::Fill) + .style(ButtonStyle::Primary) + .on_press(ManagementAppPluginMsgIn::DownloadPlugin { plugin_id: plugin.plugin_id.clone() }) + .into(); + + column_content.push(check_for_updates_button); + let remove_text: Element<_> = text("Remove plugin") .into(); @@ -380,6 +397,7 @@ impl ManagementAppPluginsState { } let content: Element<_> = column(column_content) + .spacing(8.0) .into(); container(content) From cf2aa7030d47b0d2b4767d6ffabed73a4043983d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:41:29 +0200 Subject: [PATCH 125/540] Fix download info panel being transparent and sometimes partially obstructed --- rust/management_client/src/theme/container.rs | 6 +++--- rust/management_client/src/ui.rs | 1 + rust/management_client/src/views/general.rs | 7 +++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rust/management_client/src/theme/container.rs b/rust/management_client/src/theme/container.rs index b748a51..1fc204f 100644 --- a/rust/management_client/src/theme/container.rs +++ b/rust/management_client/src/theme/container.rs @@ -1,7 +1,7 @@ use iced::Border; use iced::widget::container; -use crate::theme::{GauntletSettingsTheme, BACKGROUND_LIGHTEST, BACKGROUND_DARKER}; +use crate::theme::{GauntletSettingsTheme, BACKGROUND_LIGHTEST, BACKGROUND_DARKER, BACKGROUND_LIGHTER}; #[derive(Default)] pub enum ContainerStyle { @@ -19,9 +19,9 @@ impl container::StyleSheet for GauntletSettingsTheme { ContainerStyle::Transparent => Default::default(), ContainerStyle::Box => { container::Appearance { - background: Some(BACKGROUND_LIGHTEST.to_iced().into()), + background: Some(BACKGROUND_DARKER.to_iced().into()), border: Border { - color: BACKGROUND_DARKER.to_iced(), + color: BACKGROUND_LIGHTER.to_iced(), radius: 10.0.into(), width: 1.0, }, diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index a2641a8..3baef16 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -732,6 +732,7 @@ impl Application for ManagementAppModel { container(downloads) .padding(4) .width(Length::Fixed(400.0)) + .max_height(500.0) .style(ContainerStyle::Box) .into() }; diff --git a/rust/management_client/src/views/general.rs b/rust/management_client/src/views/general.rs index f66be06..cf538d0 100644 --- a/rust/management_client/src/views/general.rs +++ b/rust/management_client/src/views/general.rs @@ -1,14 +1,13 @@ -use std::sync::Arc; -use iced::{Alignment, Command, Length}; use iced::alignment::Horizontal; -use iced::widget::{column, container, row, Space, text}; +use iced::widget::{column, container, row, text, Space}; +use iced::{Alignment, Command, Length}; use common::model::{PhysicalKey, PhysicalShortcut}; use common::rpc::backend_api::{BackendApi, BackendApiError}; use crate::components::shortcut_selector::ShortcutSelector; -use crate::theme::Element; use crate::theme::shortcut_selector::ShortcutSelectorStyle; +use crate::theme::Element; pub struct ManagementAppGeneralState { backend_api: Option, From bb0ceead12df00495952dc053585f7784af1ecb9 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 10 Oct 2024 20:10:19 +0200 Subject: [PATCH 126/540] Highlight preference value fields which are required but not yet filled --- dev_plugin/gauntlet.toml | 2 +- rust/management_client/src/theme/container.rs | 20 +++- rust/management_client/src/views/plugins.rs | 17 +-- .../src/views/plugins/preferences.rs | 102 +++++++++++++----- rust/server/src/plugins/js/preferences.rs | 6 +- 5 files changed, 107 insertions(+), 40 deletions(-) diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index f1c82af..8ebd256 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -9,7 +9,7 @@ id = 'testBool' name = 'Test Boolean' type = 'bool' default = true -description = "test bool description" +description = "test bool description, not super long description to test things" [[preferences]] id = 'testBool2' diff --git a/rust/management_client/src/theme/container.rs b/rust/management_client/src/theme/container.rs index 1fc204f..dfd7126 100644 --- a/rust/management_client/src/theme/container.rs +++ b/rust/management_client/src/theme/container.rs @@ -1,14 +1,15 @@ -use iced::Border; +use iced::{Border, Color}; use iced::widget::container; -use crate::theme::{GauntletSettingsTheme, BACKGROUND_LIGHTEST, BACKGROUND_DARKER, BACKGROUND_LIGHTER}; +use crate::theme::{GauntletSettingsTheme, BACKGROUND_LIGHTEST, BACKGROUND_DARKER, BACKGROUND_LIGHTER, DANGER, TRANSPARENT}; #[derive(Default)] pub enum ContainerStyle { #[default] Transparent, Box, - TextInputLike + TextInputLike, + TextInputMissingValue } impl container::StyleSheet for GauntletSettingsTheme { @@ -39,6 +40,19 @@ impl container::StyleSheet for GauntletSettingsTheme { ..Default::default() } } + ContainerStyle::TextInputMissingValue => { + let color = DANGER.to_iced(); + + container::Appearance { + background: Some(Color::new(color.r, color.g, color.b, 0.3).into()), + border: Border { + color: TRANSPARENT.to_iced(), + radius: 4.0.into(), + width: 0.0, + }, + ..Default::default() + } + } } } } \ No newline at end of file diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index 69d8f1e..2fa5321 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -340,8 +340,10 @@ impl ManagementAppPluginsState { .padding(Padding::new(8.0)) .into(); - column_content.push(description_label); - column_content.push(description); + let content: Element<_> = column(vec![description_label, description]) + .into(); + + column_content.push(content); } column_content.push( @@ -350,7 +352,7 @@ impl ManagementAppPluginsState { ); let content: Element<_> = column(column_content) - .padding(Padding::from([0.0, 4.0, 0.0, 0.0])) + .spacing(12) .into(); let content: Element<_> = scrollable(content) @@ -403,7 +405,6 @@ impl ManagementAppPluginsState { container(content) .width(Length::Fill) .height(Length::Fill) - .padding(Padding::from([4.0, 0.0, 0.0, 0.0])) .into() } } @@ -450,8 +451,10 @@ impl ManagementAppPluginsState { .padding(Padding::new(8.0)) .into(); - column_content.push(description_label); - column_content.push(description); + let content: Element<_> = column(vec![description_label, description]) + .into(); + + column_content.push(content); } column_content.push( @@ -460,7 +463,7 @@ impl ManagementAppPluginsState { ); let column: Element<_> = column(column_content) - .padding(Padding::from([0.0, 4.0, 0.0, 0.0])) + .spacing(12) .into(); let column: Element<_> = scrollable(column) diff --git a/rust/management_client/src/views/plugins/preferences.rs b/rust/management_client/src/views/plugins/preferences.rs index faa0f0c..e01813f 100644 --- a/rust/management_client/src/views/plugins/preferences.rs +++ b/rust/management_client/src/views/plugins/preferences.rs @@ -1,14 +1,15 @@ -use std::collections::HashMap; -use std::fmt::Display; -use iced::{Length, Padding}; +use crate::theme::button::ButtonStyle; +use crate::theme::container::ContainerStyle; +use crate::theme::text::TextStyle; +use crate::theme::Element; +use crate::views::plugins::PluginPreferenceUserDataState; +use common::model::{EntrypointId, PluginId, PluginPreference}; use iced::widget::{button, checkbox, column, container, pick_list, row, text, text_input}; +use iced::{Length, Padding}; use iced_aw::core::icons; use iced_aw::number_input; -use common::model::{EntrypointId, PluginId, PluginPreference}; -use crate::theme::button::ButtonStyle; -use crate::theme::Element; -use crate::theme::text::TextStyle; -use crate::views::plugins::PluginPreferenceUserDataState; +use std::collections::HashMap; +use std::fmt::Display; #[derive(Debug, Clone)] pub enum PluginPreferencesMsg { @@ -75,17 +76,19 @@ pub fn preferences_ui<'a>( .padding(Padding::from([0.0, 0.0, 0.0, 8.0])) .into(); - column_content.push(preference_label); + let mut input_field_column = vec![]; + + input_field_column.push(preference_label); if !description.trim().is_empty() { let description = container(text(description)) .padding(Padding::from([4.0, 8.0])) .into(); - column_content.push(description); + input_field_column.push(description); } - let input_fields: Vec> = match preference { + let input_field: Element<_> = match preference { PluginPreference::Number { default, .. } => { let value = match user_data { None => None, @@ -93,6 +96,8 @@ pub fn preferences_ui<'a>( Some(_) => unreachable!() }; + let missing = value.as_ref().or(default.as_ref()).is_none(); + let value = value.or(default.to_owned()).unwrap_or_default(); let input_field: Element<_> = number_input(value, f64::MAX, std::convert::identity) @@ -114,9 +119,10 @@ pub fn preferences_ui<'a>( let input_field = container(input_field) .width(Length::Fill) .padding(Padding::from([4.0, 8.0])) + .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) .into(); - vec![input_field] + input_field } PluginPreference::String { default, .. } => { let value = match user_data { @@ -125,6 +131,8 @@ pub fn preferences_ui<'a>( Some(_) => unreachable!() }; + let missing = value.as_ref().or(default.as_ref()).is_none(); + let default = default.to_owned().unwrap_or_default(); let input_field: Element<_> = text_input(&default, &value.unwrap_or_default()) @@ -142,9 +150,10 @@ pub fn preferences_ui<'a>( let input_field = container(input_field) .padding(Padding::new(8.0)) + .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) .into(); - vec![input_field] + input_field } PluginPreference::Enum { default, enum_values, .. } => { let value = match user_data { @@ -153,6 +162,8 @@ pub fn preferences_ui<'a>( Some(_) => unreachable!() }; + let missing = value.as_ref().or(default.as_ref()).is_none(); + let enum_values: Vec<_> = enum_values.iter() .map(|enum_item| SelectItem { label: enum_item.label.to_owned(), value: enum_item.value.to_owned() }) .collect(); @@ -180,9 +191,10 @@ pub fn preferences_ui<'a>( let input_field = container(input_field) .padding(Padding::new(8.0)) .width(Length::Fill) + .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) .into(); - vec![input_field] + input_field } PluginPreference::Bool { default, .. } => { let value = match user_data { @@ -191,6 +203,8 @@ pub fn preferences_ui<'a>( Some(_) => unreachable!() }; + let missing = value.as_ref().or(default.as_ref()).is_none(); + let input_field: Element<_> = checkbox(preference_name.clone(), value.or(default.to_owned()).unwrap_or(false)) .on_toggle(Box::new(move |value| { PluginPreferencesMsg::UpdatePreferenceValue { @@ -206,17 +220,20 @@ pub fn preferences_ui<'a>( let input_field = container(input_field) .padding(Padding::new(8.0)) + .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) .into(); - vec![input_field] + input_field } - PluginPreference::ListOfStrings { .. } => { + PluginPreference::ListOfStrings { default, .. } => { let (value, new_value) = match user_data { None => (None, "".to_owned()), Some(PluginPreferenceUserDataState::ListOfStrings { value, new_value }) => (value.to_owned(), new_value.to_owned()), Some(_) => unreachable!() }; + let missing = value.as_ref().or(default.as_ref()).is_none(); + let mut items: Vec<_> = value.clone() .unwrap_or(vec![]) .iter() @@ -324,15 +341,24 @@ pub fn preferences_ui<'a>( items.push(add_item); - items + let content: Element<_> = column(items) + .into(); + + let content: Element<_> = container(content) + .padding(0) + .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .into(); + + content } - PluginPreference::ListOfNumbers { .. } => { + PluginPreference::ListOfNumbers { default, .. } => { let (value, new_value) = match user_data { None => (None, 0.0), Some(PluginPreferenceUserDataState::ListOfNumbers { value, new_value }) => (value.to_owned(), new_value.to_owned()), Some(_) => unreachable!() }; + let missing = value.as_ref().or(default.as_ref()).is_none(); let mut items: Vec<_> = value.clone() .unwrap_or(vec![]) @@ -440,15 +466,25 @@ pub fn preferences_ui<'a>( items.push(add_item); - items + let content: Element<_> = column(items) + .into(); + + let content: Element<_> = container(content) + .padding(0) + .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .into(); + + content } - PluginPreference::ListOfEnums { enum_values, .. } => { + PluginPreference::ListOfEnums { enum_values, default, .. } => { let (value, new_value) = match user_data { None => (None, None), Some(PluginPreferenceUserDataState::ListOfEnums { value, new_value }) => (value.to_owned(), new_value.to_owned()), Some(_) => unreachable!() }; + let missing = value.as_ref().or(default.as_ref()).is_none(); + let mut items: Vec<_> = value.clone() .unwrap_or(vec![]) .iter() @@ -567,15 +603,29 @@ pub fn preferences_ui<'a>( items.push(add_item); - items + let content: Element<_> = column(items) + .into(); + + let content: Element<_> = container(content) + .padding(0) + .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .into(); + + content } }; - for input_field in input_fields { - column_content.push(input_field); - } + input_field_column.push(input_field); + + let content: Element<_> = column(input_field_column) + .into(); + + column_content.push(content); } - column(column_content) - .into() // todo width full? + let element: Element<_> = column(column_content) + .spacing(12) + .into(); + + element } \ No newline at end of file diff --git a/rust/server/src/plugins/js/preferences.rs b/rust/server/src/plugins/js/preferences.rs index ec06db2..889d3ed 100644 --- a/rust/server/src/plugins/js/preferences.rs +++ b/rust/server/src/plugins/js/preferences.rs @@ -81,7 +81,7 @@ async fn plugin_preferences_required(state: Rc>) -> anyhow::Res let DbReadPlugin { preferences, preferences_user_data, .. } = repository .get_plugin_by_id(&plugin_id.to_string()).await?; - Ok(all_preferences_required(preferences, preferences_user_data)) + Ok(any_preferences_missing_value(preferences, preferences_user_data)) } #[op] @@ -104,11 +104,11 @@ async fn entrypoint_preferences_required(state: Rc>, entrypoint let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = repository .get_entrypoint_by_id(&plugin_id.to_string(), &entrypoint_id).await?; - Ok(all_preferences_required(preferences, preferences_user_data)) + Ok(any_preferences_missing_value(preferences, preferences_user_data)) } -fn all_preferences_required(preferences: HashMap, preferences_user_data: HashMap) -> bool { +fn any_preferences_missing_value(preferences: HashMap, preferences_user_data: HashMap) -> bool { for (name, preference) in preferences { match preferences_user_data.get(&name) { None => { From b9f5b32037d1102019d54c26288f3763b2642ae2 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 11 Oct 2024 18:26:24 +0200 Subject: [PATCH 127/540] Add flatpak application support for Application plugin on Linux --- rust/server/src/plugins/applications/linux.rs | 66 ++++++++++++------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/rust/server/src/plugins/applications/linux.rs b/rust/server/src/plugins/applications/linux.rs index f6c34d8..5fe02f2 100644 --- a/rust/server/src/plugins/applications/linux.rs +++ b/rust/server/src/plugins/applications/linux.rs @@ -1,6 +1,7 @@ use std::collections::hash_map::Entry::Vacant; use std::collections::HashMap; -use std::env; +use std::{env, fs}; +use std::fs::Metadata; use std::path::{PathBuf}; use freedesktop_entry_parser::parse_entry; @@ -25,23 +26,34 @@ fn find_application_dirs() -> Option> { .join("share") } }; - let extra_data_dirs = match env::var_os("XDG_DATA_DIRS") { + + let mut extra_data_dirs = match env::var_os("XDG_DATA_DIRS") { Some(val) => { env::split_paths(&val).map(PathBuf::from).collect() }, None => { vec![ PathBuf::from("/usr/local/share"), - PathBuf::from("/usr/share") + PathBuf::from("/usr/share"), + PathBuf::from("/var/lib/flatpak/exports/share"), ] } }; + let flatpak = data_home.to_path_buf() + .join("flatpak") + .join("exports") + .join("share"); + let mut res = Vec::new(); - res.push(data_home.join("applications")); - for dir in extra_data_dirs { - res.push(dir.join("applications")); - } + res.push(data_home); + res.push(flatpak); + res.append(&mut extra_data_dirs); + + let res = res.into_iter() + .map(|d| d.join("applications")) + .collect(); + Some(res) } @@ -58,30 +70,36 @@ pub fn get_apps() -> Vec { let found_desktop_entries = WalkDir::new(app_dir.clone()) .into_iter() .filter_map(|dir_entry| dir_entry.ok()) - .filter(|dir_entry| dir_entry.file_type().is_file()) .filter_map(|path| { let path = path.path(); - tracing::debug!("path: {:?}", path); + tracing::debug!("Found application at: {:?}", path); - match path.extension() { - None => None, - Some(extension) => { - match extension.to_str() { - Some("desktop") => { + // follows symlinks needed for flatpak + let Ok(metadata) = fs::metadata(path) else { + return None; + }; - let desktop_id = path.strip_prefix(&app_dir) - .ok()? - .to_str()? - .to_owned(); + if !metadata.is_file() { + return None; + } - let entry = create_app_entry(path.to_path_buf())?; + let Some(extension) = path.extension() else { + return None; + }; - Some((desktop_id, entry)) - }, - _ => None, - } - } + match extension.to_str() { + Some("desktop") => { + let desktop_id = path.strip_prefix(&app_dir) + .ok()? + .to_str()? + .to_owned(); + + let entry = create_app_entry(path.to_path_buf())?; + + Some((desktop_id, entry)) + }, + _ => None, } }) .collect::>(); From 1fb03fdcfd1d17755eef3250b931923d3298e110 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 11 Oct 2024 18:28:15 +0200 Subject: [PATCH 128/540] Rename action "Copy content" to "Copy result" for calculator plugin --- bundled_plugins/gauntlet/src/calculator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundled_plugins/gauntlet/src/calculator.tsx b/bundled_plugins/gauntlet/src/calculator.tsx index a8c84de..13987e3 100644 --- a/bundled_plugins/gauntlet/src/calculator.tsx +++ b/bundled_plugins/gauntlet/src/calculator.tsx @@ -29,7 +29,7 @@ export default function Calculator(props: { text: string }): ReactNode | undefin actions={ { await Clipboard.writeText(right) }} From 978fafefcdf7882d387417f84cb966a8b2af1466 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 12 Oct 2024 20:50:31 +0200 Subject: [PATCH 129/540] Fix entrypoint icons not working --- rust/server/src/plugins/data_db_repository.rs | 2 +- rust/server/src/plugins/icon_cache.rs | 2 +- rust/server/src/plugins/js/search.rs | 4 ++-- tools | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 8d0d404..219305d 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -1012,7 +1012,7 @@ impl DataDbRepository { .bind(new_entrypoint.description) .bind(Json(new_entrypoint.actions)) .bind(Json(actions_user_data)) - .bind(Json(new_entrypoint.icon_path)) + .bind(new_entrypoint.icon_path) .bind(uuid) .execute(&mut *tx) .await?; diff --git a/rust/server/src/plugins/icon_cache.rs b/rust/server/src/plugins/icon_cache.rs index 8d6ea19..41007bc 100644 --- a/rust/server/src/plugins/icon_cache.rs +++ b/rust/server/src/plugins/icon_cache.rs @@ -35,7 +35,7 @@ impl IconCache { Ok(()) } - pub fn save_entrypoint_icon_to_cache(&self, plugin_uuid: &str, entrypoint_uuid: &str, data: Vec) -> anyhow::Result { + pub fn save_entrypoint_icon_to_cache(&self, plugin_uuid: &str, entrypoint_uuid: &str, data: impl AsRef<[u8]>) -> anyhow::Result { let cache_dir = self.dirs.icon_cache_dir(); let plugin_cache_dir = cache_dir.join(plugin_uuid); std::fs::create_dir_all(&plugin_cache_dir)?; diff --git a/rust/server/src/plugins/js/search.rs b/rust/server/src/plugins/js/search.rs index 66e772f..f6b482c 100644 --- a/rust/server/src/plugins/js/search.rs +++ b/rust/server/src/plugins/js/search.rs @@ -66,7 +66,7 @@ async fn reload_search_index(state: Rc>, generated_commands: Ve .map(|item| { let entrypoint_icon_path = match item.entrypoint_icon { None => None, - Some(data) => Some(icon_cache.save_entrypoint_icon_to_cache(&plugin_uuid, &item.entrypoint_uuid, data)?), + Some(data) => Some(icon_cache.save_entrypoint_icon_to_cache(&plugin_uuid, &item.entrypoint_uuid, &data)?), }; let entrypoint_frecency = frecency_map.get(&item.entrypoint_id).cloned().unwrap_or(0.0); @@ -125,7 +125,7 @@ async fn reload_search_index(state: Rc>, generated_commands: Ve let entrypoint_icon_path = match entrypoint.icon_path { None => None, Some(path_to_asset) => { - match icon_asset_data.remove(&(entrypoint.id, path_to_asset)) { + match icon_asset_data.get(&(entrypoint.id, path_to_asset)) { None => None, Some(data) => Some(icon_cache.save_entrypoint_icon_to_cache(&plugin_uuid, &entrypoint.uuid, data)?) } diff --git a/tools b/tools index 6fc6230..6e78122 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit 6fc6230976ff22719e39a746617da7c1ad901d61 +Subproject commit 6e781221180857778684106d229aecf6c1869a31 From f9aa94f13b64fad52bef7e3ec6a9233d56978253 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:19:23 +0200 Subject: [PATCH 130/540] Fix calculator plugin not working after bundled plugin merge --- rust/server/src/plugins/js/mod.rs | 2 +- rust/server/src/plugins/js/plugins/numbat.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index cf939df..20cd887 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -324,7 +324,7 @@ async fn start_js_runtime( let core_url = "gauntlet:core".parse().expect("should be valid"); let unused_url = "gauntlet:unused".parse().expect("should be valid"); - let numbat_context = if plugin_id.to_string() == "bundled://calculator" { + let numbat_context = if plugin_id.to_string() == "bundled://gauntlet" { Some(NumbatContext::new()) } else { None diff --git a/rust/server/src/plugins/js/plugins/numbat.rs b/rust/server/src/plugins/js/plugins/numbat.rs index b1b127f..2bfc5a8 100644 --- a/rust/server/src/plugins/js/plugins/numbat.rs +++ b/rust/server/src/plugins/js/plugins/numbat.rs @@ -47,7 +47,7 @@ fn run_numbat(state: Rc>, input: String) -> anyhow::Result Date: Sat, 12 Oct 2024 21:33:21 +0200 Subject: [PATCH 131/540] On macOS show return arrow icons instead of "ENTER" text for shortcuts --- rust/common_ui/src/lib.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/rust/common_ui/src/lib.rs b/rust/common_ui/src/lib.rs index bba0880..2f665d8 100644 --- a/rust/common_ui/src/lib.rs +++ b/rust/common_ui/src/lib.rs @@ -7,11 +7,28 @@ use iced_aw::core::icons; use common::model::{PhysicalKey, PhysicalShortcut}; pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>(shortcut: &PhysicalShortcut) -> (Element<'a, Message, Theme>, Option>, Option>, Option>, Option>) { + let (key_name, show_shift) = match shortcut.physical_key { + PhysicalKey::Enter => { + let key_name = if cfg!(target_os = "macos") { + text(icons::Bootstrap::ArrowReturnLeft) + .font(icons::BOOTSTRAP_FONT) + .into() + } else { + text("ENTER") + .into() + }; - let (key_name, show_shift) = physical_key_name(&shortcut.physical_key, shortcut.modifier_shift); + (key_name, shortcut.modifier_shift) + } + _ => { + let (key_name, show_shift) = physical_key_name(&shortcut.physical_key, shortcut.modifier_shift); - let key_name: Element<_, _> = text(key_name) - .into(); + let key_name: Element<_, _> = text(key_name) + .into(); + + (key_name, show_shift) + } + }; let alt_modifier_text = if shortcut.modifier_alt { if cfg!(target_os = "macos") { From 8bfd93ab5763aa5e5e02c1ecc5a28f298864189e Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:34:57 +0200 Subject: [PATCH 132/540] Replace all upper-case key names with just first letter upper-case --- rust/common_ui/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rust/common_ui/src/lib.rs b/rust/common_ui/src/lib.rs index 2f665d8..d12399d 100644 --- a/rust/common_ui/src/lib.rs +++ b/rust/common_ui/src/lib.rs @@ -14,7 +14,7 @@ pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>(shortcut: &Ph .font(icons::BOOTSTRAP_FONT) .into() } else { - text("ENTER") + text("Enter") .into() }; @@ -39,7 +39,7 @@ pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>(shortcut: &Ph ) } else { Some( - text("ALT") + text("Alt") .into() ) } @@ -56,12 +56,12 @@ pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>(shortcut: &Ph ) } else if cfg!(target_os = "windows") { Some( - text("WIN") // is it possible to have shortcuts that use win? + text("Win") // is it possible to have shortcuts that use win? .into() ) } else { Some( - text("SUPER") + text("Super") .into() ) } @@ -78,7 +78,7 @@ pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>(shortcut: &Ph ) } else { Some( - text("CTRL") + text("Ctrl") .into() ) } @@ -95,7 +95,7 @@ pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>(shortcut: &Ph ) } else { Some( - text("SHIFT") + text("Shift") .into() ) } From 8397b108e0e348388d05f132a0ba7736b17fa987 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:41:37 +0200 Subject: [PATCH 133/540] Implement hud feedback window --- dev_plugin/src/command-generator.ts | 3 +- js/api/src/helpers.ts | 6 +- js/react_renderer/src/renderer.ts | 4 ++ js/typings/index.d.ts | 2 + rust/client/src/ui/hud/mod.rs | 93 +++++++++++++++++++++++++++ rust/client/src/ui/mod.rs | 82 +++++++++++++++++++++-- rust/client/src/ui/theme/container.rs | 24 +++++++ rust/client/src/ui/theme/mod.rs | 11 ++++ rust/common/src/model.rs | 5 +- rust/common/src/rpc/frontend_api.rs | 13 ++++ rust/server/src/model.rs | 5 +- rust/server/src/plugins/js/mod.rs | 9 ++- rust/server/src/plugins/js/ui.rs | 15 +++++ 13 files changed, 263 insertions(+), 9 deletions(-) create mode 100644 rust/client/src/ui/hud/mod.rs diff --git a/dev_plugin/src/command-generator.ts b/dev_plugin/src/command-generator.ts index 2a15c4f..774a3ce 100644 --- a/dev_plugin/src/command-generator.ts +++ b/dev_plugin/src/command-generator.ts @@ -1,4 +1,4 @@ -import { GeneratedCommand } from "@project-gauntlet/api/helpers"; +import { GeneratedCommand, showHud } from "@project-gauntlet/api/helpers"; export default function CommandGenerator(): GeneratedCommand[] { return [ @@ -45,6 +45,7 @@ export default function CommandGenerator(): GeneratedCommand[] { id: 'generated-test-3', name: 'Generated Item 3', fn: () => { + showHud("HUD test display") console.log('generated-test-3') } }, diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index e1127ea..0f78fe1 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -1,5 +1,5 @@ // @ts-ignore TODO how to add declaration for this? -import { getAssetDataSync, getPluginPreferences, getEntrypointPreferences, } from "gauntlet:renderer"; +import { getAssetDataSync, getPluginPreferences, getEntrypointPreferences, showHudWindow } from "gauntlet:renderer"; // @ts-expect-error does typescript support such symbol declarations? const denoCore: DenoCore = Deno[Deno.internal].core; @@ -17,6 +17,10 @@ export function entrypointPreferences>(): T { return getEntrypointPreferences() } +export function showHud(display: string): void { + return showHudWindow(display) +} + export interface GeneratedCommand { id: string name: string diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index a1331ee..5c9801a 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -122,6 +122,10 @@ export function getEntrypointPreferences(): Record { return gauntletContextValue.entrypointPreferences() } +export function showHudWindow(display: string): void { + InternalApi.show_hud(display) +} + function createWidget(hostContext: HostContext, type: ComponentType, properties: Props, children: UiWidget[] = []): Instance { const props = Object.fromEntries( Object.entries(properties) diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 003721d..895cd1b 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -118,6 +118,8 @@ interface InternalApi { reload_search_index(searchItems: AdditionalSearchItem[], refreshSearchList: boolean): Promise; + show_hud(display: string): void; + op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, entrypoint_id: string, container: UiWidget): void; show_plugin_error_view(entrypoint_id: string, render_location: RenderLocation): void; diff --git a/rust/client/src/ui/hud/mod.rs b/rust/client/src/ui/hud/mod.rs new file mode 100644 index 0000000..4adf255 --- /dev/null +++ b/rust/client/src/ui/hud/mod.rs @@ -0,0 +1,93 @@ +use crate::ui::AppMsg; +use iced::advanced::layout::Limits; +use iced::window::{Level, Position, Settings}; +use iced::{window, Command, Size}; +use std::convert; +use std::time::Duration; + +const HUD_WINDOW_WIDTH: f32 = 400.0; +const HUD_WINDOW_HEIGHT: f32 = 40.0; + +pub fn show_hud_window( + #[cfg(target_os = "linux")] + wayland: bool, +) -> Command { + + let settings = Settings { + size: Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT), + position: Position::Centered, + resizable: false, + decorations: false, + transparent: true, + visible: true, + level: Level::AlwaysOnTop, + #[cfg(target_os = "macos")] + platform_specific: PlatformSpecific { + activation_policy: window::settings::ActivationPolicy::Accessory, + activate_ignoring_other_apps: false, + ..Default::default() + }, + exit_on_close_request: false, + ..Default::default() + }; + + let (id, show_command) = window::spawn(settings); + + let close_command = Command::perform(async move { + tokio::time::sleep(Duration::from_secs(2)).await; + + AppMsg::CloseHudWindow { id } + }, convert::identity); + + + #[cfg(target_os = "linux")] + if wayland { + iced::wayland::commands::layer_surface::get_layer_surface(layer_shell_settings()) + } else { + Command::batch([ + show_command, + close_command + ]) + } + + #[cfg(not(target_os = "linux"))] + Command::batch([ + show_command, + close_command + ]) +} + +pub fn close_hud_window( + #[cfg(target_os = "linux")] + wayland: bool, + id: window::Id +) -> Command { + let command = window::close(id); + + #[cfg(target_os = "linux")] + if wayland { + iced::wayland::commands::layer_surface::destroy_layer_surface(id) + } else { + command + } + + #[cfg(not(target_os = "linux"))] + command +} + +#[cfg(target_os = "linux")] +fn layer_shell_settings() -> iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { + iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { + id: window::Id::unique(), + layer: iced::wayland::commands::layer_surface::Layer::Overlay, + keyboard_interactivity: iced::wayland::commands::layer_surface::KeyboardInteractivity::None, + pointer_interactivity: false, + anchor: iced::wayland::commands::layer_surface::Anchor::empty(), + output: Default::default(), + namespace: "Gauntlet HUD".to_string(), + margin: Default::default(), + exclusive_zone: 0, + size: Some((Some(HUD_WINDOW_WIDTH as u32), Some(HUD_WINDOW_HEIGHT as u32))), + size_limits: Limits::new(Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT), Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT)), + } +} diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 4701acd..c8598fd 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -35,7 +35,7 @@ use crate::model::UiViewEvent; use crate::ui::inline_view_container::{inline_view_action_panel, inline_view_container}; use crate::ui::search_list::search_list; use crate::ui::theme::{Element, ThemableWidget}; -use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::container::{ContainerStyle, ContainerStyleInner}; use crate::ui::theme::text_input::TextInputStyle; use crate::ui::view_container::view_container; use crate::ui::widget::{render_root, ActionPanel, ActionPanelItem, ComponentRenderContext, ComponentWidgetEvent}; @@ -52,8 +52,10 @@ mod sys_tray; mod custom_widgets; mod scroll_handle; mod state; +mod hud; pub use theme::GauntletTheme; +use crate::ui::hud::{close_hud_window, show_hud_window}; use crate::ui::scroll_handle::ScrollHandle; use crate::ui::state::{ErrorViewData, Focus, GlobalState, MainViewState, PluginViewData, PluginViewState}; use crate::ui::widget_container::PluginWidgetContainer; @@ -72,6 +74,7 @@ pub struct AppModel { client_context: Arc>, global_state: GlobalState, search_results: Vec, + hud_display: Option } @@ -152,6 +155,12 @@ pub enum AppMsg { InlineViewShortcuts { shortcuts: HashMap> }, + ShowHud { + display: String + }, + CloseHudWindow { + id: window::Id + }, } pub struct AppFlags { @@ -377,13 +386,18 @@ impl Application for AppModel { global_state, client_context, search_results: vec![], + hud_display: None, }, Command::batch(commands), ) } - fn title(&self, _window: window::Id) -> String { - "Gauntlet".to_owned() + fn title(&self, window: window::Id) -> String { + if window != window::Id::MAIN { + "Gauntlet".to_owned() + } else { + "Gauntlet HUD".to_owned() + } } fn update(&mut self, message: Self::Message) -> Command { @@ -983,10 +997,63 @@ impl Application for AppModel { Command::none() } + AppMsg::ShowHud { display } => { + self.hud_display = Some(display); + + show_hud_window( + #[cfg(target_os = "linux")] + self.wayland, + ) + } + AppMsg::CloseHudWindow { id } => { + self.hud_display = None; + + close_hud_window( + #[cfg(target_os = "linux")] + self.wayland, + id + ) + } } } - fn view(&self, _window: window::Id) -> Element<'_, Self::Message> { + fn view(&self, window: window::Id) -> Element<'_, Self::Message> { + if window != window::Id::MAIN { + return match &self.hud_display { + Some(hud_display) => { + let hud: Element<_> = text(&hud_display) + .into(); + + let hud = container(hud) + .center_x() + .center_y() + .height(Length::Fill) + .themed(ContainerStyle::HudInner); + + let hud = container(hud) + .center_x() + .center_y() + .height(Length::Fill) + .themed(ContainerStyle::Hud); + + let hud = container(hud) + .height(Length::Fill) + .width(Length::Fill) + .center_x() + .center_y() + .style(ContainerStyleInner::Transparent) + .into(); + + hud + } + None => { + horizontal_space() + .into() + } + } + } + + match &self.global_state { GlobalState::ErrorView { error_view } => { match error_view { @@ -1824,6 +1891,13 @@ async fn request_loop( AppMsg::UpdateSearchResults } + UiRequestData::ShowHud { display } => { + responder.respond(UiResponseData::Nothing); + + AppMsg::ShowHud { + display + } + } } }; diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 3c1bc1c..08d865b 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -57,6 +57,8 @@ pub enum ContainerStyle { IconAccessory, InlineInner, InlineName, + HudInner, + Hud, } #[derive(Default)] @@ -74,6 +76,7 @@ pub enum ContainerStyleInner { ContentImage, RootBottomPanel, InlineInner, + Hud, } @@ -219,6 +222,21 @@ impl container::StyleSheet for GauntletTheme { ..Appearance::default() } } + ContainerStyleInner::Hud => { + let theme = &self.hud; + let background_color = &theme.background_color; + + Appearance { + text_color: None, + background: Some(background_color.to_iced().into()), + border: Border { + radius: theme.border_radius.into(), + width: theme.border_width, + color: theme.border_color.to_iced(), + }, + shadow: Default::default(), + } + } } } } @@ -396,6 +414,12 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau ContainerStyle::IconAccessory => { self.padding(theme.icon_accessory.padding.to_iced()) } + ContainerStyle::HudInner => { + self.padding(theme.hud_content.padding.to_iced()) + } + ContainerStyle::Hud => { + self.style(ContainerStyleInner::Hud) + } }.into() } } diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 5a37303..6217b56 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -123,6 +123,8 @@ pub struct GauntletTheme { loading_bar: ThemeLoadingBar, text_accessory: ThemePaddingTextColorSpacing, icon_accessory: ThemeIconAccessory, + hud: ThemeRoot, + hud_content: ThemePaddingOnly } impl Default for GauntletTheme { @@ -618,6 +620,15 @@ impl GauntletTheme { padding: padding(4.0, 4.0, 4.0, 16.0), icon_color: text_lighter_color, }, + hud: ThemeRoot { + background_color: ThemeColor::new(0x1E1E1E, 0.7), + border_radius: 30.0, + border_width: 0.0, + border_color: TRANSPARENT, + }, + hud_content: ThemePaddingOnly { + padding: padding_axis(8.0, 16.0), + }, } } } diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 23ca912..525fa12 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -146,7 +146,10 @@ pub enum UiRequestData { entrypoint_id: EntrypointId, render_location: UiRenderLocation, }, - RequestSearchResultUpdate + RequestSearchResultUpdate, + ShowHud { + display: String + }, } #[derive(Debug)] diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index 2508a28..fe15f94 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -111,4 +111,17 @@ impl FrontendApi { Ok(()) } + + pub async fn show_hud( + &mut self, + display: String, + ) -> Result<(), FrontendApiError> { + let request = UiRequestData::ShowHud { + display, + }; + + let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await?; + + Ok(()) + } } \ No newline at end of file diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 2ee6c08..b884e37 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -28,7 +28,10 @@ pub enum JsUiRequestData { entrypoint_id: EntrypointId, plugin_preferences_required: bool, entrypoint_preferences_required: bool - } + }, + ShowHud { + display: String + }, } #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 20cd887..126eb91 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -46,7 +46,7 @@ use crate::plugins::js::plugins::numbat::{run_numbat, NumbatContext}; use crate::plugins::js::plugins::settings::open_settings; use crate::plugins::js::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; use crate::plugins::js::search::reload_search_index; -use crate::plugins::js::ui::{clear_inline_view, fetch_action_id_for_shortcut, op_component_model, op_inline_view_endpoint_id, op_react_replace_view, show_plugin_error_view, show_preferences_required_view}; +use crate::plugins::js::ui::{clear_inline_view, fetch_action_id_for_shortcut, op_component_model, op_inline_view_endpoint_id, op_react_replace_view, show_hud, show_plugin_error_view, show_preferences_required_view}; use crate::plugins::run_status::RunStatusGuard; use crate::search::{SearchIndex, SearchIndexItem}; @@ -506,6 +506,7 @@ deno_core::extension!( show_preferences_required_view, op_component_model, fetch_action_id_for_shortcut, + show_hud, // preferences get_plugin_preferences, @@ -632,6 +633,12 @@ async fn make_request_async(plugin_id: PluginId, plugin_name: String, frontend_a frontend_api.show_plugin_error_view(plugin_id, entrypoint_id, render_location).await?; + Ok(JsUiResponseData::Nothing) + } + JsUiRequestData::ShowHud { display } => { + + frontend_api.show_hud(display).await?; + Ok(JsUiResponseData::Nothing) } } diff --git a/rust/server/src/plugins/js/ui.rs b/rust/server/src/plugins/js/ui.rs index bd89a76..7e0a199 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/server/src/plugins/js/ui.rs @@ -158,6 +158,21 @@ async fn fetch_action_id_for_shortcut( Ok(result) } +#[op] +async fn show_hud(state: Rc>, display: String) -> anyhow::Result<()> { + let data = JsUiRequestData::ShowHud { + display + }; + + match make_request(&state, data).context("ShowHud frontend response")? { + JsUiResponseData::Nothing => { + tracing::trace!("Calling show_hud returned"); + Ok(()) + } + value @ _ => panic!("unsupported response type {:?}", value), + } +} + fn from_js_to_intermediate_widget(state: Rc>, scope: &mut v8::HandleScope, ui_widget: JsUiWidget, component_model: &ComponentModel, shared_types: &IndexMap) -> anyhow::Result { let children = ui_widget.widget_children.into_iter() .map(|child| from_js_to_intermediate_widget(state.clone(), scope, child, component_model, shared_types)) From d6d619b4ec69ff9a55cd10b42b9275a0c6a0e51b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:44:05 +0200 Subject: [PATCH 134/540] Show hud when copying calculator result --- bundled_plugins/gauntlet/src/calculator.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundled_plugins/gauntlet/src/calculator.tsx b/bundled_plugins/gauntlet/src/calculator.tsx index 13987e3..a9d2d8e 100644 --- a/bundled_plugins/gauntlet/src/calculator.tsx +++ b/bundled_plugins/gauntlet/src/calculator.tsx @@ -1,6 +1,6 @@ import { Action, ActionPanel, Content, Icons, Inline } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; -import { Clipboard } from "@project-gauntlet/api/helpers"; +import { Clipboard, showHud } from "@project-gauntlet/api/helpers"; // @ts-expect-error const denoCore: DenoCore = Deno[Deno.internal].core; @@ -32,6 +32,7 @@ export default function Calculator(props: { text: string }): ReactNode | undefin label={"Copy result"} onAction={async () => { await Clipboard.writeText(right) + showHud("Result copied") }} /> From f1c7619fed9135243893ff34c84eb143111cd1f9 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:54:09 +0200 Subject: [PATCH 135/540] Fix compilation on macOS --- rust/client/src/ui/hud/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/client/src/ui/hud/mod.rs b/rust/client/src/ui/hud/mod.rs index 4adf255..9521eda 100644 --- a/rust/client/src/ui/hud/mod.rs +++ b/rust/client/src/ui/hud/mod.rs @@ -4,6 +4,7 @@ use iced::window::{Level, Position, Settings}; use iced::{window, Command, Size}; use std::convert; use std::time::Duration; +use iced::window::settings::PlatformSpecific; const HUD_WINDOW_WIDTH: f32 = 400.0; const HUD_WINDOW_HEIGHT: f32 = 40.0; From 8d8ddd8f285c71468412962f05cadc9109737932 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 13 Oct 2024 16:33:47 +0200 Subject: [PATCH 136/540] Update numbat to 1.14 --- Cargo.lock | 227 ++++++++++++++++++++++++++++++++++++++--- rust/server/Cargo.toml | 2 +- 2 files changed, 212 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6cf3970..fcac34c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -620,6 +620,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "basic-toml" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +dependencies = [ + "serde", +] + [[package]] name = "better_scoped_tls" version = "0.1.1" @@ -1443,7 +1452,7 @@ dependencies = [ "libm", "log", "rangemap", - "rustc-hash", + "rustc-hash 1.1.0", "rustybuzz", "self_cell", "swash", @@ -2575,7 +2584,7 @@ checksum = "3f115ea5b6f5d0d02a25a9364f41b8c4f857452c299309dcfd29a694724d0566" dependencies = [ "bumpalo", "num-bigint", - "rustc-hash", + "rustc-hash 1.1.0", "swc_atoms", "swc_common", "swc_ecma_ast", @@ -2795,6 +2804,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.2.8" @@ -3906,6 +3925,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "hashlink" version = "0.8.4" @@ -4062,6 +4087,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "hyper" version = "0.14.28" @@ -4232,7 +4266,7 @@ dependencies = [ "log", "once_cell", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "thiserror", "unicode-segmentation", "xxhash-rust", @@ -4318,7 +4352,7 @@ dependencies = [ "iced_graphics", "kurbo 0.10.4", "log", - "rustc-hash", + "rustc-hash 1.1.0", "softbuffer", "tiny-skia", "xxhash-rust", @@ -4526,6 +4560,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -5348,6 +5383,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -5446,7 +5491,7 @@ dependencies = [ "indexmap 2.2.6", "log", "num-traits", - "rustc-hash", + "rustc-hash 1.1.0", "spirv", "termcolor", "thiserror", @@ -5720,9 +5765,9 @@ dependencies = [ [[package]] name = "numbat" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9d54727dbec99dde47300a528312ef872c9a198cdba705d1837b355a6cf24d" +checksum = "5124c7a716bd197d4ad501237fa890771f69f38b34eb87f4514fdebf0cdcaf5b" dependencies = [ "codespan-reporting", "heck 0.4.1", @@ -5736,6 +5781,7 @@ dependencies = [ "num-rational", "num-traits", "numbat-exchange-rates", + "plotly", "pretty_dtoa", "rand", "rust-embed", @@ -5936,6 +5982,18 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "once_map" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed29bb6f7d6ac14023acb332a356f3891265d780e254057c866dbe7a909d2d2d" +dependencies = [ + "ahash", + "hashbrown 0.15.0", + "parking_lot 0.12.1", + "stable_deref_trait", +] + [[package]] name = "oneshot" version = "0.1.6" @@ -6510,6 +6568,36 @@ dependencies = [ "time 0.3.36", ] +[[package]] +name = "plotly" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1ffd11c8a6ef0b730b9d3e46ad2404f79905825cb20223fa0547434a2dff54" +dependencies = [ + "dyn-clone", + "erased-serde", + "once_cell", + "plotly_derive", + "rand", + "rinja", + "serde", + "serde_json", + "serde_repr", + "serde_with", +] + +[[package]] +name = "plotly_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e940d8d8db30c6f4cc37dab9aab61f4c9cc1e6efb6d18902ab88fa09c03560" +dependencies = [ + "darling", + "proc-macro2 1.0.80", + "quote 1.0.36", + "syn 2.0.59", +] + [[package]] name = "pmutil" version = "0.6.1" @@ -7270,6 +7358,51 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rinja" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28580fecce391f3c0e65a692e5f2b5db258ba2346ee04f355ae56473ab973dc" +dependencies = [ + "humansize", + "itoa", + "num-traits", + "percent-encoding", + "rinja_derive", + "serde", + "serde_json", +] + +[[package]] +name = "rinja_derive" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1ae91455a4c82892d9513fcfa1ac8faff6c523602d0041536341882714aede" +dependencies = [ + "basic-toml", + "memchr", + "mime", + "mime_guess", + "once_map", + "proc-macro2 1.0.80", + "quote 1.0.36", + "rinja_parser", + "rustc-hash 2.0.0", + "serde", + "syn 2.0.59", +] + +[[package]] +name = "rinja_parser" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea17639e1f35032e1c67539856e498c04cd65fe2a45f55ec437ec55e4be941" +dependencies = [ + "memchr", + "nom", + "serde", +] + [[package]] name = "ripemd" version = "0.1.3" @@ -7407,6 +7540,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc_version" version = "0.2.3" @@ -7809,6 +7948,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2 1.0.80", + "quote 1.0.36", + "syn 2.0.59", +] + [[package]] name = "serde_spanned" version = "0.6.5" @@ -7846,6 +7996,36 @@ dependencies = [ "v8", ] +[[package]] +name = "serde_with" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +dependencies = [ + "base64 0.22.0", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time 0.3.36", +] + +[[package]] +name = "serde_with_macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +dependencies = [ + "darling", + "proc-macro2 1.0.80", + "quote 1.0.36", + "syn 2.0.59", +] + [[package]] name = "server" version = "0.0.0" @@ -8655,7 +8835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8066e17abb484602da673e2d35138ab32ce53f26368d9c92113510e1659220b" dependencies = [ "once_cell", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "string_cache", "string_cache_codegen", @@ -8676,7 +8856,7 @@ dependencies = [ "new_debug_unreachable", "num-bigint", "once_cell", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "siphasher 0.3.11", "sourcemap", @@ -8740,7 +8920,7 @@ dependencies = [ "memchr", "num-bigint", "once_cell", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "sourcemap", "swc_atoms", @@ -8807,7 +8987,7 @@ dependencies = [ "indexmap 1.9.3", "once_cell", "phf 0.10.1", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "smallvec", "swc_atoms", @@ -8853,7 +9033,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62d3a04de35f6c79d8f343822138e7313934d3530cc4e4f891a079f7e2415c1a" dependencies = [ "either", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "smallvec", "swc_atoms", @@ -8915,7 +9095,7 @@ dependencies = [ "indexmap 1.9.3", "num_cpus", "once_cell", - "rustc-hash", + "rustc-hash 1.1.0", "swc_atoms", "swc_common", "swc_ecma_ast", @@ -9114,7 +9294,7 @@ dependencies = [ "rayon", "regex", "rust-stemmers", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "serde_json", "sketches-ddsketch", @@ -9821,6 +10001,12 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50c0c7479c430935701ff2532e3091e6680ec03f2f89ffcd9988b08e885b90a5" +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + [[package]] name = "typenum" version = "1.17.0" @@ -9868,6 +10054,15 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -10605,7 +10800,7 @@ dependencies = [ "parking_lot 0.12.1", "profiling", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror", "web-sys", @@ -10649,7 +10844,7 @@ dependencies = [ "range-alloc", "raw-window-handle", "renderdoc-sys", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror", "wasm-bindgen", diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index e781b64..44c4d9c 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -30,7 +30,7 @@ client = { path = "../client" } walkdir = "2.4.0" include_dir = "0.7.3" open = "5" -numbat = "1.13.0" +numbat = "1.14.0" uuid = "1.8" resvg = { version = "0.41", default-features = false} image = "0.25" From 06cc7ad8c83f9aa1751f9578e0a72159b9886003 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 13 Oct 2024 17:34:17 +0200 Subject: [PATCH 137/540] Update demo video --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9fd9b0e..b795984 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Web-first cross-platform application launcher with React-based plugins. > > There will probably be breaking changes which will be documented in [changelog](CHANGELOG.md). -https://github.com/user-attachments/assets/1aa790dc-fce9-4ac5-97d8-3b81b83acf2e +https://github.com/user-attachments/assets/19964ed6-9cd9-48d4-9835-6be04de14b66 ## Features From 6ea9748027bd0b9d12ded2242f610077607f3789 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 13 Oct 2024 17:37:34 +0200 Subject: [PATCH 138/540] Fix prompt in main search bar resetting after going back from plugin view --- rust/client/src/ui/mod.rs | 30 +++++++++++++++++++----------- rust/client/src/ui/state/mod.rs | 2 -- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index c8598fd..8ec10d4 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -70,6 +70,9 @@ pub struct AppModel { #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: tray_icon::TrayIcon, + // ephemeral state + prompt: String, + // state client_context: Arc>, global_state: GlobalState, @@ -382,6 +385,9 @@ impl Application for AppModel { #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: sys_tray::create_tray(), + // ephemeral state + prompt: "".to_string(), + // state global_state, client_context, @@ -441,10 +447,10 @@ impl Application for AppModel { Command::none() } else { match &mut self.global_state { - GlobalState::MainView { focused_search_result, sub_state, prompt, ..} => { + GlobalState::MainView { focused_search_result, sub_state, ..} => { new_prompt.truncate(100); // search query uses regex so just to be safe truncate the prompt - *prompt = new_prompt.clone(); + self.prompt = new_prompt.clone(); focused_search_result.reset(true); @@ -459,8 +465,8 @@ impl Application for AppModel { } AppMsg::UpdateSearchResults => { match &self.global_state { - GlobalState::MainView { prompt, .. } => { - self.search(prompt.clone(), false) + GlobalState::MainView { .. } => { + self.search(self.prompt.clone(), false) } _ => Command::none() } @@ -537,9 +543,9 @@ impl Application for AppModel { }, Key::Named(Named::Backspace) => { match &mut self.global_state { - GlobalState::MainView { sub_state, search_field_id, prompt, .. } => { + GlobalState::MainView { sub_state, search_field_id, .. } => { match sub_state { - MainViewState::None => Self::backspace_prompt(prompt, search_field_id.clone()), + MainViewState::None => Self::backspace_prompt(&mut self.prompt, search_field_id.clone()), MainViewState::SearchResultActionPanel { .. } => Command::none(), MainViewState::InlineViewActionPanel { .. } => Command::none() } @@ -550,7 +556,7 @@ impl Application for AppModel { }, _ => { match &mut self.global_state { - GlobalState::MainView { sub_state, search_field_id, prompt, focused_search_result, .. } => { + GlobalState::MainView { sub_state, search_field_id, focused_search_result, .. } => { match sub_state { MainViewState::None => { match physical_key_model(physical_key, modifiers) { @@ -583,10 +589,10 @@ impl Application for AppModel { ) } } else { - Self::append_prompt(prompt, text, search_field_id.clone(), modifiers) + Self::append_prompt(&mut self.prompt, text, search_field_id.clone(), modifiers) } } - _ => Self::append_prompt(prompt, text, search_field_id.clone(), modifiers) + _ => Self::append_prompt(&mut self.prompt, text, search_field_id.clone(), modifiers) } } MainViewState::SearchResultActionPanel { .. } => { @@ -1262,8 +1268,8 @@ impl Application for AppModel { } } } - GlobalState::MainView { focused_search_result, sub_state, prompt, search_field_id, .. } => { - let input: Element<_> = text_input("Search...", prompt) + GlobalState::MainView { focused_search_result, sub_state, search_field_id, .. } => { + let input: Element<_> = text_input("Search...", &self.prompt) .on_input(AppMsg::PromptChanged) .on_submit(AppMsg::PromptSubmit) .ignore_with_modifiers(true) @@ -1551,6 +1557,8 @@ impl AppModel { GlobalState::initial(&mut self.global_state, self.client_context.clone()) ); + self.prompt = "".to_string(); + let mut client_context = self.client_context.write().expect("lock is poisoned"); client_context.clear_all_inline_views(); diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index f6835b5..4d70e3b 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -20,7 +20,6 @@ pub enum GlobalState { search_field_id: text_input::Id, // ephemeral state - prompt: String, focused_search_result: ScrollHandle, // state @@ -69,7 +68,6 @@ impl GlobalState { pub fn new(search_field_id: text_input::Id, client_context: Arc>) -> GlobalState { GlobalState::MainView { search_field_id, - prompt: "".to_string(), focused_search_result: ScrollHandle::new(true), sub_state: MainViewState::new(), pending_plugin_view_data: None, From f6e3748e780ceb762311945032a8f5e8956688c6 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 13 Oct 2024 17:37:55 +0200 Subject: [PATCH 139/540] Update CHANGELOG.md, README.md and THEME.md --- CHANGELOG.md | 95 ++++++++++++++++++++++++++++++++++++++++++++- README.md | 104 +++++++++++++++++++++++++++----------------------- docs/THEME.md | 4 +- 3 files changed, 152 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efd0d75..b073a4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,99 @@ and this project doesn't adhere to Semantic Versioning, see [Versioning](./READM For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://github.com/project-gauntlet/tools/blob/main/CHANGELOG.md) ## [Unreleased] +### General +- Main view now has action bar and action panel + - Action bar displays current primary action depending on focused result item + - ALT + K (OPT + K on macOS) is available to open action panel + - Content of action panel can be defined by plugins + - `"inline-view"` and `"command-generator"` entrypoint types can now specify custom actions on main view + - Plugin can also provide shortcut that will be available depending on focused result item without opening the action panel +- Primary and secondary actions + - First action in action panel is now considered primary and can be run using ENTER without opening action panel + - Second action in action panel is now considered secondary and can be run using SHIFT + ENTER without opening action panel + - Works for all places that can define actions: `"inline-view"`, `"command-generator"` and `"view"` entrypoint types +- Action panel now supports keyboard navigation +- All bundled plugins are merged into one +- It is now possible to update plugin using "Check for updates" button in settings + +### Bundled plugin +#### `Applications` +- Add Flatpak application support on Linux +- Fixed no applications being shown on macOS Sequoia (15) +- Fixed crash on macOS if macOS version only contains two segments, e.g. `15.0` vs `15.0.1` +- Fixed some applications not having icons on macOS + +#### `Calculator` +- It is now possible to copy result of calculation using primary action and its shortcut + - After copying, popup is shown to indicate that the result was copied +- Updated `numbat` dependency to [1.14.0](https://github.com/sharkdp/numbat/releases/tag/v1.14.0) + - Notable change: "Add lowercase aliases for currency units" + +### Plugin API +- Plugin permissions reworked + - **BREAKING CHANGE**: Plugin manifest property `permissions.ffi` removed + - FFI in Deno is an unstable feature + - May be brought back in future + - **BREAKING CHANGE**: Plugin manifest property `permissions.high_resolution_time` removed + - This is done in preparation for Deno update, newer versions of which removed this permission + - **BREAKING CHANGE**: Plugin manifest property `permissions.fs_read_access` renamed to `permissions.filesystem.read` + - **BREAKING CHANGE**: Plugin manifest property `permissions.fs_write_access` renamed to `permissions.filesystem.write` + - **BREAKING CHANGE**: Plugin manifest property `permissions.run_subprocess` has been split into 2 properties: `permissions.exec.command` and `permissions.exec.executable` + - `command` is for commands on `PATH`, e.g. `"ls"` + - `executable` is for absolute paths to binary, e.g. `"/usr/bin/ls"` + - **BREAKING CHANGE**: Windows-style paths are not allowed in plugins that do not support Windows + - **BREAKING CHANGE**: Unix-style paths are not allowed in plugins that do not support Linux or macOS + - **BREAKING CHANGE**: Plugin manifest property `permissions.network` now can only contain domain and optionally port of URL + - **BREAKING CHANGE**: Path permissions (`permissions.filesystem.read`, `permissions.filesystem.write` and `permissions.exec.executable`) now can only contain absolute paths + - Path permissions (`permissions.filesystem.read`, `permissions.filesystem.write` and `permissions.exec.executable`) can now contain variables which will be replaced at plugin load time + - Examples: `{linux:user-home}/.local/share`, `{common:plugin-cache}/my-plugin-cache` + - Variables can only be used at the beginning of the path + - List of currently available variables + - `{macos:user-home}` + - Resolves to `$HOME`, i.e. `/Users/` + - Only available if plugin supports macOS + - `{linux:user-home}` + - Resolves to `$HOME`, i.e. `/home/` + - Only available if plugin supports Linux + - `{windows:user-home}` + - Resolves to `{FOLDERID_Profile}`, i.e. `C:\Users\` + - Only available if plugin supports Windows + - `{common:plugin-data}` + - On Windows: `{FOLDERID_RoamingAppData}\Gauntlet\data\plugins\` + - On Linux: `$XDG_DATA_HOME/gauntlet/plugins/` + - On macOS: `$HOME/Library/Application Support/dev.project-gauntlet.gauntlet/plugins/` + - `{common:plugin-cache}` + - On Windows: `{FOLDERID_LocalAppData}\Gauntlet\cache\plugins\` + - On Linux: `$XDG_CACHE_HOME/gauntlet/plugins/` + - On macOS: `$HOME/Library/Application Support/dev.project-gauntlet.gauntlet/plugins/` +- ``'s `title` property is now optional +- `` have a new `accessory` property, which provides an ability to specify text and/or icon under the grid cell +- `` have a new `accessories` property, which provides an ability to specify one or multiple text and/or icon items on the right side of list item +- **BREAKING CHANGE**: ``'s `title` property renamed to `label` +- Added `entrypoint.icon` plugin manifest property that accepts path to image inside plugin's `assets` directory +- Added `showHud` function that will create a simple popup window with text provided to that function + +### Theming API +- **BREAKING CHANGE**: Current color theme version increased to `3` +- **BREAKING CHANGE**: Current everything theme version increased to `3` + +### UI/UX Improvements +- Grid styling refined +- Inline view styling refined +- Plugin and entrypoint names of rendered inline view are now shown above that inline view +- Made color of text slightly more bright +- Focused (by keyboard navigation) and hovered (by hovering with mouse) search items now have distinct styling +- Slightly increased size of icons in main search view +- Plugin ID is now shown in sidebar in settings when plugin is selected +- "Remove plugin" button has been moved to the bottom of the sidebar in settings +- In settings required preferences that do not have value provided or do not have default value are now highlighted +- Names of keys of shortcuts were changed from all upper-case to first letter only upper-case + +### Fixes +- Fixed panic when trying to stop already stopped plugin +- Fixed crash on macOS if `openssl@v3` library is not installed +- Fixed inline view still being shown after main view was closed and reopened +- Fixed download info panel in settings sometimes going outside of window size and being cut off ## [9] - 2024-09-15 @@ -49,7 +142,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git - Refined styling to accommodate this change - **BREAKING CHANGE**: Current color theme version increased to `2` - **BREAKING CHANGE**: Current everything theme version increased to `2` - + ### `Applications` plugin - Add macOS System settings items like Sound, Network, etc - Both pre- and post-Ventura macOS settings are supported diff --git a/README.md b/README.md index b795984..43e8e59 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,16 @@ https://github.com/user-attachments/assets/19964ed6-9cd9-48d4-9835-6be04de14b66 - Dynamically provide list of one-shot commands - Render quick "inline" content directly under main search bar based on value in it - Get content from and add to Clipboard - - Currently, 3 bundled plugins are provided - - Applications: provides list of applications - - Calculator: shows result of mathematical operations directly under main search bar - - Powered by [Numbat](https://github.com/sharkdp/numbat) - - Settings: open Gauntlet Settings from Gauntlet itself - Plugins are distributed as separate branch in Git repository, meaning plugin distribution doesn't need any central server - Plugins IDs are just Git Repository URLs +- Built-in functionality is provided by bundled plugin + - Applications: shows applications installed on the system in search results + - Calculator: shows result of mathematical operations directly under main search bar + - Includes converting currency using exchange rates + - Powered by [Numbat](https://github.com/sharkdp/numbat) + - Settings: open Gauntlet Settings + - More to come, see [#15](https://github.com/project-gauntlet/gauntlet/issues/15) - [React](https://github.com/facebook/react)-based UI for plugins - Implemented using custom React Reconciler (no Electron) - [iced-rs](https://github.com/iced-rs/iced) is used for UI @@ -41,12 +43,13 @@ https://github.com/user-attachments/assets/19964ed6-9cd9-48d4-9835-6be04de14b66 - Frecency-based search result ordering - Frecency is a combination of frequency and recency - More often the item is used the higher in the result list it will be, but items used a lot in the past will be ranked lower than items used the same amount of times recently + - Currently, there is no fuzzy matching. Results are matched per word by substring - Designed with cross-platform in mind - Permissions - By default, plugins do not have access to host system - - If plugin asked for access to filesystem, env variables, FFI or running commands, it is required to specify + - If plugin asked for access to filesystem, env variables or running commands, it is required to specify which operating systems it supports. - - If plugin doesn't use filesystem, env variables, ffi or running commands and just uses network and/or UI, it + - If plugin doesn't use filesystem, env variables or running commands and just uses network and/or UI, it is cross-platform - Shortcuts - Plugins are allowed to use only limited set of keys for shortcuts to support widest possible range of keyboards @@ -67,30 +70,25 @@ https://github.com/user-attachments/assets/19964ed6-9cd9-48d4-9835-6be04de14b66 - Both X11 and Wayland (via LayerShell protocol) are supported - macOS - Windows - - built-in "Applications" plugin is not yet implemented. See [#9](https://github.com/project-gauntlet/gauntlet/issues/9) + - Bundled "Applications" plugin is not yet implemented. See [#9](https://github.com/project-gauntlet/gauntlet/issues/9) -##### UI +##### Planned features -###### Implemented +- See [#13](https://github.com/project-gauntlet/gauntlet/issues/13) +- See [#15](https://github.com/project-gauntlet/gauntlet/issues/15) +- See [#16](https://github.com/project-gauntlet/gauntlet/issues/16) -- Detail -- Form -- Action Panel -- List -- Grid -- Inline - - View directly under main search bar - - Requires separate permission to be explicitly specified in manifest because it reads everything user enters in main search bar -- Settings window -- Action Shortcuts -- Theming - -###### Planned - -See [#13](https://github.com/project-gauntlet/gauntlet/issues/13) - -##### APIs +##### Plugin APIs +- UI + - Detail + - Form + - Action Panel + - List + - Grid + - Inline + - View directly under main search bar + - Requires separate permission to be explicitly specified in manifest because it reads everything user enters in main search bar - Stack-based Navigation - Assets - Files placed into `assets` directory in root of plugin repository are accessible at plugin runtime using `assetData` function @@ -99,6 +97,9 @@ See [#13](https://github.com/project-gauntlet/gauntlet/issues/13) - Clipboard - Accessible via `Clipboard` api - Requires separate permission to be explicitly specified in manifest +- HUD + - Shows small popup window with feedback information + - Accessible via `showHud` function - React Helper Hooks - `usePromise` - Helper to run promises in a context of React view @@ -120,10 +121,6 @@ See [#13](https://github.com/project-gauntlet/gauntlet/issues/13) - Follows `stale-while-revalidate` caching strategy - Uses `useCachedPromise` Hook internally -###### Planned - -See [#13](https://github.com/project-gauntlet/gauntlet/issues/13) - ## Getting Started ### Create your own plugin @@ -260,15 +257,33 @@ path = 'src/inline-view.tsx' type = 'inline-view' description = 'Some entrypoint description' # required -[permissions] # For allowed values see: https://docs.deno.com/runtime/manual/basics/permissions -environment = ["ENV_VAR_NAME"] # array of strings, if specified requires supported_system to be specified as well -high_resolution_time = false # boolean -network = ["github.com"] # array of strings -ffi = ["path/to/dynamic/lib"] # array of strings, if specified requires supported_system to be specified as well -fs_read_access = ["path/to/something"] # array of strings, if specified requires supported_system to be specified as well -fs_write_access = ["path/to/something"] # array of strings, if specified requires supported_system to be specified as well -run_subprocess = ["program"] # array of strings, if specified requires supported_system to be specified as well -system = ["apiName"] # array of strings, if specified requires supported_system to be specified as well +[permissions] +network = ["github.com", "example.com:8833"] +clipboard = ["read", "write", "clear"] +main_search_bar = ["read"] + +# if specified requires supported_system to be specified as well +environment = ["ENV_VAR_NAME"] + +# if specified requires supported_system to be specified as well +system = ["apiName"] + +# if specified requires supported_system to be specified as well +[permissions.filesystem] +read = [ + "C:\\ProgramFiles\\test", + "C:/ProgramFiles/test", + "{windows:user-home}\\test", + "{windows:user-home}/test", + "{linux:user-home}/test", + "/etc/test" +] +write = ["/home/exidex/.test"] + +# if specified requires supported_system to be specified as well +[permissions.exec] +command = ["ls"] +executable = ["/usr/bin/ls"] [[supported_system]] os = 'linux' # 'linux', 'windows' or 'macos' @@ -319,7 +334,7 @@ Server is an application that exposes gRPC server. All plugins run on server. Each plugin in its own sandboxed Deno Worker. In plugin manifest it is possible to configure permissions which will allow plugin to have access to filesystem, -network, environment variables, ffi or subprocess execution. +network, environment variables or subprocess execution. Server saves plugins themselves and state of plugins into SQLite database. Frontend is GUI module that uses [iced-rs](https://github.com/iced-rs/iced) as a GUI framework. It is run in the same process as a server. @@ -346,13 +361,6 @@ Plugins (or rather its compiled state: manifest, js code and assets) are distrib Which means there is no one central place required for plugin distribution. And to install plugin all you need is Git repository url. -Application defines set of React components to use for plugins. -Creating and validating components involves some boilerplate. -Component model was created for help manage is. -It is essentially a json file which defines what components exist, what properties and event handler they have. -This file is then used -to generate TypeScript typings for `@project-gauntlet/api` and Rust validation code for server and frontend. - ## Application packaging for Linux This section contains a list of things diff --git a/docs/THEME.md b/docs/THEME.md index 303fb6b..eb50c6c 100644 --- a/docs/THEME.md +++ b/docs/THEME.md @@ -13,8 +13,8 @@ theme will stop working until it is updated. This may change in the future Current theme version: -- Color: `2` -- Everything: `2` +- Color: `3` +- Everything: `3` Theming is only applied to main window and doesn't affect settings From cbcbcf8b9450b3d42664f00359161680918ee277 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 13 Oct 2024 15:42:37 +0000 Subject: [PATCH 140/540] Prepare for v10 release --- CHANGELOG.md | 2 ++ VERSION | 2 +- js/api/package.json | 2 +- js/deno/package.json | 2 +- package-lock.json | 4 ++-- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b073a4b..eee2448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project doesn't adhere to Semantic Versioning, see [Versioning](./READM For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://github.com/project-gauntlet/tools/blob/main/CHANGELOG.md) ## [Unreleased] + +## [10] - 2024-10-13 ### General - Main view now has action bar and action panel - Action bar displays current primary action depending on focused result item diff --git a/VERSION b/VERSION index f11c82a..9a03714 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -9 \ No newline at end of file +10 \ No newline at end of file diff --git a/js/api/package.json b/js/api/package.json index ba0c04e..2feac9d 100644 --- a/js/api/package.json +++ b/js/api/package.json @@ -1,6 +1,6 @@ { "name": "@project-gauntlet/api", - "version": "0.9.0", + "version": "0.10.0", "type": "module", "exports": { "./components": { diff --git a/js/deno/package.json b/js/deno/package.json index 293b7ce..7452087 100644 --- a/js/deno/package.json +++ b/js/deno/package.json @@ -1,6 +1,6 @@ { "name": "@project-gauntlet/deno", - "version": "0.9.0", + "version": "0.10.0", "type": "module", "exports": { ".": { diff --git a/package-lock.json b/package-lock.json index 3c3fbd4..59cae4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ }, "js/api": { "name": "@project-gauntlet/api", - "version": "0.9.0", + "version": "0.10.0", "devDependencies": { "@project-gauntlet/typings": "*", "typescript": "^5.3.3" @@ -99,7 +99,7 @@ }, "js/deno": { "name": "@project-gauntlet/deno", - "version": "0.9.0", + "version": "0.10.0", "devDependencies": { "@types/node": "^18.17.1", "typescript": "^5.3.3" From ed29af658347e445f3f20be6d5ae0a86ab40e43b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 18 Oct 2024 18:35:29 +0200 Subject: [PATCH 141/540] Fix hud window not disappearing on wayland --- rust/client/src/ui/hud/mod.rs | 56 +++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/rust/client/src/ui/hud/mod.rs b/rust/client/src/ui/hud/mod.rs index 9521eda..c45a853 100644 --- a/rust/client/src/ui/hud/mod.rs +++ b/rust/client/src/ui/hud/mod.rs @@ -4,7 +4,6 @@ use iced::window::{Level, Position, Settings}; use iced::{window, Command, Size}; use std::convert; use std::time::Duration; -use iced::window::settings::PlatformSpecific; const HUD_WINDOW_WIDTH: f32 = 400.0; const HUD_WINDOW_HEIGHT: f32 = 40.0; @@ -23,7 +22,7 @@ pub fn show_hud_window( visible: true, level: Level::AlwaysOnTop, #[cfg(target_os = "macos")] - platform_specific: PlatformSpecific { + platform_specific: iced::window::settings::PlatformSpecific { activation_policy: window::settings::ActivationPolicy::Accessory, activate_ignoring_other_apps: false, ..Default::default() @@ -32,19 +31,21 @@ pub fn show_hud_window( ..Default::default() }; - let (id, show_command) = window::spawn(settings); - - let close_command = Command::perform(async move { - tokio::time::sleep(Duration::from_secs(2)).await; - - AppMsg::CloseHudWindow { id } - }, convert::identity); - - #[cfg(target_os = "linux")] if wayland { - iced::wayland::commands::layer_surface::get_layer_surface(layer_shell_settings()) + let id = window::Id::unique(); + + let show_command = iced::wayland::commands::layer_surface::get_layer_surface(layer_shell_settings(id)); + let close_command = in_2_seconds(AppMsg::CloseHudWindow { id }); + + Command::batch([ + show_command, + close_command + ]) } else { + let (id, show_command) = window::spawn(settings); + let close_command = in_2_seconds(AppMsg::CloseHudWindow { id }); + Command::batch([ show_command, close_command @@ -52,10 +53,15 @@ pub fn show_hud_window( } #[cfg(not(target_os = "linux"))] - Command::batch([ - show_command, - close_command - ]) + { + let (id, show_command) = window::spawn(settings); + let close_command = in_2_seconds(AppMsg::CloseHudWindow { id }); + + Command::batch([ + show_command, + close_command + ]) + } } pub fn close_hud_window( @@ -63,23 +69,21 @@ pub fn close_hud_window( wayland: bool, id: window::Id ) -> Command { - let command = window::close(id); - #[cfg(target_os = "linux")] if wayland { iced::wayland::commands::layer_surface::destroy_layer_surface(id) } else { - command + window::close(id) } #[cfg(not(target_os = "linux"))] - command + window::close(id) } #[cfg(target_os = "linux")] -fn layer_shell_settings() -> iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { +fn layer_shell_settings(id: window::Id) -> iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { - id: window::Id::unique(), + id, layer: iced::wayland::commands::layer_surface::Layer::Overlay, keyboard_interactivity: iced::wayland::commands::layer_surface::KeyboardInteractivity::None, pointer_interactivity: false, @@ -92,3 +96,11 @@ fn layer_shell_settings() -> iced::wayland::runtime::command::platform_specific: size_limits: Limits::new(Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT), Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT)), } } + +fn in_2_seconds(msg: AppMsg) -> Command { + Command::perform(async move { + tokio::time::sleep(Duration::from_secs(2)).await; + + msg + }, convert::identity) +} From c3c58d31283207bd5918edd67ad188f21989a193 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 18 Oct 2024 19:10:30 +0200 Subject: [PATCH 142/540] Reduce scope of try catch in calculator bundled plugin --- bundled_plugins/gauntlet/src/calculator.tsx | 84 +++++++++++---------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/bundled_plugins/gauntlet/src/calculator.tsx b/bundled_plugins/gauntlet/src/calculator.tsx index a9d2d8e..3a2ad06 100644 --- a/bundled_plugins/gauntlet/src/calculator.tsx +++ b/bundled_plugins/gauntlet/src/calculator.tsx @@ -13,45 +13,51 @@ interface InternalApi { export default function Calculator(props: { text: string }): ReactNode | undefined { const text = props.text; - try { - if (text.length < 3) { - return undefined - } - - const { left, right } = InternalApi.run_numbat(text); - - if (left == right) { - return undefined - } - - return ( - - { - await Clipboard.writeText(right) - showHud("Result copied") - }} - /> - - } - > - - - {left} - - - - - - {right} - - - - ) - } catch (e) { + if (text.length < 3) { return undefined } + + let result; + + try { + result = InternalApi.run_numbat(text); + } catch (e) { + // this view is executed on every key press in main search bar + // when numbat run fails it means expression is not valid so we return here and do not show inline view + return undefined + } + + const { left, right } = result; + + if (left == right) { + return undefined + } + + return ( + + { + await Clipboard.writeText(right) + showHud("Result copied") + }} + /> + + } + > + + + {left} + + + + + + {right} + + + + ) } From 97ff3e53b6a27f41a3f245c1977385691289e86b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 18 Oct 2024 20:46:57 +0200 Subject: [PATCH 143/540] Fix clipboard operations not working on wayland --- Cargo.lock | 157 ++++++++++++++++++++++++++++++++++++----- rust/server/Cargo.toml | 2 +- 2 files changed, 141 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fcac34c..862ac94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,20 +264,22 @@ checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" [[package]] name = "arboard" -version = "3.4.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89" +checksum = "ac57f2b058a76363e357c056e4f74f1945bf734d37b8b3ef49066c4787dde0fc" dependencies = [ - "clipboard-win", - "core-graphics 0.23.2", - "image 0.25.1", + "clipboard-win 4.5.0", + "core-graphics 0.22.3", + "image 0.24.9", "log", - "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc", + "objc-foundation", + "objc_id", "parking_lot 0.12.1", - "windows-sys 0.48.0", - "x11rb", + "thiserror", + "winapi", + "wl-clipboard-rs", + "x11rb 0.10.1", ] [[package]] @@ -1116,13 +1118,24 @@ dependencies = [ "utils", ] +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code 2.3.1", + "str-buf", + "winapi", +] + [[package]] name = "clipboard-win" version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" dependencies = [ - "error-code", + "error-code 3.2.0", ] [[package]] @@ -1152,7 +1165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" dependencies = [ "thiserror", - "x11rb", + "x11rb 0.13.0", ] [[package]] @@ -2385,6 +2398,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derive-new" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" +dependencies = [ + "proc-macro2 1.0.80", + "quote 1.0.36", + "syn 1.0.109", +] + [[package]] name = "derive_builder" version = "0.20.1" @@ -2845,6 +2869,16 @@ dependencies = [ "libc", ] +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + [[package]] name = "error-code" version = "3.2.0" @@ -3454,6 +3488,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -6093,6 +6137,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "os_pipe" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "ouroboros" version = "0.18.3" @@ -8387,7 +8441,7 @@ dependencies = [ "wayland-sys 0.31.1", "web-sys", "windows-sys 0.52.0", - "x11rb", + "x11rb 0.13.0", ] [[package]] @@ -8680,6 +8734,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + [[package]] name = "strfmt" version = "0.2.4" @@ -9914,6 +9974,20 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tree_magic_mini" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469a727cac55b41448315cc10427c069c618ac59bb6a4480283fcd811749bdc2" +dependencies = [ + "fnv", + "home", + "memchr", + "nom", + "once_cell", + "petgraph", +] + [[package]] name = "triomphe" version = "0.1.11" @@ -10918,6 +10992,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "winapi-wsapoll" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eafc5f679c576995526e81635d0cf9695841736712b4e892f87abbe6fed3f28" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -10930,7 +11013,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d692d46038c433f9daee7ad8757e002a4248c20b0a3fbc991d99521d3bcb6d" dependencies = [ - "clipboard-win", + "clipboard-win 5.3.1", "clipboard_macos", "clipboard_wayland", "clipboard_x11", @@ -11224,7 +11307,7 @@ dependencies = [ "web-time", "windows-sys 0.48.0", "x11-dl", - "x11rb", + "x11rb 0.13.0", "xkbcommon-dl", ] @@ -11274,6 +11357,24 @@ dependencies = [ "toml 0.5.11", ] +[[package]] +name = "wl-clipboard-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "981a303dfbb75d659f6612d05a14b2e363c103d24f676a2d44a00d18507a1ad9" +dependencies = [ + "derive-new", + "libc", + "log", + "nix 0.24.3", + "os_pipe", + "tempfile", + "thiserror", + "tree_magic_mini", + "wayland-client 0.29.5", + "wayland-protocols 0.29.5", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -11285,6 +11386,19 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" +dependencies = [ + "gethostname 0.2.3", + "nix 0.24.3", + "winapi", + "winapi-wsapoll", + "x11rb-protocol 0.10.0", +] + [[package]] name = "x11rb" version = "0.13.0" @@ -11292,12 +11406,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" dependencies = [ "as-raw-xcb-connection", - "gethostname", + "gethostname 0.4.3", "libc", "libloading 0.8.3", "once_cell", "rustix", - "x11rb-protocol", + "x11rb-protocol 0.13.0", +] + +[[package]] +name = "x11rb-protocol" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" +dependencies = [ + "nix 0.24.3", ] [[package]] diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 44c4d9c..e6c66b5 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -34,7 +34,7 @@ numbat = "1.14.0" uuid = "1.8" resvg = { version = "0.41", default-features = false} image = "0.25" -arboard = "3.4.0" +arboard = { version = "=3.2.1", features = ["wayland-data-control"] } # TODO update when dependency hell is solved global-hotkey = "0.4.2" ureq = "2.10.0" bytes = "1.6.0" From b9eea0d5ef5fc2253e4e7f26d6143c08b46c9337 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 18 Oct 2024 21:42:15 +0200 Subject: [PATCH 144/540] Fix primary action of first search result being called if primary action of inline view is called using enter key, causing duplicate action --- js/react_renderer/src/renderer.ts | 3 ++- rust/client/src/ui/mod.rs | 20 +++++++------------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index 5c9801a..5aee8b6 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -326,7 +326,8 @@ export const createHostConfig = (): HostConfig< }, replaceContainerChildren(container: RootUiWidget, newChildren: ChildSet): void { - InternalApi.op_log_trace("renderer_js_persistence", `replaceContainerChildren is called, container: ${Deno.inspect(container)}, newChildren: ${Deno.inspect(newChildren)}`) + // TODO Deno.inspect is always executed + InternalApi.op_log_trace("renderer_js_persistence", `replaceContainerChildren is called, container: ${Deno.inspect(container)}, newChildren: ${Deno.inspect(newChildren, { depth: Number.MAX_VALUE })}`) container.widgetChildren = newChildren diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 8ec10d4..b5743b4 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -912,9 +912,7 @@ impl Application for AppModel { }; let render_location = UiRenderLocation::InlineView; - Command::batch([ - Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) - ]) + Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) } None => Command::none() } @@ -1553,16 +1551,6 @@ impl AppModel { GlobalState::ErrorView { .. } => {} } - commands.push( - GlobalState::initial(&mut self.global_state, self.client_context.clone()) - ); - - self.prompt = "".to_string(); - - let mut client_context = self.client_context.write().expect("lock is poisoned"); - - client_context.clear_all_inline_views(); - Command::batch(commands) } @@ -1598,6 +1586,12 @@ impl AppModel { } fn reset_window_state(&mut self) -> Command { + self.prompt = "".to_string(); + + let mut client_context = self.client_context.write().expect("lock is poisoned"); + + client_context.clear_all_inline_views(); + let mut commands = vec![ GlobalState::initial(&mut self.global_state, self.client_context.clone()), ]; From 7bd8a5f58304f3a7e68331e2f1e94d114575c8fb Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 20 Oct 2024 14:43:46 +0200 Subject: [PATCH 145/540] Rework clipboard, fixes writeText() not working on KDE --- rust/server/src/plugins/js/clipboard.rs | 270 +++++++++++++++--------- rust/server/src/plugins/js/mod.rs | 14 +- rust/server/src/plugins/mod.rs | 9 +- 3 files changed, 189 insertions(+), 104 deletions(-) diff --git a/rust/server/src/plugins/js/clipboard.rs b/rust/server/src/plugins/js/clipboard.rs index 350f31c..4fa4155 100644 --- a/rust/server/src/plugins/js/clipboard.rs +++ b/rust/server/src/plugins/js/clipboard.rs @@ -1,52 +1,34 @@ use std::cell::RefCell; use std::io::Cursor; use std::rc::Rc; -use anyhow::anyhow; +use std::sync::{Arc, RwLock}; +use std::time::Duration; +use anyhow::{anyhow, Context, Error}; use arboard::ImageData; use deno_core::{op, OpState}; use image::RgbaImage; use serde::{Deserialize, Serialize}; use tokio::task::spawn_blocking; use crate::plugins::js::permissions::PluginPermissionsClipboard; -use crate::plugins::js::PluginData; +use crate::plugins::js::{clipboard, PluginData}; -fn unknown_err_clipboard(err: arboard::Error) -> anyhow::Error { - anyhow!("UNKNOWN_ERROR: {:?}", err) +#[derive(Clone)] +pub struct Clipboard { + clipboard: Arc>, } -fn unknown_err_image(err: image::ImageError) -> anyhow::Error { - anyhow!("UNKNOWN_ERROR: {:?}", err) -} +impl Clipboard { + pub fn new() -> anyhow::Result { + let clipboard = arboard::Clipboard::new() + .context("error while creating clipboard")?; -fn unable_to_convert_image_err() -> anyhow::Error { - anyhow!("UNABLE_TO_CONVERT_IMAGE") -} - -#[derive(Debug, Serialize, Deserialize)] -struct ClipboardData { - text_data: Option, - png_data: Option> -} - -#[op] -async fn clipboard_read(state: Rc>) -> anyhow::Result { - { - let state = state.borrow(); - - let allow = state - .borrow::() - .permissions() - .clipboard - .contains(&PluginPermissionsClipboard::Read); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); - } + Ok(Self { + clipboard: Arc::new(RwLock::new(clipboard)), + }) } - spawn_blocking(|| { - let mut clipboard = arboard::Clipboard::new() - .map_err(|err| unknown_err_clipboard(err))?; + fn read(&self) -> anyhow::Result { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); let png_data = match clipboard.get_image() { Ok(data) => { @@ -86,29 +68,10 @@ async fn clipboard_read(state: Rc>) -> anyhow::Result>) -> anyhow::Result> { - { - let state = state.borrow(); - - let allow = state - .borrow::() - .permissions() - .clipboard - .contains(&PluginPermissionsClipboard::Read); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); - } } - spawn_blocking(|| { - let mut clipboard = arboard::Clipboard::new() - .map_err(|err| unknown_err_clipboard(err))?; + fn read_text(&self) -> anyhow::Result> { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); let data = match clipboard.get_text() { Ok(data) => Some(data), @@ -123,28 +86,10 @@ async fn clipboard_read_text(state: Rc>) -> anyhow::Result>, data: ClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2 - { - let state = state.borrow(); - - let allow = state - .borrow::() - .permissions() - .clipboard - .contains(&PluginPermissionsClipboard::Write); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); - } } - spawn_blocking(|| { - let mut clipboard = arboard::Clipboard::new() - .map_err(|err| unknown_err_clipboard(err))?; + fn write(&self, data: ClipboardData) -> anyhow::Result<()> { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); if let Some(png_data) = data.png_data { @@ -175,16 +120,113 @@ async fn clipboard_write(state: Rc>, data: ClipboardData) -> an } Ok(()) - }).await? + } + + fn write_text(&self, data: String) -> anyhow::Result<()> { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); + + clipboard.set_text(data) + .map_err(|err| unknown_err_clipboard(err))?; + + Ok(()) + } + + fn clear(&self) -> anyhow::Result<()> { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); + + clipboard.clear() + .map_err(|err| unknown_err_clipboard(err))?; + + Ok(()) + } +} + +fn unknown_err_clipboard(err: arboard::Error) -> Error { + anyhow!("UNKNOWN_ERROR: {:?}", err) +} + +fn unknown_err_image(err: image::ImageError) -> Error { + anyhow!("UNKNOWN_ERROR: {:?}", err) +} + +fn unable_to_convert_image_err() -> Error { + anyhow!("UNABLE_TO_CONVERT_IMAGE") +} + +#[derive(Debug, Serialize, Deserialize)] +struct ClipboardData { + text_data: Option, + png_data: Option> } #[op] -async fn clipboard_write_text(state: Rc>, data: String) -> anyhow::Result<()> { - { +async fn clipboard_read(state: Rc>) -> anyhow::Result { + let clipboard = { let state = state.borrow(); - let allow = state - .borrow::() + let plugin_data = state + .borrow::(); + + let allow = plugin_data + .permissions() + .clipboard + .contains(&PluginPermissionsClipboard::Read); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); + } + + tracing::debug!("Reading from clipboard, plugin id: {:?}", plugin_data.plugin_id); + + let clipboard = state + .borrow::() + .clone(); + + clipboard + }; + + spawn_blocking(move || clipboard.read()).await? +} + + +#[op] +async fn clipboard_read_text(state: Rc>) -> anyhow::Result> { + let clipboard = { + let state = state.borrow(); + + let plugin_data = state + .borrow::(); + + let allow = plugin_data + .permissions() + .clipboard + .contains(&PluginPermissionsClipboard::Read); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); + } + + tracing::debug!("Reading text from clipboard, plugin id: {:?}", plugin_data.plugin_id); + + let clipboard = state + .borrow::() + .clone(); + + clipboard + }; + + spawn_blocking(move || clipboard.read_text()).await? +} + +#[op] +async fn clipboard_write(state: Rc>, data: ClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2 + let clipboard = { + let state = state.borrow(); + + let plugin_data = state + .borrow::(); + + let allow = plugin_data .permissions() .clipboard .contains(&PluginPermissionsClipboard::Write); @@ -192,26 +234,57 @@ async fn clipboard_write_text(state: Rc>, data: String) -> anyh if !allow { return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); } - } - spawn_blocking(|| { - let mut clipboard = arboard::Clipboard::new() - .map_err(|err| unknown_err_clipboard(err))?; + tracing::debug!("Writing to clipboard, plugin id: {:?}", plugin_data.plugin_id); - clipboard.set_text(data) - .map_err(|err| unknown_err_clipboard(err))?; + let clipboard = state + .borrow::() + .clone(); - Ok(()) - }).await? + clipboard + }; + + spawn_blocking(move || clipboard.write(data)).await? +} + +#[op] +async fn clipboard_write_text(state: Rc>, data: String) -> anyhow::Result<()> { + let clipboard = { + let state = state.borrow(); + + let plugin_data = state + .borrow::(); + + let allow = plugin_data + .permissions() + .clipboard + .contains(&PluginPermissionsClipboard::Write); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); + } + + tracing::debug!("Writing text to clipboard, plugin id: {:?}", plugin_data.plugin_id); + + let clipboard = state + .borrow::() + .clone(); + + clipboard + }; + + spawn_blocking(move || clipboard.write_text(data)).await? } #[op] async fn clipboard_clear(state: Rc>) -> anyhow::Result<()> { - { + let clipboard = { let state = state.borrow(); - let allow = state - .borrow::() + let plugin_data = state + .borrow::(); + + let allow = plugin_data .permissions() .clipboard .contains(&PluginPermissionsClipboard::Clear); @@ -219,14 +292,15 @@ async fn clipboard_clear(state: Rc>) -> anyhow::Result<()> { if !allow { return Err(anyhow!("Plugin doesn't have 'clear' permission for clipboard")); } - } - spawn_blocking(|| { - let mut clipboard = arboard::Clipboard::new() - .map_err(|err| unknown_err_clipboard(err))?; + tracing::debug!("Clearing clipboard, plugin id: {:?}", plugin_data.plugin_id); - clipboard.clear()?; + let clipboard = state + .borrow::() + .clone(); - Ok(()) - }).await? + clipboard + }; + + spawn_blocking(move || clipboard.clear()).await? } diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 126eb91..1aa246b 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -37,7 +37,7 @@ use crate::plugins::applications::{get_apps, DesktopEntry}; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; -use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; +use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text, Clipboard}; use crate::plugins::js::command_generators::get_command_generator_entrypoint_ids; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; use crate::plugins::js::permissions::{permissions_to_deno, PluginPermissions, PluginPermissionsClipboard}; @@ -57,7 +57,7 @@ mod assets; mod preferences; mod search; mod command_generators; -mod clipboard; +pub mod clipboard; pub mod permissions; pub struct PluginRuntimeData { @@ -74,6 +74,7 @@ pub struct PluginRuntimeData { pub icon_cache: IconCache, pub frontend_api: FrontendApi, pub dirs: Dirs, + pub clipboard: Clipboard, } pub struct PluginCode { @@ -252,7 +253,8 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run data.db_repository, data.search_index, data.icon_cache, - data.dirs + data.dirs, + data.clipboard ).await }) } => { @@ -294,6 +296,7 @@ async fn start_js_runtime( search_index: SearchIndex, icon_cache: IconCache, dirs: Dirs, + clipboard: Clipboard, ) -> anyhow::Result<()> { let dev_plugin = plugin_id.to_string().starts_with("file://"); @@ -353,7 +356,8 @@ async fn start_js_runtime( repository, search_index, icon_cache, - numbat_context + numbat_context, + clipboard, )], // maybe_inspector_server: Some(inspector_server.clone()), // should_wait_for_inspector_session: true, @@ -543,6 +547,7 @@ deno_core::extension!( search_index: SearchIndex, icon_cache: IconCache, numbat_context: Option, + clipboard: Clipboard, }, state = |state, options| { state.put(options.event_receiver); @@ -553,6 +558,7 @@ deno_core::extension!( state.put(options.search_index); state.put(options.icon_cache); state.put(options.numbat_context); + state.put(options.clipboard); }, ); diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 64a5242..b2d77eb 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -21,6 +21,7 @@ use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_st use crate::plugins::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; use crate::plugins::icon_cache::IconCache; use crate::plugins::js::{AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginRuntimeData, start_plugin_runtime}; +use crate::plugins::js::clipboard::Clipboard; use crate::plugins::js::permissions::{PluginPermissions, PluginPermissionsClipboard, PluginPermissionsExec, PluginPermissionsFileSystem, PluginPermissionsMainSearchBar}; use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; @@ -53,7 +54,8 @@ pub struct ApplicationManager { frontend_api: FrontendApi, global_hotkey_manager: GlobalHotKeyManager, current_hotkey: Mutex>, - dirs: Dirs + dirs: Dirs, + clipboard: Clipboard, } impl ApplicationManager { @@ -67,6 +69,7 @@ impl ApplicationManager { let run_status_holder = RunStatusHolder::new(); let search_index = SearchIndex::create_index(frontend_api.clone())?; let global_hotkey_manager = GlobalHotKeyManager::new()?; + let clipboard = Clipboard::new()?; let (command_broadcaster, _) = tokio::sync::broadcast::channel::(100); @@ -82,6 +85,7 @@ impl ApplicationManager { icon_cache, frontend_api, global_hotkey_manager, + clipboard, current_hotkey: Mutex::new(None), dirs }; @@ -564,7 +568,8 @@ impl ApplicationManager { search_index: self.search_index.clone(), icon_cache: self.icon_cache.clone(), frontend_api: self.frontend_api.clone(), - dirs: self.dirs.clone() + dirs: self.dirs.clone(), + clipboard: self.clipboard.clone(), }; self.start_plugin_runtime(data); From 689461c456cece002de6b6e6e15385646825b5b1 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:49:07 +0200 Subject: [PATCH 146/540] Refactor ui action execution handling. Change primary action label on action bar into a button --- rust/client/src/ui/mod.rs | 251 +++++++++++++++++--------- rust/client/src/ui/state/mod.rs | 63 ++----- rust/client/src/ui/theme/button.rs | 8 +- rust/client/src/ui/theme/container.rs | 4 + rust/client/src/ui/theme/row.rs | 8 +- rust/client/src/ui/widget.rs | 37 +++- 6 files changed, 222 insertions(+), 149 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index b5743b4..45b93ba 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -120,10 +120,6 @@ pub enum AppMsg { ToggleActionPanel { keyboard: bool }, - OnEntrypointAction { - keyboard: bool, - widget_id: UiWidgetId - }, ShowPreferenceRequiredView { plugin_id: PluginId, entrypoint_id: EntrypointId, @@ -164,6 +160,20 @@ pub enum AppMsg { CloseHudWindow { id: window::Id }, + OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus, + OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result: SearchResult }, + OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result: SearchResult, widget_id: UiWidgetId }, + OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id: UiWidgetId }, + OnPrimaryActionPluginViewNoPanelKeyboardWithFocus { widget_id: UiWidgetId }, + OnPrimaryActionPluginViewAnyPanelKeyboardWithFocus { widget_id: UiWidgetId }, + OnAnyActionPluginViewAnyPanel { widget_id: UiWidgetId }, + OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result: SearchResult }, + OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus, + OnSecondaryActionPluginViewNoPanelKeyboardWithFocus { widget_id: UiWidgetId }, + OnAnyActionMainViewAnyPanelMouse { widget_id: UiWidgetId }, + OnPrimaryActionMainViewActionPanelMouse { widget_id: UiWidgetId }, + ResetMainViewState, + OnAnyActionMainViewNoPanelKeyboardAtIndex { index: usize }, } pub struct AppFlags { @@ -896,98 +906,145 @@ impl Application for AppModel { Command::none() } - AppMsg::OnEntrypointAction { keyboard, widget_id} => { + AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus => { + Command::perform(async {}, move |_| AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index: 0 }) + } + AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result } => { + Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_result, None)) + } + AppMsg::OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id } => { + let run_action_command = if widget_id == 0 { + Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_result, None)) + } else { + Command::perform(async {}, move |_| AppMsg::RunSearchItemAction(search_result, Some(widget_id - 1))) + }; + + Command::batch([ + run_action_command, + Command::perform(async {}, |_| AppMsg::ResetMainViewState) + ]) + } + AppMsg::OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id } => { + let client_context = self.client_context.read().expect("lock is poisoned"); + + match client_context.get_first_inline_view_container() { + Some(container) => { + let plugin_id = container.get_plugin_id(); + + let widget_event = ComponentWidgetEvent::RunAction { + widget_id, + }; + let render_location = UiRenderLocation::InlineView; + + Command::batch([ + Command::perform(async {}, move |_| AppMsg::ToggleActionPanel { keyboard: true }), + Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + ]) + } + None => Command::none() + } + } + AppMsg::OnPrimaryActionPluginViewNoPanelKeyboardWithFocus { widget_id } => { + Command::perform(async {}, move |_| AppMsg::OnAnyActionPluginViewAnyPanel { widget_id }) + } + AppMsg::OnPrimaryActionPluginViewAnyPanelKeyboardWithFocus { widget_id } => { + let client_context = self.client_context.read().expect("lock is poisoned"); + + let plugin_id = client_context.get_view_plugin_id(); + + let widget_event = ComponentWidgetEvent::RunAction { + widget_id, + }; + let render_location = UiRenderLocation::View; + + Command::batch([ + Command::perform(async {}, move |_| AppMsg::ToggleActionPanel { keyboard: true }), + Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + ]) + } + AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id: _ } => { + // widget_id here is always 0 + match &self.global_state { + GlobalState::MainView { focused_search_result, .. } => { + if let Some(search_result) = focused_search_result.get(&self.search_results) { + let search_result = search_result.clone(); + Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result }) + } else { + Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus) + } + } + GlobalState::PluginView { .. } => Command::none(), + GlobalState::ErrorView { .. } => Command::none() + } + } + AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus => { + Command::perform(async {}, move |_| AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index: 1 }) + } + AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result } => { + Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_result, Some(0))) + } + AppMsg::OnSecondaryActionPluginViewNoPanelKeyboardWithFocus { widget_id } => { + Command::perform(async {}, move |_| AppMsg::OnAnyActionPluginViewAnyPanel { widget_id }) + } + AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index } => { + let client_context = self.client_context.read().expect("lock is poisoned"); + + if let Some(container) = client_context.get_first_inline_view_container() { + let plugin_id = container.get_plugin_id(); + let action_ids = container.get_action_ids(); + + match action_ids.get(index) { + Some(widget_id) => { + let widget_id = *widget_id; + + let widget_event = ComponentWidgetEvent::RunAction { + widget_id, + }; + let render_location = UiRenderLocation::InlineView; + + Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + } + None => Command::none() + } + } else { + Command::none() + } + } + AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id } => { match &mut self.global_state { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { - MainViewState::None => { - let client_context = self.client_context.read().expect("lock is poisoned"); - - match client_context.get_first_inline_view_container() { - Some(container) => { - let plugin_id = container.get_plugin_id(); - - let widget_event = ComponentWidgetEvent::RunAction { - widget_id, - }; - let render_location = UiRenderLocation::InlineView; - - Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) - } - None => Command::none() - } - }, + MainViewState::None => Command::none(), MainViewState::SearchResultActionPanel { .. } => { - if let Some(search_item) = focused_search_result.get(&self.search_results) { - MainViewState::initial(sub_state); - - let search_item = search_item.clone(); - if widget_id == 0 { - Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) - } else { - Command::perform(async {}, move |_| AppMsg::RunSearchItemAction(search_item, Some(widget_id - 1))) - } + if let Some(search_result) = focused_search_result.get(&self.search_results) { + let search_result = search_result.clone(); + Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id }) } else { Command::none() } } MainViewState::InlineViewActionPanel { .. } => { - let client_context = self.client_context.read().expect("lock is poisoned"); - - match client_context.get_first_inline_view_container() { - Some(container) => { - let plugin_id = container.get_plugin_id(); - - let widget_event = ComponentWidgetEvent::RunAction { - widget_id, - }; - let render_location = UiRenderLocation::InlineView; - - Command::batch([ - Command::perform(async {}, move |_| AppMsg::ToggleActionPanel { keyboard }), - Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) - ]) - } - None => Command::none() - } + Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id }) } } } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { client_context, sub_state, .. } => { - match sub_state { - PluginViewState::None => { - let client_context = client_context.read().expect("lock is poisoned"); - - let plugin_id = client_context.get_view_plugin_id(); - - let widget_event = ComponentWidgetEvent::RunAction { - widget_id, - }; - - let render_location = UiRenderLocation::View; - - Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) - }, - PluginViewState::ActionPanel { .. } => { - let client_context = client_context.read().expect("lock is poisoned"); - - let plugin_id = client_context.get_view_plugin_id(); - - let widget_event = ComponentWidgetEvent::RunAction { - widget_id, - }; - let render_location = UiRenderLocation::View; - - Command::batch([ - Command::perform(async {}, move |_| AppMsg::ToggleActionPanel { keyboard }), - Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) - ]) - } - } - } + GlobalState::PluginView { .. } => Command::none() } } + AppMsg::OnAnyActionPluginViewAnyPanel { widget_id } => { + let client_context = self.client_context.read().expect("lock is poisoned"); + + let plugin_id = client_context.get_view_plugin_id(); + + let widget_event = ComponentWidgetEvent::RunAction { + widget_id, + }; + + let render_location = UiRenderLocation::View; + + Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + } AppMsg::OpenPluginView(plugin_id, entrypoint_id) => { self.open_plugin_view(plugin_id, entrypoint_id) } @@ -1018,6 +1075,17 @@ impl Application for AppModel { id ) } + AppMsg::ResetMainViewState => { + match &mut self.global_state { + GlobalState::MainView { sub_state, .. } => { + MainViewState::initial(sub_state); + + Command::none() + } + GlobalState::ErrorView { .. } => Command::none(), + GlobalState::PluginView { .. } => Command::none(), + } + } } } @@ -1346,12 +1414,14 @@ impl Application for AppModel { }) .collect(); + let primary_action_widget_id = 0; + if actions.len() == 0 { - (Some((label, default_shortcut)), None) + (Some((label, primary_action_widget_id, default_shortcut)), None) } else { let primary_action = ActionPanelItem::Action { label: label.clone(), - widget_id: 0, + widget_id: primary_action_widget_id, physical_shortcut: Some(default_shortcut.clone()), }; @@ -1362,7 +1432,7 @@ impl Application for AppModel { items: actions, }; - (Some((label, default_shortcut)), Some(action_panel)) + (Some((label, primary_action_widget_id, default_shortcut)), Some(action_panel)) } } else { match inline_view_action_panel(self.client_context.clone()) { @@ -1370,7 +1440,7 @@ impl Application for AppModel { Some(action_panel) => { match action_panel.find_first() { None => (None, None), - Some(action) => { + Some((label, widget_id)) => { let shortcut = PhysicalShortcut { physical_key: PhysicalKey::Enter, modifier_shift: false, @@ -1379,7 +1449,7 @@ impl Application for AppModel { modifier_meta: false }; - (Some((action, shortcut)), Some(action_panel)) + (Some((label, widget_id, shortcut)), Some(action_panel)) } } } @@ -1398,7 +1468,8 @@ impl Application for AppModel { None::<&ScrollHandle>, "".to_string(), || AppMsg::ToggleActionPanel { keyboard: false }, - |widget_id| AppMsg::OnEntrypointAction { widget_id, keyboard: false } + |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, + |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, ) } MainViewState::SearchResultActionPanel { focused_action_item, .. } => { @@ -1412,7 +1483,8 @@ impl Application for AppModel { Some(focused_action_item), "".to_string(), || AppMsg::ToggleActionPanel { keyboard: false }, - |widget_id| AppMsg::OnEntrypointAction { widget_id, keyboard: false } + |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, + |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, ) } MainViewState::InlineViewActionPanel { focused_action_item, .. } => { @@ -1426,7 +1498,8 @@ impl Application for AppModel { Some(focused_action_item), "".to_string(), || AppMsg::ToggleActionPanel { keyboard: false }, - |widget_id| AppMsg::OnEntrypointAction { widget_id, keyboard: false } + |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, + |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, ) } }; diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 4d70e3b..56458ae 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -134,36 +134,26 @@ pub trait Focus { impl Focus for GlobalState { fn primary(&mut self, focus_list: &[SearchResult]) -> Command { match self { - GlobalState::MainView { focused_search_result, sub_state, client_context, .. } => { + GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { MainViewState::None => { - if let Some(search_item) = focused_search_result.get(focus_list) { - let search_item = search_item.clone(); - Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, None)) + if let Some(search_result) = focused_search_result.get(focus_list) { + let search_result = search_result.clone(); + Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result }) } else { - let client_context = client_context.read().expect("lock is poisoned"); - - if let Some(container) = client_context.get_first_inline_view_container() { - let action_ids = container.get_action_ids(); - - match action_ids.get(0) { - Some(widget_id) => { - let widget_id = *widget_id; - - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) - } - None => Command::none() - } - } else { - Command::none() - } + Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus) } } MainViewState::SearchResultActionPanel { focused_action_item, .. } => { match focused_action_item.index { None => Command::none(), Some(widget_id) => { - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + if let Some(search_result) = focused_search_result.get(&focus_list) { + let search_result = search_result.clone(); + Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id }) + } else { + Command::none() + } } } } @@ -171,7 +161,7 @@ impl Focus for GlobalState { match focused_action_item.index { None => Command::none(), Some(widget_id) => { - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id }) } } } @@ -186,7 +176,7 @@ impl Focus for GlobalState { PluginViewState::None => { if let Some(widget_id) = action_ids.get(0) { let widget_id = *widget_id; - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + Command::perform(async {}, move |_| AppMsg::OnPrimaryActionPluginViewNoPanelKeyboardWithFocus { widget_id }) } else { Command::none() } @@ -194,7 +184,7 @@ impl Focus for GlobalState { PluginViewState::ActionPanel { focused_action_item, .. } => { if let Some(widget_id) = focused_action_item.get(&action_ids) { let widget_id = *widget_id; - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + Command::perform(async {}, move |_| AppMsg::OnPrimaryActionPluginViewAnyPanelKeyboardWithFocus { widget_id }) } else { Command::none() } @@ -210,26 +200,11 @@ impl Focus for GlobalState { GlobalState::MainView { focused_search_result, sub_state, client_context, .. } => { match sub_state { MainViewState::None => { - if let Some(search_item) = focused_search_result.get(focus_list) { - let search_item = search_item.clone(); - Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_item, Some(0))) + if let Some(search_result) = focused_search_result.get(focus_list) { + let search_result = search_result.clone(); + Command::perform(async {}, move |_| AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result }) } else { - let client_context = client_context.read().expect("lock is poisoned"); - - if let Some(container) = client_context.get_first_inline_view_container() { - let action_ids = container.get_action_ids(); - - match action_ids.get(1) { - Some(widget_id) => { - let widget_id = *widget_id; - - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) - } - None => Command::none() - } - } else { - Command::none() - } + Command::perform(async {}, move |_| AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus) } } MainViewState::SearchResultActionPanel { .. } | MainViewState::InlineViewActionPanel { .. } => { @@ -247,7 +222,7 @@ impl Focus for GlobalState { PluginViewState::None => { if let Some(widget_id) = action_ids.get(1) { let widget_id = *widget_id; - Command::perform(async {}, move |_| AppMsg::OnEntrypointAction { widget_id, keyboard: true }) + Command::perform(async {}, move |_| AppMsg::OnSecondaryActionPluginViewNoPanelKeyboardWithFocus { widget_id }) } else { Command::none() } diff --git a/rust/client/src/ui/theme/button.rs b/rust/client/src/ui/theme/button.rs index 4ee88f4..df94ce8 100644 --- a/rust/client/src/ui/theme/button.rs +++ b/rust/client/src/ui/theme/button.rs @@ -19,6 +19,7 @@ pub enum ButtonStyle { MainListItemFocused, MetadataLink, RootBottomPanelActionToggleButton, + RootBottomPanelPrimaryActionButton, RootTopPanelBackButton, MetadataTagItem, } @@ -38,6 +39,11 @@ impl ButtonStyle { theme.padding.to_iced() }, + ButtonStyle::RootBottomPanelPrimaryActionButton => { + let theme = &theme.root_bottom_panel_action_toggle_button; + + theme.padding.to_iced() + }, ButtonStyle::RootTopPanelBackButton => { let theme = &theme.root_top_panel_button; @@ -81,7 +87,7 @@ impl ButtonStyle { fn appearance(&self, theme: &GauntletTheme, state: ButtonState) -> Appearance { let (background_color, background_color_hover, text_color, text_color_hover, border_radius, border_width, border_color) = match &self { - ButtonStyle::RootBottomPanelActionToggleButton => { + ButtonStyle::RootBottomPanelPrimaryActionButton | ButtonStyle::RootBottomPanelActionToggleButton => { let theme = &theme.root_bottom_panel_action_toggle_button; (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) }, diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 08d865b..83afb7a 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -59,6 +59,7 @@ pub enum ContainerStyle { InlineName, HudInner, Hud, + RootBottomPanelPrimaryActionButton, } #[derive(Default)] @@ -404,6 +405,9 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau ContainerStyle::RootBottomPanelPrimaryActionText => { self.padding(theme.root_bottom_panel_primary_action_text.padding.to_iced()) } + ContainerStyle::RootBottomPanelPrimaryActionButton => { + self.padding(Padding::from([0.0, theme.root_bottom_panel.spacing, 0.0, 0.0])) + } ContainerStyle::TextAccessory => { self.padding(theme.text_accessory.padding.to_iced()) } diff --git a/rust/client/src/ui/theme/row.rs b/rust/client/src/ui/theme/row.rs index 4a88c5e..fdfdbad 100644 --- a/rust/client/src/ui/theme/row.rs +++ b/rust/client/src/ui/theme/row.rs @@ -1,6 +1,6 @@ -use iced::{Padding, Renderer}; +use crate::ui::theme::{get_theme, Element, GauntletTheme, ThemableWidget}; use iced::widget::Row; -use crate::ui::theme::{Element, GauntletTheme, get_theme, ThemableWidget}; +use iced::Renderer; pub enum RowStyle { ActionShortcut, @@ -9,7 +9,6 @@ pub enum RowStyle { GridSectionTitle, GridItemTitle, RootBottomPanel, - RootBottomPanelPrimaryAction, } impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletTheme, Renderer> { @@ -39,9 +38,6 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletT RowStyle::RootBottomPanel => { self.spacing(theme.root_bottom_panel.spacing) } - RowStyle::RootBottomPanelPrimaryAction => { - self.padding(Padding::from([0.0, theme.root_bottom_panel.spacing, 0.0, 0.0])) - } }.into() } } \ No newline at end of file diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 2c31c8e..f7fa65d 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1352,7 +1352,7 @@ fn render_plugin_root<'a>( let primary_action = action_panel.as_mut() .map(|panel| panel.find_first()) .flatten() - .map(|label| { + .map(|(label, widget_id)| { let shortcut = PhysicalShortcut { physical_key: PhysicalKey::Enter, modifier_shift: false, @@ -1361,7 +1361,7 @@ fn render_plugin_root<'a>( modifier_meta: false }; - (label.to_string(), shortcut) + (label.to_string(), widget_id, shortcut) }); match plugin_view_state { @@ -1376,6 +1376,7 @@ fn render_plugin_root<'a>( None::<&ScrollHandle>, entrypoint_name, || ComponentWidgetEvent::ToggleActionPanel { widget_id }, + |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id }, |widget_id| ComponentWidgetEvent::ActionClick { widget_id } ) } @@ -1390,6 +1391,7 @@ fn render_plugin_root<'a>( Some(&focused_action_item), entrypoint_name, || ComponentWidgetEvent::ToggleActionPanel { widget_id }, + |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id }, |widget_id| ComponentWidgetEvent::ActionClick { widget_id } ) } @@ -1407,7 +1409,7 @@ impl ActionPanel { self.items.iter().map(|item| item.action_count()).sum() } - pub fn find_first(&self) -> Option { + pub fn find_first(&self) -> Option<(String, UiWidgetId)> { ActionPanelItem::find_first(&self.items) } } @@ -1435,11 +1437,11 @@ impl ActionPanelItem { } } - fn find_first(items: &[ActionPanelItem]) -> Option { + fn find_first(items: &[ActionPanelItem]) -> Option<(String, UiWidgetId)> { for item in items { match item { - ActionPanelItem::Action { label, .. } => { - return Some(label.to_string()) + ActionPanelItem::Action { label, widget_id, .. } => { + return Some((label.to_string(), *widget_id)) } ActionPanelItem::ActionSection { items, .. } => { if let Some(item) = Self::find_first(items) { @@ -1653,11 +1655,12 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( top_panel: Element<'a, T>, top_separator: Element<'a, T>, content: Element<'a, T>, - primary_action: Option<(String, PhysicalShortcut)>, + primary_action: Option<(String, UiWidgetId, PhysicalShortcut)>, action_panel: Option, action_panel_scroll_handle: Option<&ScrollHandle>, entrypoint_name: String, on_panel_toggle_click: impl Fn() -> T, + on_panel_primary_click: impl Fn(UiWidgetId) -> T, on_action_click: impl Fn(UiWidgetId) -> T, ) -> Element<'a, T> { let entrypoint_name: Element<_> = text(entrypoint_name) @@ -1666,7 +1669,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( let panel_height = 16 + 8 + 2; // TODO get value from theme let primary_action = match primary_action { - Some((label, shortcut)) => { + Some((label, widget_id, shortcut)) => { let label: Element<_> = text(label) .themed(TextStyle::RootBottomPanelPrimaryActionText); @@ -1676,7 +1679,14 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( let shortcut = render_shortcut(&shortcut); let content: Element<_> = row(vec![label, shortcut]) - .themed(RowStyle::RootBottomPanelPrimaryAction); + .into(); + + let content: Element<_> = button(content) + .on_press(on_panel_primary_click(widget_id)) + .themed(ButtonStyle::RootBottomPanelPrimaryActionButton); + + let content: Element<_> = container(content) + .themed(ContainerStyle::RootBottomPanelPrimaryActionButton); Some(content) } @@ -1974,6 +1984,9 @@ pub enum ComponentWidgetEvent { widget_id: UiWidgetId, }, PreviousView, + RunPrimaryAction { + widget_id: UiWidgetId, + }, } impl ComponentWidgetEvent { @@ -2082,6 +2095,11 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::PreviousView => { panic!("handle event on PreviousView event is not supposed to be called") } + ComponentWidgetEvent::RunPrimaryAction { widget_id } => { + Some(UiViewEvent::AppEvent { + event: AppMsg::OnAnyActionPluginViewAnyPanel { widget_id } + }) + } } } @@ -2101,6 +2119,7 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::ToggleActionPanel { widget_id } => widget_id, ComponentWidgetEvent::ListItemClick { widget_id, .. } => widget_id, ComponentWidgetEvent::GridItemClick { widget_id, .. } => widget_id, + ComponentWidgetEvent::RunPrimaryAction { widget_id } => widget_id, ComponentWidgetEvent::PreviousView => panic!("widget_id on PreviousView event is not supposed to be called"), }.to_owned() } From f1191057d22eb646c60c3ea4a00614f341662455 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:22:01 +0200 Subject: [PATCH 147/540] Fix emojis not working in multiple places across the application --- rust/client/src/ui/mod.rs | 4 +++ rust/client/src/ui/search_list.rs | 4 ++- rust/client/src/ui/widget.rs | 29 +++++++++++++++++-- rust/management_client/src/ui.rs | 6 ++++ rust/management_client/src/views/general.rs | 3 +- rust/management_client/src/views/plugins.rs | 17 +++++++++-- .../src/views/plugins/preferences.rs | 7 ++++- .../src/views/plugins/table.rs | 7 ++++- 8 files changed, 68 insertions(+), 9 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 45b93ba..96a1af5 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -14,6 +14,7 @@ use iced::keyboard::{Key, Modifiers}; use iced::keyboard::key::Named; use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, Space}; use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; +use iced::widget::text::Shaping; use iced::widget::text_input::focus; use iced::window::{Level, Position, Screenshot}; use iced::window::settings::PlatformSpecific; @@ -1094,6 +1095,7 @@ impl Application for AppModel { return match &self.hud_display { Some(hud_display) => { let hud: Element<_> = text(&hud_display) + .shaping(Shaping::Advanced) .into(); let hud = container(hud) @@ -1155,6 +1157,7 @@ impl Application for AppModel { }; let description: Element<_> = text(description_text) + .shaping(Shaping::Advanced) .into(); let description = container(description) @@ -1250,6 +1253,7 @@ impl Application for AppModel { .themed(ContainerStyle::PluginErrorViewDescription); let error_description: Element<_> = text(display) + .shaping(Shaping::Advanced) .into(); let error_description = container(error_description) diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index 44a685f..c633ed8 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -5,7 +5,7 @@ use iced::widget::button; use iced::widget::component; use iced::widget::row; use iced::widget::text; - +use iced::widget::text::Shaping; use common::model::SearchResult; use crate::ui::scroll_handle::ScrollHandle; use crate::ui::theme::{Element, GauntletTheme, ThemableWidget}; @@ -64,6 +64,7 @@ impl<'a, Message> Component for SearchList<'a, Message> .enumerate() .map(|(index, search_result)| { let main_text: Element<_> = text(&search_result.entrypoint_name) + .shaping(Shaping::Advanced) .into(); let main_text: Element<_> = container(main_text) .themed(ContainerStyle::MainListItemText); @@ -73,6 +74,7 @@ impl<'a, Message> Component for SearchList<'a, Message> .into(); let sub_text: Element<_> = text(&search_result.plugin_name) + .shaping(Shaping::Advanced) .themed(TextStyle::MainListItemSubtext); let sub_text: Element<_> = container(sub_text) .themed(ContainerStyle::MainListItemSubText); // FIXME find a way to set padding based on whether the scroll bar is visible diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index f7fa65d..46c2332 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -13,6 +13,7 @@ use iced::widget::image::Handle; use iced::widget::tooltip::Position; use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, mouse_area, pick_list, row, scrollable, text, text_input, tooltip, vertical_rule, Space}; use iced::{Alignment, Font, Length}; +use iced::widget::text::Shaping; use iced_aw::core::icons; use iced_aw::date_picker::Date; use iced_aw::floating_element::Offset; @@ -322,6 +323,7 @@ impl ComponentWidgetWrapper { link } else { let href: Element<_> = text(href) + .shaping(Shaping::Advanced) .into(); tooltip(link, href, Position::Top) @@ -568,7 +570,10 @@ impl ComponentWidgetWrapper { panic!("unexpected state kind {:?}", state) }; - let button = button(text(state_value.to_string())) + let button_text = text(state_value.to_string()) + .shaping(Shaping::Advanced); + + let button = button(button_text) .on_press(ComponentWidgetEvent::ToggleDatePicker { widget_id }); // TODO unable to customize buttons here, split to separate button styles @@ -667,6 +672,7 @@ impl ComponentWidgetWrapper { } Some(label) => { let label: Element<_> = text(label) + .shaping(Shaping::Advanced) .horizontal_alignment(Horizontal::Right) .width(Length::Fill) .into(); @@ -752,6 +758,7 @@ impl ComponentWidgetWrapper { }; let name: Element<_> = text(format!("{} - {}", plugin_name, entrypoint_name)) + .shaping(Shaping::Advanced) .themed(TextStyle::InlineName); let name: Element<_> = container(name) @@ -813,12 +820,14 @@ impl ComponentWidgetWrapper { }); let title: Element<_> = text(title) + .shaping(Shaping::Advanced) .into(); let subtitle: Element<_> = match description { None => horizontal_space().into(), Some(subtitle) => { text(subtitle) + .shaping(Shaping::Advanced) .themed(TextStyle::EmptyViewSubtitle) } }; @@ -865,6 +874,7 @@ impl ComponentWidgetWrapper { None => content, Some(tooltip_text) => { let tooltip_text: Element<_> = text(tooltip_text) + .shaping(Shaping::Advanced) .into(); tooltip(content, tooltip_text, Position::Top) @@ -890,6 +900,7 @@ impl ComponentWidgetWrapper { }); let text_content: Element<_> = text(text_value) + .shaping(Shaping::Advanced) .themed(TextStyle::TextAccessory); let mut content: Vec> = vec![]; @@ -916,6 +927,7 @@ impl ComponentWidgetWrapper { None => content, Some(tooltip_text) => { let tooltip_text: Element<_> = text(tooltip_text) + .shaping(Shaping::Advanced) .into(); tooltip(content, tooltip_text, Position::Top) @@ -943,6 +955,7 @@ impl ComponentWidgetWrapper { }); let title: Element<_> = text(title) + .shaping(Shaping::Advanced) .into(); let title: Element<_> = container(title) .themed(ContainerStyle::ListItemTitle); @@ -958,6 +971,7 @@ impl ComponentWidgetWrapper { if let Some(subtitle) = subtitle { let subtitle: Element<_> = text(subtitle) + .shaping(Shaping::Advanced) .themed(TextStyle::ListItemSubtitle); let subtitle: Element<_> = container(subtitle) .themed(ContainerStyle::ListItemSubtitle); @@ -1099,6 +1113,7 @@ impl ComponentWidgetWrapper { if let Some(title) = title { // TODO text truncation when iced supports it let title = text(title) + .shaping(Shaping::Advanced) .themed(TextStyle::GridItemTitle); sub_content_left.push(title); @@ -1106,6 +1121,7 @@ impl ComponentWidgetWrapper { if let Some(subtitle) = subtitle { let subtitle = text(subtitle) + .shaping(Shaping::Advanced) .themed(TextStyle::GridItemSubTitle); sub_content_left.push(subtitle); @@ -1244,6 +1260,7 @@ impl Display for SelectItem { fn render_metadata_item<'a>(label: &str, value: Element<'a, ComponentWidgetEvent>) -> Element<'a, ComponentWidgetEvent> { let label: Element<_> = text(label) + .shaping(Shaping::Advanced) .themed(TextStyle::MetadataItemLabel); let label = container(label) @@ -1296,6 +1313,7 @@ fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option< if let Some(title) = title { let title: Element<_> = text(title) + .shaping(Shaping::Advanced) .size(15) .themed(theme_kind_title_text); @@ -1304,6 +1322,7 @@ fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option< if let Some(subtitle) = subtitle { let subtitle: Element<_> = text(subtitle) + .shaping(Shaping::Advanced) .size(15) .themed(theme_kind_subtitle_text); @@ -1529,6 +1548,7 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( if let Some(title) = title { let text: Element<_> = text(title) + .shaping(Shaping::Advanced) .font(Font { weight: Weight::Bold, ..Font::DEFAULT @@ -1578,6 +1598,7 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( let content: Element<_> = if let Some(shortcut_element) = shortcut_element { let text: Element<_> = text(label) + .shaping(Shaping::Advanced) .into(); let space: Element<_> = horizontal_space() @@ -1588,6 +1609,7 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( .into() } else { text(label) + .shaping(Shaping::Advanced) .into() }; @@ -1664,6 +1686,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( on_action_click: impl Fn(UiWidgetId) -> T, ) -> Element<'a, T> { let entrypoint_name: Element<_> = text(entrypoint_name) + .shaping(Shaping::Advanced) .into(); let panel_height = 16 + 8 + 2; // TODO get value from theme @@ -1671,6 +1694,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( let primary_action = match primary_action { Some((label, widget_id, shortcut)) => { let label: Element<_> = text(label) + .shaping(Shaping::Advanced) .themed(TextStyle::RootBottomPanelPrimaryActionText); let label: Element<_> = container(label) @@ -1813,7 +1837,8 @@ fn render_text_part<'a>(value: &str, context: ComponentRenderContext) -> Element ComponentRenderContext::InlineRoot { .. } => panic!("not supposed to be passed to text part") }; - let mut text = text(value); + let mut text = text(value) + .shaping(Shaping::Advanced); if let Some(size) = header { text = text diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index 3baef16..bdc1c37 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::time::Duration; use iced::{Alignment, alignment, Application, color, Command, executor, font, futures, Length, Padding, Settings, Size, Subscription, time, window}; +use iced::advanced::text::Shaping; use iced::advanced::Widget; use iced::widget::{button, column, container, horizontal_rule, horizontal_space, mouse_area, row, scrollable, text}; use iced_aw::{floating_element, Spinner}; @@ -343,6 +344,7 @@ impl Application for ManagementAppModel { .into(); let error_description: Element<_> = text(display) + .shaping(Shaping::Advanced) .into(); let error_description = container(error_description) @@ -612,6 +614,7 @@ impl Application for ManagementAppModel { .into(); let plugin_id: Element<_> = text(plugin_id.to_string()) + .shaping(Shaping::Advanced) .style(TextStyle::Subtitle) .size(14) .into(); @@ -647,6 +650,7 @@ impl Application for ManagementAppModel { .into(); let plugin_id: Element<_> = text(plugin_id.to_string()) + .shaping(Shaping::Advanced) .style(TextStyle::Subtitle) .size(14) .into(); @@ -663,6 +667,7 @@ impl Application for ManagementAppModel { .into(); let message: Element<_> = text(message.to_string()) + .shaping(Shaping::Advanced) .into(); let message: Element<_> = container(message) @@ -688,6 +693,7 @@ impl Application for ManagementAppModel { .into(); let plugin_id: Element<_> = text(plugin_id.to_string()) + .shaping(Shaping::Advanced) .size(14) .style(TextStyle::Subtitle) .into(); diff --git a/rust/management_client/src/views/general.rs b/rust/management_client/src/views/general.rs index cf538d0..a4736f8 100644 --- a/rust/management_client/src/views/general.rs +++ b/rust/management_client/src/views/general.rs @@ -1,7 +1,7 @@ use iced::alignment::Horizontal; use iced::widget::{column, container, row, text, Space}; use iced::{Alignment, Command, Length}; - +use iced::widget::text::Shaping; use common::model::{PhysicalKey, PhysicalShortcut}; use common::rpc::backend_api::{BackendApi, BackendApiError}; @@ -109,6 +109,7 @@ impl ManagementAppGeneralState { fn view_field<'a>(&self, label: &str, input: Element<'a, ManagementAppGeneralMsgIn>) -> Element<'a, ManagementAppGeneralMsgIn> { let label: Element<_> = text(label) + .shaping(Shaping::Advanced) .horizontal_alignment(Horizontal::Right) .width(Length::Fill) .into(); diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index 2fa5321..8270d1e 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use iced::{Alignment, Command, Length, Padding}; use iced::widget::{button, column, container, row, scrollable, text, text_input, vertical_rule}; +use iced::widget::text::Shaping; use iced_aw::core::icons; use common::{settings_env_data_from_string, SettingsEnvData}; @@ -309,11 +310,15 @@ impl ManagementAppPluginsState { .into() } Some(plugin) => { - let name = container(text(&plugin.plugin_name)) + let name = text(&plugin.plugin_name) + .shaping(Shaping::Advanced); + + let name = container(name) .padding(Padding::new(8.0)) .into(); let id: Element<_> = text(&plugin.plugin_id.to_string()) + .shaping(Shaping::Advanced) .style(TextStyle::Subtitle) .into(); @@ -336,7 +341,10 @@ impl ManagementAppPluginsState { .padding(Padding::from([0.0, 0.0, 0.0, 8.0])) .into(); - let description = container(text(&plugin.plugin_description)) + let description = text(&plugin.plugin_description) + .shaping(Shaping::Advanced); + + let description = container(description) .padding(Padding::new(8.0)) .into(); @@ -429,7 +437,10 @@ impl ManagementAppPluginsState { .into() } Some(entrypoint) => { - let name = container(text(&entrypoint.entrypoint_name)) + let name = text(&entrypoint.entrypoint_name) + .shaping(Shaping::Advanced); + + let name = container(name) .padding(Padding::new(8.0)) .into(); diff --git a/rust/management_client/src/views/plugins/preferences.rs b/rust/management_client/src/views/plugins/preferences.rs index e01813f..f2208c9 100644 --- a/rust/management_client/src/views/plugins/preferences.rs +++ b/rust/management_client/src/views/plugins/preferences.rs @@ -10,6 +10,7 @@ use iced_aw::core::icons; use iced_aw::number_input; use std::collections::HashMap; use std::fmt::Display; +use iced::widget::text::Shaping; #[derive(Debug, Clone)] pub enum PluginPreferencesMsg { @@ -68,6 +69,7 @@ pub fn preferences_ui<'a>( let description = description.to_owned(); let preference_label: Element<_> = text(&preference_name) + .shaping(Shaping::Advanced) .size(14) .style(TextStyle::Subtitle) .into(); @@ -81,7 +83,10 @@ pub fn preferences_ui<'a>( input_field_column.push(preference_label); if !description.trim().is_empty() { - let description = container(text(description)) + let description = text(description) + .shaping(Shaping::Advanced); + + let description = container(description) .padding(Padding::from([4.0, 8.0])) .into(); diff --git a/rust/management_client/src/views/plugins/table.rs b/rust/management_client/src/views/plugins/table.rs index fb6f156..5e14649 100644 --- a/rust/management_client/src/views/plugins/table.rs +++ b/rust/management_client/src/views/plugins/table.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use std::rc::Rc; use iced::{Command, Length, Renderer}; +use iced::advanced::text::Shaping; use iced::widget::{button, checkbox, container, horizontal_space, row, scrollable, Space, text}; use iced_aw::core::icons; use iced_table::table; @@ -258,7 +259,10 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); - container(text(&plugin.plugin_name)) + let plugin_name = text(&plugin.plugin_name) + .shaping(Shaping::Advanced); + + container(plugin_name) .center_y() .into() } @@ -268,6 +272,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo let entrypoint = plugin.entrypoints.get(&entrypoint_id).unwrap(); let text: Element<_> = text(&entrypoint.entrypoint_name) + .shaping(Shaping::Advanced) .into(); let text: Element<_> = row(vec![ From f594b6310e7a3cde08df4a082e9bafc61c866d96 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:01:26 +0200 Subject: [PATCH 148/540] Fix broken scenarios --- rust/client/src/ui/mod.rs | 7 +- rust/common/src/rpc/backend_api.rs | 2 +- rust/scenario_runner/src/frontend_mock.rs | 3 +- .../plugins/docs_detail/src/content_image.tsx | 6 -- scenarios/plugins/docs_grid/src/main.tsx | 60 ++++++++++------- scenarios/plugins/docs_grid/src/section.tsx | 66 ++++++++++++------- scenarios/plugins/docs_inline/gauntlet.toml | 3 + scenarios/plugins/docs_list/src/detail.tsx | 20 +++--- scenarios/plugins/docs_list/src/main.tsx | 20 +++--- scenarios/plugins/docs_list/src/section.tsx | 16 ++--- 10 files changed, 117 insertions(+), 86 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 96a1af5..1d9cd90 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -148,7 +148,6 @@ pub enum AppMsg { screenshot: Screenshot }, Close, - ResetWindowState, ShowBackendError(BackendForFrontendApiError), ClosePluginView(PluginId), OpenPluginView(PluginId, EntrypointId), @@ -285,11 +284,14 @@ impl Application for AppModel { let backend_api = BackendForFrontendApi::new(backend_sender); let mut commands = vec![ - Command::perform(async {}, |_| AppMsg::ResetWindowState), font::load(icons::BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded), ]; if !wayland { + commands.push( + window::gain_focus(window::Id::MAIN), + ); + commands.push( window::change_level(window::Id::MAIN, Level::AlwaysOnTop), ) @@ -714,7 +716,6 @@ impl Application for AppModel { result.expect("unable to load font"); Command::none() } - AppMsg::ResetWindowState => self.reset_window_state(), AppMsg::ShowWindow => self.show_window(), AppMsg::HideWindow => self.hide_window(), AppMsg::ShowPreferenceRequiredView { diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index d81ed0d..a834feb 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -203,7 +203,7 @@ impl BackendForFrontendApi { pub enum BackendApiError { #[error("Timeout Error")] Timeout, - #[error("Internal Backend Error")] + #[error("Internal Backend Error: {display:?}")] Internal { display: String }, diff --git a/rust/scenario_runner/src/frontend_mock.rs b/rust/scenario_runner/src/frontend_mock.rs index 228f6a6..585edcb 100644 --- a/rust/scenario_runner/src/frontend_mock.rs +++ b/rust/scenario_runner/src/frontend_mock.rs @@ -25,7 +25,6 @@ pub async fn start_scenario_runner_frontend( let scenario_plugin_dir = scenario_dir .join("plugins") .join(&plugin_name) - .join("dist") .to_str() .expect("scenario_plugin_dir is invalid UTF-8") .to_string(); @@ -152,7 +151,7 @@ async fn request_loop(mut request_receiver: RequestReceiver { + UiRequestData::ShowHud { .. } | UiRequestData::ShowWindow | UiRequestData::ClearInlineView { .. } => { unreachable!() } UiRequestData::RequestSearchResultUpdate => { diff --git a/scenarios/plugins/docs_detail/src/content_image.tsx b/scenarios/plugins/docs_detail/src/content_image.tsx index 7bf2bf9..2109e1c 100644 --- a/scenarios/plugins/docs_detail/src/content_image.tsx +++ b/scenarios/plugins/docs_detail/src/content_image.tsx @@ -1,12 +1,6 @@ import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; -async function readFile(url: string): Promise { - const res = await fetch(url); - const blob = await res.blob(); - return await blob.arrayBuffer() -} - const imgUrl = "https://static.wikia.nocookie.net/starwars/images/a/ae/The_Whills_Strike_Back.png/revision/latest/scale-to-width-down/400?cb=20201006180053" export default function Main(): ReactNode { diff --git a/scenarios/plugins/docs_grid/src/main.tsx b/scenarios/plugins/docs_grid/src/main.tsx index e315e25..96b59bb 100644 --- a/scenarios/plugins/docs_grid/src/main.tsx +++ b/scenarios/plugins/docs_grid/src/main.tsx @@ -1,12 +1,6 @@ import { ReactElement } from "react"; import { Grid } from "@project-gauntlet/api/components"; -async function readFile(url: string): Promise { - const res = await fetch(url); - const blob = await res.blob(); - return await blob.arrayBuffer() -} - const nabooImage = "https://static.wikia.nocookie.net/star-wars-canon/images/2/24/NabooFull-SW.png/revision/latest/scale-to-width-down/150?cb=20151218205422" const rylothImage = "https://static.wikia.nocookie.net/star-wars-canon/images/4/48/Dagobah_ep3.jpg/revision/latest/scale-to-width-down/150?cb=20161103221846" const tatooineImage = "https://static.wikia.nocookie.net/star-wars-canon/images/b/b7/Ryloth_Rebels.png/revision/latest/scale-to-width-down/150?cb=20161103040944" @@ -20,32 +14,50 @@ const dantooineImage = "https://static.wikia.nocookie.net/starwars/images/a/a5/D export default function Main(): ReactElement { return ( - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + ) diff --git a/scenarios/plugins/docs_grid/src/section.tsx b/scenarios/plugins/docs_grid/src/section.tsx index 06e1cfa..55e79ab 100644 --- a/scenarios/plugins/docs_grid/src/section.tsx +++ b/scenarios/plugins/docs_grid/src/section.tsx @@ -18,40 +18,62 @@ export default function Main(): ReactElement { return ( - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + diff --git a/scenarios/plugins/docs_inline/gauntlet.toml b/scenarios/plugins/docs_inline/gauntlet.toml index 0af8f62..6a39e9e 100644 --- a/scenarios/plugins/docs_inline/gauntlet.toml +++ b/scenarios/plugins/docs_inline/gauntlet.toml @@ -9,3 +9,6 @@ path = 'src/main.tsx' type = 'inline-view' description = '' +[permissions] +main_search_bar = ["read"] + diff --git a/scenarios/plugins/docs_list/src/detail.tsx b/scenarios/plugins/docs_list/src/detail.tsx index 72d6a6b..d5d5854 100644 --- a/scenarios/plugins/docs_list/src/detail.tsx +++ b/scenarios/plugins/docs_list/src/detail.tsx @@ -4,16 +4,16 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - - - - - - - - - - + + + + + + + + + + Sentient diff --git a/scenarios/plugins/docs_list/src/main.tsx b/scenarios/plugins/docs_list/src/main.tsx index 6458e49..6bdafeb 100644 --- a/scenarios/plugins/docs_list/src/main.tsx +++ b/scenarios/plugins/docs_list/src/main.tsx @@ -4,16 +4,16 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - - - - - - - - - - + + + + + + + + + + ) } diff --git a/scenarios/plugins/docs_list/src/section.tsx b/scenarios/plugins/docs_list/src/section.tsx index 9f23846..3d9db2b 100644 --- a/scenarios/plugins/docs_list/src/section.tsx +++ b/scenarios/plugins/docs_list/src/section.tsx @@ -4,17 +4,17 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - + - - - + + + - - - - + + + + ) From 8605293c8153c7057f61c31378734e7ff895a333 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 27 Oct 2024 10:28:11 +0100 Subject: [PATCH 149/540] Add docs code segment markers to scenarios --- scenarios/plugins/docs_detail/gauntlet.toml | 26 +++++++++++++++++ scenarios/plugins/docs_detail/src/content.tsx | 2 ++ .../docs_detail/src/content_code_block.tsx | 2 ++ .../docs_detail/src/content_header.tsx | 2 ++ .../src/content_horizontal_break.tsx | 2 ++ .../plugins/docs_detail/src/content_image.tsx | 2 ++ .../docs_detail/src/content_paragraph.tsx | 2 ++ scenarios/plugins/docs_detail/src/main.tsx | 2 ++ .../plugins/docs_detail/src/metadata.tsx | 2 ++ .../plugins/docs_detail/src/metadata_icon.tsx | 2 ++ .../plugins/docs_detail/src/metadata_link.tsx | 2 ++ .../docs_detail/src/metadata_separator.tsx | 2 ++ .../docs_detail/src/metadata_tag_list.tsx | 2 ++ .../docs_detail/src/metadata_value.tsx | 2 ++ scenarios/plugins/docs_form/gauntlet.toml | 28 +++++++++---------- scenarios/plugins/docs_form/src/checkbox.tsx | 2 ++ .../plugins/docs_form/src/date-picker.tsx | 2 ++ scenarios/plugins/docs_form/src/main.tsx | 2 ++ .../plugins/docs_form/src/password-field.tsx | 2 ++ scenarios/plugins/docs_form/src/select.tsx | 2 ++ scenarios/plugins/docs_form/src/separator.tsx | 2 ++ .../plugins/docs_form/src/text-field.tsx | 2 ++ scenarios/plugins/docs_grid/gauntlet.toml | 6 ++++ .../plugins/docs_grid/src/empty_view.tsx | 2 ++ scenarios/plugins/docs_grid/src/main.tsx | 2 ++ scenarios/plugins/docs_grid/src/section.tsx | 2 ++ scenarios/plugins/docs_inline/gauntlet.toml | 2 ++ scenarios/plugins/docs_inline/src/main.tsx | 6 ++++ scenarios/plugins/docs_list/gauntlet.toml | 8 ++++++ scenarios/plugins/docs_list/src/detail.tsx | 2 ++ .../plugins/docs_list/src/empty_view.tsx | 2 ++ scenarios/plugins/docs_list/src/main.tsx | 2 ++ scenarios/plugins/docs_list/src/section.tsx | 2 ++ 33 files changed, 116 insertions(+), 14 deletions(-) diff --git a/scenarios/plugins/docs_detail/gauntlet.toml b/scenarios/plugins/docs_detail/gauntlet.toml index a34fdfe..f950b31 100644 --- a/scenarios/plugins/docs_detail/gauntlet.toml +++ b/scenarios/plugins/docs_detail/gauntlet.toml @@ -2,96 +2,122 @@ name = 'Docs Detail' description = '' +# docs-code-segment:start content [[entrypoint]] id = 'content' name = 'Content' path = 'src/content.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start content-code-block [[entrypoint]] id = 'content-code-block' name = 'Content Code Block' path = 'src/content_code_block.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start content-header [[entrypoint]] id = 'content-header' name = 'Content Header' path = 'src/content_header.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start content-horizontal-break [[entrypoint]] id = 'content-horizontal-break' name = 'Content Horizontal Break' path = 'src/content_horizontal_break.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start content-image [[entrypoint]] id = 'content-image' name = 'Content Image' path = 'src/content_image.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start content-paragraph [[entrypoint]] id = 'content-paragraph' name = 'Content Paragraph' path = 'src/content_paragraph.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start main [[entrypoint]] id = 'main' name = 'Main' path = 'src/main.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start metadata [[entrypoint]] id = 'metadata' name = 'Metadata' path = 'src/metadata.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start metadata-icon [[entrypoint]] id = 'metadata-icon' name = 'Metadata Icon' path = 'src/metadata_icon.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start metadata-link [[entrypoint]] id = 'metadata-link' name = 'Metadata Link' path = 'src/metadata_link.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start metadata-separator [[entrypoint]] id = 'metadata-separator' name = 'Metadata Separator' path = 'src/metadata_separator.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start metadata-tag-list [[entrypoint]] id = 'metadata-tag-list' name = 'Metadata Tag List' path = 'src/metadata_tag_list.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start metadata-value [[entrypoint]] id = 'metadata-value' name = 'Metadata Value' path = 'src/metadata_value.tsx' type = 'view' description = '' +# docs-code-segment:end [permissions] network = ["static.wikia.nocookie.net"] diff --git a/scenarios/plugins/docs_detail/src/content.tsx b/scenarios/plugins/docs_detail/src/content.tsx index 3229f11..d21df5a 100644 --- a/scenarios/plugins/docs_detail/src/content.tsx +++ b/scenarios/plugins/docs_detail/src/content.tsx @@ -3,6 +3,7 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( + // docs-code-segment:start @@ -21,5 +22,6 @@ export default function Main(): ReactNode { + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/content_code_block.tsx b/scenarios/plugins/docs_detail/src/content_code_block.tsx index 791e071..40a50da 100644 --- a/scenarios/plugins/docs_detail/src/content_code_block.tsx +++ b/scenarios/plugins/docs_detail/src/content_code_block.tsx @@ -11,6 +11,7 @@ const code = ` export default function Main(): ReactNode { return ( + // docs-code-segment:start @@ -18,5 +19,6 @@ export default function Main(): ReactNode { + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/content_header.tsx b/scenarios/plugins/docs_detail/src/content_header.tsx index 299defd..5605c12 100644 --- a/scenarios/plugins/docs_detail/src/content_header.tsx +++ b/scenarios/plugins/docs_detail/src/content_header.tsx @@ -3,6 +3,7 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( + // docs-code-segment:start @@ -25,5 +26,6 @@ export default function Main(): ReactNode { + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/content_horizontal_break.tsx b/scenarios/plugins/docs_detail/src/content_horizontal_break.tsx index b5f0a3d..a127c08 100644 --- a/scenarios/plugins/docs_detail/src/content_horizontal_break.tsx +++ b/scenarios/plugins/docs_detail/src/content_horizontal_break.tsx @@ -3,6 +3,7 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( + // docs-code-segment:start @@ -14,5 +15,6 @@ export default function Main(): ReactNode { + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/content_image.tsx b/scenarios/plugins/docs_detail/src/content_image.tsx index 2109e1c..be0a100 100644 --- a/scenarios/plugins/docs_detail/src/content_image.tsx +++ b/scenarios/plugins/docs_detail/src/content_image.tsx @@ -5,10 +5,12 @@ const imgUrl = "https://static.wikia.nocookie.net/starwars/images/a/ae/The_Whill export default function Main(): ReactNode { return ( + // docs-code-segment:start + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/content_paragraph.tsx b/scenarios/plugins/docs_detail/src/content_paragraph.tsx index e7bfdf4..42c70da 100644 --- a/scenarios/plugins/docs_detail/src/content_paragraph.tsx +++ b/scenarios/plugins/docs_detail/src/content_paragraph.tsx @@ -3,6 +3,7 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( + // docs-code-segment:start @@ -12,5 +13,6 @@ export default function Main(): ReactNode { + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/main.tsx b/scenarios/plugins/docs_detail/src/main.tsx index 8073b8f..4c2fe95 100644 --- a/scenarios/plugins/docs_detail/src/main.tsx +++ b/scenarios/plugins/docs_detail/src/main.tsx @@ -3,6 +3,7 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( + // docs-code-segment:start Sentient @@ -34,5 +35,6 @@ export default function Main(): ReactNode { + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/metadata.tsx b/scenarios/plugins/docs_detail/src/metadata.tsx index 5ea91c9..96b5e4c 100644 --- a/scenarios/plugins/docs_detail/src/metadata.tsx +++ b/scenarios/plugins/docs_detail/src/metadata.tsx @@ -3,6 +3,7 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( + // docs-code-segment:start Ezaraa @@ -20,5 +21,6 @@ export default function Main(): ReactNode { + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/metadata_icon.tsx b/scenarios/plugins/docs_detail/src/metadata_icon.tsx index 48b8b41..a523529 100644 --- a/scenarios/plugins/docs_detail/src/metadata_icon.tsx +++ b/scenarios/plugins/docs_detail/src/metadata_icon.tsx @@ -3,10 +3,12 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( + // docs-code-segment:start + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/metadata_link.tsx b/scenarios/plugins/docs_detail/src/metadata_link.tsx index bbaef8c..00246ff 100644 --- a/scenarios/plugins/docs_detail/src/metadata_link.tsx +++ b/scenarios/plugins/docs_detail/src/metadata_link.tsx @@ -3,10 +3,12 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( + // docs-code-segment:start Link + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/metadata_separator.tsx b/scenarios/plugins/docs_detail/src/metadata_separator.tsx index 1b0921f..a0abcb7 100644 --- a/scenarios/plugins/docs_detail/src/metadata_separator.tsx +++ b/scenarios/plugins/docs_detail/src/metadata_separator.tsx @@ -3,6 +3,7 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( + // docs-code-segment:start Ezaraa @@ -10,5 +11,6 @@ export default function Main(): ReactNode { Sentient + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/metadata_tag_list.tsx b/scenarios/plugins/docs_detail/src/metadata_tag_list.tsx index 858a784..233d892 100644 --- a/scenarios/plugins/docs_detail/src/metadata_tag_list.tsx +++ b/scenarios/plugins/docs_detail/src/metadata_tag_list.tsx @@ -3,6 +3,7 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( + // docs-code-segment:start @@ -14,5 +15,6 @@ export default function Main(): ReactNode { + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/metadata_value.tsx b/scenarios/plugins/docs_detail/src/metadata_value.tsx index 0f4c7b4..7c66025 100644 --- a/scenarios/plugins/docs_detail/src/metadata_value.tsx +++ b/scenarios/plugins/docs_detail/src/metadata_value.tsx @@ -3,10 +3,12 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( + // docs-code-segment:start David Tennant + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_form/gauntlet.toml b/scenarios/plugins/docs_form/gauntlet.toml index 57aa6ea..f2bed62 100644 --- a/scenarios/plugins/docs_form/gauntlet.toml +++ b/scenarios/plugins/docs_form/gauntlet.toml @@ -3,66 +3,66 @@ name = 'Docs Form' description = '' +# docs-code-segment:start checkbox [[entrypoint]] id = 'checkbox' name = 'Checkbox' path = 'src/checkbox.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start date-picker [[entrypoint]] id = 'date-picker' name = 'Date Picker' path = 'src/date-picker.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start main [[entrypoint]] id = 'main' name = 'Main' path = 'src/main.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start password-field [[entrypoint]] id = 'password-field' name = 'Password Field' path = 'src/password-field.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start select [[entrypoint]] id = 'select' name = 'Select' path = 'src/select.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start separator [[entrypoint]] id = 'separator' name = 'Separator' path = 'src/separator.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start text-field [[entrypoint]] id = 'text-field' name = 'Text Field' path = 'src/text-field.tsx' type = 'view' description = '' - - - - - - - - - - - - - - +# docs-code-segment:end diff --git a/scenarios/plugins/docs_form/src/checkbox.tsx b/scenarios/plugins/docs_form/src/checkbox.tsx index ddb21cc..fc5c1ab 100644 --- a/scenarios/plugins/docs_form/src/checkbox.tsx +++ b/scenarios/plugins/docs_form/src/checkbox.tsx @@ -3,6 +3,7 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( + // docs-code-segment:start + // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_form/src/date-picker.tsx b/scenarios/plugins/docs_form/src/date-picker.tsx index 7707525..ac388c9 100644 --- a/scenarios/plugins/docs_form/src/date-picker.tsx +++ b/scenarios/plugins/docs_form/src/date-picker.tsx @@ -3,6 +3,7 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( + // docs-code-segment:start
+ // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_form/src/main.tsx b/scenarios/plugins/docs_form/src/main.tsx index 42fccd1..66d1fe0 100644 --- a/scenarios/plugins/docs_form/src/main.tsx +++ b/scenarios/plugins/docs_form/src/main.tsx @@ -3,6 +3,7 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( + // docs-code-segment:start
+ // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_form/src/password-field.tsx b/scenarios/plugins/docs_form/src/password-field.tsx index f77cfe8..05e8b2e 100644 --- a/scenarios/plugins/docs_form/src/password-field.tsx +++ b/scenarios/plugins/docs_form/src/password-field.tsx @@ -3,9 +3,11 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( + // docs-code-segment:start
+ // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_form/src/select.tsx b/scenarios/plugins/docs_form/src/select.tsx index ef6d34f..033839c 100644 --- a/scenarios/plugins/docs_form/src/select.tsx +++ b/scenarios/plugins/docs_form/src/select.tsx @@ -3,6 +3,7 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( + // docs-code-segment:start
Burger @@ -13,6 +14,7 @@ export default function Main(): ReactElement { Seafood
+ // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_form/src/separator.tsx b/scenarios/plugins/docs_form/src/separator.tsx index 5c2b458..3f73598 100644 --- a/scenarios/plugins/docs_form/src/separator.tsx +++ b/scenarios/plugins/docs_form/src/separator.tsx @@ -3,6 +3,7 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( + // docs-code-segment:start
+ // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_form/src/text-field.tsx b/scenarios/plugins/docs_form/src/text-field.tsx index d30a05b..53553c2 100644 --- a/scenarios/plugins/docs_form/src/text-field.tsx +++ b/scenarios/plugins/docs_form/src/text-field.tsx @@ -3,6 +3,7 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( + // docs-code-segment:start
+ // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_grid/gauntlet.toml b/scenarios/plugins/docs_grid/gauntlet.toml index 0042963..ad0833c 100644 --- a/scenarios/plugins/docs_grid/gauntlet.toml +++ b/scenarios/plugins/docs_grid/gauntlet.toml @@ -3,26 +3,32 @@ name = 'Docs Grid' description = '' +# docs-code-segment:start empty-view [[entrypoint]] id = 'empty-view' name = 'Empty View' path = 'src/empty_view.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start main [[entrypoint]] id = 'main' name = 'Main' path = 'src/main.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start section [[entrypoint]] id = 'section' name = 'Section' path = 'src/section.tsx' type = 'view' description = '' +# docs-code-segment:end [permissions] diff --git a/scenarios/plugins/docs_grid/src/empty_view.tsx b/scenarios/plugins/docs_grid/src/empty_view.tsx index eec1cc6..ad04a63 100644 --- a/scenarios/plugins/docs_grid/src/empty_view.tsx +++ b/scenarios/plugins/docs_grid/src/empty_view.tsx @@ -5,8 +5,10 @@ const alderaanImage = "https://static.wikia.nocookie.net/starwars/images/4/4a/Al export default function Main(): ReactElement { return ( + // docs-code-segment:start + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_grid/src/main.tsx b/scenarios/plugins/docs_grid/src/main.tsx index 96b59bb..6317c17 100644 --- a/scenarios/plugins/docs_grid/src/main.tsx +++ b/scenarios/plugins/docs_grid/src/main.tsx @@ -13,6 +13,7 @@ const dantooineImage = "https://static.wikia.nocookie.net/starwars/images/a/a5/D export default function Main(): ReactElement { return ( + // docs-code-segment:start @@ -60,5 +61,6 @@ export default function Main(): ReactElement { + // docs-code-segment:end ) } \ No newline at end of file diff --git a/scenarios/plugins/docs_grid/src/section.tsx b/scenarios/plugins/docs_grid/src/section.tsx index 55e79ab..a562bba 100644 --- a/scenarios/plugins/docs_grid/src/section.tsx +++ b/scenarios/plugins/docs_grid/src/section.tsx @@ -16,6 +16,7 @@ const vader7 = "https://static.wikia.nocookie.net/starwars/images/f/fa/DarthVade export default function Main(): ReactElement { return ( + // docs-code-segment:start @@ -77,5 +78,6 @@ export default function Main(): ReactElement { + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_inline/gauntlet.toml b/scenarios/plugins/docs_inline/gauntlet.toml index 6a39e9e..2521a6c 100644 --- a/scenarios/plugins/docs_inline/gauntlet.toml +++ b/scenarios/plugins/docs_inline/gauntlet.toml @@ -2,12 +2,14 @@ name = 'Docs Inline' description = '' +# docs-code-segment:start main [[entrypoint]] id = 'main' name = 'Main' path = 'src/main.tsx' type = 'inline-view' description = '' +# docs-code-segment:end [permissions] main_search_bar = ["read"] diff --git a/scenarios/plugins/docs_inline/src/main.tsx b/scenarios/plugins/docs_inline/src/main.tsx index 069c110..57ed895 100644 --- a/scenarios/plugins/docs_inline/src/main.tsx +++ b/scenarios/plugins/docs_inline/src/main.tsx @@ -21,6 +21,7 @@ export default function Main(props: { text: string }): ReactElement { function Separator() { return ( + // docs-code-segment:start separator @@ -34,11 +35,13 @@ function Separator() { + // docs-code-segment:end ) } function ThreeSection() { return ( + // docs-code-segment:start three-section @@ -56,11 +59,13 @@ function ThreeSection() { + // docs-code-segment:end ) } function TwoSection() { return ( + // docs-code-segment:start two-section @@ -73,5 +78,6 @@ function TwoSection() { + // docs-code-segment:end ) } \ No newline at end of file diff --git a/scenarios/plugins/docs_list/gauntlet.toml b/scenarios/plugins/docs_list/gauntlet.toml index 1bb6264..b50cffd 100644 --- a/scenarios/plugins/docs_list/gauntlet.toml +++ b/scenarios/plugins/docs_list/gauntlet.toml @@ -3,33 +3,41 @@ name = 'Docs List' description = '' +# docs-code-segment:start detail [[entrypoint]] id = 'detail' name = 'Detail' path = 'src/detail.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start empty-view [[entrypoint]] id = 'empty-view' name = 'Empty View' path = 'src/empty_view.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start main [[entrypoint]] id = 'main' name = 'Main' path = 'src/main.tsx' type = 'view' description = '' +# docs-code-segment:end +# docs-code-segment:start section [[entrypoint]] id = 'section' name = 'Section' path = 'src/section.tsx' type = 'view' description = '' +# docs-code-segment:end [permissions] network = ["static.wikia.nocookie.net"] diff --git a/scenarios/plugins/docs_list/src/detail.tsx b/scenarios/plugins/docs_list/src/detail.tsx index d5d5854..40d8b77 100644 --- a/scenarios/plugins/docs_list/src/detail.tsx +++ b/scenarios/plugins/docs_list/src/detail.tsx @@ -3,6 +3,7 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( + // docs-code-segment:start @@ -50,5 +51,6 @@ export default function Main(): ReactElement {
+ // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_list/src/empty_view.tsx b/scenarios/plugins/docs_list/src/empty_view.tsx index eec1cc6..ad04a63 100644 --- a/scenarios/plugins/docs_list/src/empty_view.tsx +++ b/scenarios/plugins/docs_list/src/empty_view.tsx @@ -5,8 +5,10 @@ const alderaanImage = "https://static.wikia.nocookie.net/starwars/images/4/4a/Al export default function Main(): ReactElement { return ( + // docs-code-segment:start + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_list/src/main.tsx b/scenarios/plugins/docs_list/src/main.tsx index 6bdafeb..ff80162 100644 --- a/scenarios/plugins/docs_list/src/main.tsx +++ b/scenarios/plugins/docs_list/src/main.tsx @@ -3,6 +3,7 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( + // docs-code-segment:start @@ -15,5 +16,6 @@ export default function Main(): ReactElement { + // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_list/src/section.tsx b/scenarios/plugins/docs_list/src/section.tsx index 3d9db2b..e2829f1 100644 --- a/scenarios/plugins/docs_list/src/section.tsx +++ b/scenarios/plugins/docs_list/src/section.tsx @@ -3,6 +3,7 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( + // docs-code-segment:start @@ -17,5 +18,6 @@ export default function Main(): ReactElement { + // docs-code-segment:end ) } From 90a1493256a3656d20f2bc8a7c72124b3ae36d7f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 27 Oct 2024 10:28:35 +0100 Subject: [PATCH 150/540] Rename default scenario input files --- .../data/docs_detail/content-code-block/{0.json => default.json} | 0 .../data/docs_detail/content-header/{0.json => default.json} | 0 .../docs_detail/content-horizontal-break/{0.json => default.json} | 0 scenarios/data/docs_detail/content-image/{0.json => default.json} | 0 .../data/docs_detail/content-paragraph/{0.json => default.json} | 0 scenarios/data/docs_detail/content/{0.json => default.json} | 0 scenarios/data/docs_detail/main/{0.json => default.json} | 0 scenarios/data/docs_detail/metadata-icon/{0.json => default.json} | 0 scenarios/data/docs_detail/metadata-link/{0.json => default.json} | 0 .../data/docs_detail/metadata-separator/{0.json => default.json} | 0 .../data/docs_detail/metadata-tag-list/{0.json => default.json} | 0 .../data/docs_detail/metadata-value/{0.json => default.json} | 0 scenarios/data/docs_detail/metadata/{0.json => default.json} | 0 scenarios/data/docs_form/checkbox/{0.json => default.json} | 0 scenarios/data/docs_form/date-picker/{0.json => default.json} | 0 scenarios/data/docs_form/main/{0.json => default.json} | 0 scenarios/data/docs_form/password-field/{0.json => default.json} | 0 scenarios/data/docs_form/select/{0.json => default.json} | 0 scenarios/data/docs_form/separator/{0.json => default.json} | 0 scenarios/data/docs_form/text-field/{0.json => default.json} | 0 scenarios/data/docs_grid/empty-view/{0.json => default.json} | 0 scenarios/data/docs_grid/main/{0.json => default.json} | 0 scenarios/data/docs_grid/section/{0.json => default.json} | 0 scenarios/data/docs_list/detail/{0.json => default.json} | 0 scenarios/data/docs_list/empty-view/{0.json => default.json} | 0 scenarios/data/docs_list/main/{0.json => default.json} | 0 scenarios/data/docs_list/section/{0.json => default.json} | 0 27 files changed, 0 insertions(+), 0 deletions(-) rename scenarios/data/docs_detail/content-code-block/{0.json => default.json} (100%) rename scenarios/data/docs_detail/content-header/{0.json => default.json} (100%) rename scenarios/data/docs_detail/content-horizontal-break/{0.json => default.json} (100%) rename scenarios/data/docs_detail/content-image/{0.json => default.json} (100%) rename scenarios/data/docs_detail/content-paragraph/{0.json => default.json} (100%) rename scenarios/data/docs_detail/content/{0.json => default.json} (100%) rename scenarios/data/docs_detail/main/{0.json => default.json} (100%) rename scenarios/data/docs_detail/metadata-icon/{0.json => default.json} (100%) rename scenarios/data/docs_detail/metadata-link/{0.json => default.json} (100%) rename scenarios/data/docs_detail/metadata-separator/{0.json => default.json} (100%) rename scenarios/data/docs_detail/metadata-tag-list/{0.json => default.json} (100%) rename scenarios/data/docs_detail/metadata-value/{0.json => default.json} (100%) rename scenarios/data/docs_detail/metadata/{0.json => default.json} (100%) rename scenarios/data/docs_form/checkbox/{0.json => default.json} (100%) rename scenarios/data/docs_form/date-picker/{0.json => default.json} (100%) rename scenarios/data/docs_form/main/{0.json => default.json} (100%) rename scenarios/data/docs_form/password-field/{0.json => default.json} (100%) rename scenarios/data/docs_form/select/{0.json => default.json} (100%) rename scenarios/data/docs_form/separator/{0.json => default.json} (100%) rename scenarios/data/docs_form/text-field/{0.json => default.json} (100%) rename scenarios/data/docs_grid/empty-view/{0.json => default.json} (100%) rename scenarios/data/docs_grid/main/{0.json => default.json} (100%) rename scenarios/data/docs_grid/section/{0.json => default.json} (100%) rename scenarios/data/docs_list/detail/{0.json => default.json} (100%) rename scenarios/data/docs_list/empty-view/{0.json => default.json} (100%) rename scenarios/data/docs_list/main/{0.json => default.json} (100%) rename scenarios/data/docs_list/section/{0.json => default.json} (100%) diff --git a/scenarios/data/docs_detail/content-code-block/0.json b/scenarios/data/docs_detail/content-code-block/default.json similarity index 100% rename from scenarios/data/docs_detail/content-code-block/0.json rename to scenarios/data/docs_detail/content-code-block/default.json diff --git a/scenarios/data/docs_detail/content-header/0.json b/scenarios/data/docs_detail/content-header/default.json similarity index 100% rename from scenarios/data/docs_detail/content-header/0.json rename to scenarios/data/docs_detail/content-header/default.json diff --git a/scenarios/data/docs_detail/content-horizontal-break/0.json b/scenarios/data/docs_detail/content-horizontal-break/default.json similarity index 100% rename from scenarios/data/docs_detail/content-horizontal-break/0.json rename to scenarios/data/docs_detail/content-horizontal-break/default.json diff --git a/scenarios/data/docs_detail/content-image/0.json b/scenarios/data/docs_detail/content-image/default.json similarity index 100% rename from scenarios/data/docs_detail/content-image/0.json rename to scenarios/data/docs_detail/content-image/default.json diff --git a/scenarios/data/docs_detail/content-paragraph/0.json b/scenarios/data/docs_detail/content-paragraph/default.json similarity index 100% rename from scenarios/data/docs_detail/content-paragraph/0.json rename to scenarios/data/docs_detail/content-paragraph/default.json diff --git a/scenarios/data/docs_detail/content/0.json b/scenarios/data/docs_detail/content/default.json similarity index 100% rename from scenarios/data/docs_detail/content/0.json rename to scenarios/data/docs_detail/content/default.json diff --git a/scenarios/data/docs_detail/main/0.json b/scenarios/data/docs_detail/main/default.json similarity index 100% rename from scenarios/data/docs_detail/main/0.json rename to scenarios/data/docs_detail/main/default.json diff --git a/scenarios/data/docs_detail/metadata-icon/0.json b/scenarios/data/docs_detail/metadata-icon/default.json similarity index 100% rename from scenarios/data/docs_detail/metadata-icon/0.json rename to scenarios/data/docs_detail/metadata-icon/default.json diff --git a/scenarios/data/docs_detail/metadata-link/0.json b/scenarios/data/docs_detail/metadata-link/default.json similarity index 100% rename from scenarios/data/docs_detail/metadata-link/0.json rename to scenarios/data/docs_detail/metadata-link/default.json diff --git a/scenarios/data/docs_detail/metadata-separator/0.json b/scenarios/data/docs_detail/metadata-separator/default.json similarity index 100% rename from scenarios/data/docs_detail/metadata-separator/0.json rename to scenarios/data/docs_detail/metadata-separator/default.json diff --git a/scenarios/data/docs_detail/metadata-tag-list/0.json b/scenarios/data/docs_detail/metadata-tag-list/default.json similarity index 100% rename from scenarios/data/docs_detail/metadata-tag-list/0.json rename to scenarios/data/docs_detail/metadata-tag-list/default.json diff --git a/scenarios/data/docs_detail/metadata-value/0.json b/scenarios/data/docs_detail/metadata-value/default.json similarity index 100% rename from scenarios/data/docs_detail/metadata-value/0.json rename to scenarios/data/docs_detail/metadata-value/default.json diff --git a/scenarios/data/docs_detail/metadata/0.json b/scenarios/data/docs_detail/metadata/default.json similarity index 100% rename from scenarios/data/docs_detail/metadata/0.json rename to scenarios/data/docs_detail/metadata/default.json diff --git a/scenarios/data/docs_form/checkbox/0.json b/scenarios/data/docs_form/checkbox/default.json similarity index 100% rename from scenarios/data/docs_form/checkbox/0.json rename to scenarios/data/docs_form/checkbox/default.json diff --git a/scenarios/data/docs_form/date-picker/0.json b/scenarios/data/docs_form/date-picker/default.json similarity index 100% rename from scenarios/data/docs_form/date-picker/0.json rename to scenarios/data/docs_form/date-picker/default.json diff --git a/scenarios/data/docs_form/main/0.json b/scenarios/data/docs_form/main/default.json similarity index 100% rename from scenarios/data/docs_form/main/0.json rename to scenarios/data/docs_form/main/default.json diff --git a/scenarios/data/docs_form/password-field/0.json b/scenarios/data/docs_form/password-field/default.json similarity index 100% rename from scenarios/data/docs_form/password-field/0.json rename to scenarios/data/docs_form/password-field/default.json diff --git a/scenarios/data/docs_form/select/0.json b/scenarios/data/docs_form/select/default.json similarity index 100% rename from scenarios/data/docs_form/select/0.json rename to scenarios/data/docs_form/select/default.json diff --git a/scenarios/data/docs_form/separator/0.json b/scenarios/data/docs_form/separator/default.json similarity index 100% rename from scenarios/data/docs_form/separator/0.json rename to scenarios/data/docs_form/separator/default.json diff --git a/scenarios/data/docs_form/text-field/0.json b/scenarios/data/docs_form/text-field/default.json similarity index 100% rename from scenarios/data/docs_form/text-field/0.json rename to scenarios/data/docs_form/text-field/default.json diff --git a/scenarios/data/docs_grid/empty-view/0.json b/scenarios/data/docs_grid/empty-view/default.json similarity index 100% rename from scenarios/data/docs_grid/empty-view/0.json rename to scenarios/data/docs_grid/empty-view/default.json diff --git a/scenarios/data/docs_grid/main/0.json b/scenarios/data/docs_grid/main/default.json similarity index 100% rename from scenarios/data/docs_grid/main/0.json rename to scenarios/data/docs_grid/main/default.json diff --git a/scenarios/data/docs_grid/section/0.json b/scenarios/data/docs_grid/section/default.json similarity index 100% rename from scenarios/data/docs_grid/section/0.json rename to scenarios/data/docs_grid/section/default.json diff --git a/scenarios/data/docs_list/detail/0.json b/scenarios/data/docs_list/detail/default.json similarity index 100% rename from scenarios/data/docs_list/detail/0.json rename to scenarios/data/docs_list/detail/default.json diff --git a/scenarios/data/docs_list/empty-view/0.json b/scenarios/data/docs_list/empty-view/default.json similarity index 100% rename from scenarios/data/docs_list/empty-view/0.json rename to scenarios/data/docs_list/empty-view/default.json diff --git a/scenarios/data/docs_list/main/0.json b/scenarios/data/docs_list/main/default.json similarity index 100% rename from scenarios/data/docs_list/main/0.json rename to scenarios/data/docs_list/main/default.json diff --git a/scenarios/data/docs_list/section/0.json b/scenarios/data/docs_list/section/default.json similarity index 100% rename from scenarios/data/docs_list/section/0.json rename to scenarios/data/docs_list/section/default.json From 508909cf7d5dff4ce6055e711a305aa466add4d8 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 27 Oct 2024 17:40:49 +0100 Subject: [PATCH 151/540] Fix global shortcut not working on windows --- Cargo.lock | 2 +- rust/client/Cargo.toml | 1 + .../plugins => client/src}/global_shortcut.rs | 9 ++- rust/client/src/lib.rs | 1 + rust/client/src/ui/mod.rs | 81 ++++++++++++++++++- rust/common/src/model.rs | 4 + rust/common/src/rpc/frontend_api.rs | 61 ++++++++++---- rust/server/Cargo.toml | 1 - rust/server/src/plugins/mod.rs | 33 +------- rust/server/src/rpc.rs | 2 +- rust/utils/src/channel.rs | 1 + 11 files changed, 144 insertions(+), 52 deletions(-) rename rust/{server/src/plugins => client/src}/global_shortcut.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index 862ac94..8370229 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1101,6 +1101,7 @@ dependencies = [ "common-ui", "component_model", "convert_case 0.6.0", + "global-hotkey", "iced", "iced_aw", "image 0.25.1", @@ -8097,7 +8098,6 @@ dependencies = [ "freedesktop-icons", "freedesktop_entry_parser", "git2", - "global-hotkey", "icns", "image 0.25.1", "include_dir", diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index c41ccc9..116fd8b 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -21,6 +21,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" once_cell = "1.19" bytes = "1.6.0" +global-hotkey = "0.4.2" [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] tray-icon = { version = "0.15.1", default-features = false } diff --git a/rust/server/src/plugins/global_shortcut.rs b/rust/client/src/global_shortcut.rs similarity index 97% rename from rust/server/src/plugins/global_shortcut.rs rename to rust/client/src/global_shortcut.rs index 2c85ae0..f605310 100644 --- a/rust/server/src/plugins/global_shortcut.rs +++ b/rust/client/src/global_shortcut.rs @@ -1,17 +1,20 @@ use global_hotkey::hotkey::{Code, HotKey, Modifiers}; +use iced::futures::channel::mpsc::Sender; +use iced::futures::SinkExt; use tokio::runtime::Handle; use common::model::{PhysicalKey, PhysicalShortcut}; use common::rpc::frontend_api::FrontendApi; +use crate::ui::AppMsg; -pub fn register_listener(frontend_api: FrontendApi) { +pub fn register_listener(msg_sender: Sender) { let handle = Handle::current(); global_hotkey::GlobalHotKeyEvent::set_event_handler(Some(move |e: global_hotkey::GlobalHotKeyEvent| { - let mut frontend_api = frontend_api.clone(); + let mut msg_sender = msg_sender.clone(); if let global_hotkey::HotKeyState::Released = e.state() { handle.spawn(async move { - if let Err(err) = frontend_api.show_window().await { + if let Err(err) = msg_sender.send(AppMsg::ShowWindow).await { tracing::warn!(target = "rpc", "error occurred when receiving shortcut event {:?}", err) } }); diff --git a/rust/client/src/lib.rs b/rust/client/src/lib.rs index 209b5ff..2b5a189 100644 --- a/rust/client/src/lib.rs +++ b/rust/client/src/lib.rs @@ -6,6 +6,7 @@ use crate::ui::GauntletTheme; pub(in crate) mod ui; pub(in crate) mod model; +pub mod global_shortcut; pub fn start_client( minimized: bool, diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 1d9cd90..8b73b0b 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -2,8 +2,10 @@ use std::collections::HashMap; use std::fs; use std::path::Path; use std::rc::Rc; -use std::sync::{Arc, RwLock as StdRwLock}; +use std::sync::{Arc, Mutex as StdMutex, Mutex, RwLock as StdRwLock}; use anyhow::anyhow; +use global_hotkey::GlobalHotKeyManager; +use global_hotkey::hotkey::HotKey; use iced::{event, executor, font, futures, keyboard, subscription, window, Alignment, Command, Event, Font, Length, Padding, Pixels, Settings, Size, Subscription}; use iced::advanced::graphics::core::SmolStr; use iced::advanced::layout::Limits; @@ -21,7 +23,7 @@ use iced::window::settings::PlatformSpecific; use iced_aw::core::icons; use serde::Deserialize; use tokio::runtime::Handle; -use tokio::sync::RwLock as TokioRwLock; +use tokio::sync::{Mutex as TokioMutex, RwLock as TokioRwLock}; use tonic::transport::Server; use client_context::ClientContext; @@ -30,7 +32,7 @@ use common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFron use common::scenario_convert::{ui_render_location_from_scenario, ui_widget_from_scenario}; use common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; use common_ui::physical_key_model; -use utils::channel::{channel, RequestReceiver, RequestSender}; +use utils::channel::{channel, RequestReceiver, RequestSender, Responder}; use crate::model::UiViewEvent; use crate::ui::inline_view_container::{inline_view_action_panel, inline_view_container}; @@ -56,6 +58,7 @@ mod state; mod hud; pub use theme::GauntletTheme; +use crate::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; use crate::ui::hud::{close_hud_window, show_hud_window}; use crate::ui::scroll_handle::ScrollHandle; use crate::ui::state::{ErrorViewData, Focus, GlobalState, MainViewState, PluginViewData, PluginViewState}; @@ -64,6 +67,8 @@ use crate::ui::widget_container::PluginWidgetContainer; pub struct AppModel { // logic backend_api: BackendForFrontendApi, + global_hotkey_manager: Arc>, + current_hotkey: Arc>>, frontend_receiver: Arc>>, focused: bool, theme: GauntletTheme, @@ -174,6 +179,10 @@ pub enum AppMsg { OnPrimaryActionMainViewActionPanelMouse { widget_id: UiWidgetId }, ResetMainViewState, OnAnyActionMainViewNoPanelKeyboardAtIndex { index: usize }, + SetGlobalShortcut { + shortcut: PhysicalShortcut, + responder: Arc>>> + }, } pub struct AppFlags { @@ -283,6 +292,9 @@ impl Application for AppModel { let backend_api = BackendForFrontendApi::new(backend_sender); + let global_hotkey_manager = GlobalHotKeyManager::new() + .expect("unable to create global hot key manager"); + let mut commands = vec![ font::load(icons::BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded), ]; @@ -391,6 +403,8 @@ impl Application for AppModel { AppModel { // logic backend_api, + global_hotkey_manager: Arc::new(StdRwLock::new(global_hotkey_manager)), + current_hotkey: Arc::new(StdMutex::new(None)), frontend_receiver: Arc::new(TokioRwLock::new(frontend_receiver)), focused: false, theme: GauntletTheme::new(), @@ -1088,6 +1102,49 @@ impl Application for AppModel { GlobalState::PluginView { .. } => Command::none(), } } + AppMsg::SetGlobalShortcut { shortcut, responder } => { + tracing::info!("Registering new global shortcut: {:?}", shortcut); + + let run = || { + let global_hotkey_manager = self.global_hotkey_manager + .read() + .expect("lock is poisoned"); + + let mut hotkey_guard = self.current_hotkey + .lock() + .expect("lock is poisoned"); + + if let Some(current_hotkey) = *hotkey_guard { + global_hotkey_manager.unregister(current_hotkey)?; + } + + let hotkey = convert_physical_shortcut_to_hotkey(shortcut); + *hotkey_guard = Some(hotkey); + + global_hotkey_manager.register(hotkey)?; + + Ok(()) + }; + + // responder is not clone and send, and we need to consume it + // so we wrap it in arc mutex option + let mut responder = responder + .lock() + .expect("lock is poisoned") + .take() + .expect("there should always be a responder here"); + + match run() { + Ok(()) => { + responder.respond(UiResponseData::Nothing); + } + Err(err) => { + responder.respond(UiResponseData::Err(err)); + } + } + + Command::none() + } } } @@ -1557,6 +1614,7 @@ impl Application for AppModel { let frontend_receiver = self.frontend_receiver.clone(); struct RequestLoop; + struct GlobalShortcutListener; let events_subscription = event::listen_with(|event, status| match status { event::Status::Ignored => Some(AppMsg::IcedEvent(event)), @@ -1567,6 +1625,17 @@ impl Application for AppModel { }); Subscription::batch([ + subscription::channel( + std::any::TypeId::of::(), + 10, + |sender| async move { + register_listener(sender.clone()); + + std::future::pending::<()>().await; + + unreachable!() + }, + ), events_subscription, subscription::channel( std::any::TypeId::of::(), @@ -1978,6 +2047,12 @@ async fn request_loop( display } } + UiRequestData::SetGlobalShortcut { shortcut } => { + AppMsg::SetGlobalShortcut { + shortcut, + responder: Arc::new(Mutex::new(Some(responder))) + } + } } }; diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 525fa12..0a73d1d 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -118,6 +118,7 @@ pub enum SearchResultEntrypointType { #[derive(Debug)] pub enum UiResponseData { Nothing, + Err(anyhow::Error), } #[derive(Debug)] @@ -150,6 +151,9 @@ pub enum UiRequestData { ShowHud { display: String }, + SetGlobalShortcut { + shortcut: PhysicalShortcut + }, } #[derive(Debug)] diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index fe15f94..7cd97c1 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -1,12 +1,15 @@ +use anyhow::anyhow; use thiserror::Error; use utils::channel::{RequestError, RequestSender}; -use crate::model::{EntrypointId, PluginId, UiRenderLocation, UiRequestData, UiResponseData, UiWidget}; +use crate::model::{EntrypointId, PhysicalShortcut, PluginId, UiRenderLocation, UiRequestData, UiResponseData, UiWidget}; -#[derive(Error, Debug, Clone)] +#[derive(Error, Debug)] pub enum FrontendApiError { #[error("Frontend wasn't able to process request in a timely manner")] TimeoutError, + #[error("Other error: {0:?}")] + OtherError(#[from] anyhow::Error), } impl From for FrontendApiError { @@ -29,14 +32,14 @@ impl FrontendApi { } } - pub async fn request_search_results_update(&mut self) -> Result<(), FrontendApiError> { + pub async fn request_search_results_update(&self) -> Result<(), FrontendApiError> { let _ = self.frontend_sender.send_receive(UiRequestData::RequestSearchResultUpdate).await; Ok(()) } pub async fn replace_view( - &mut self, + &self, plugin_id: PluginId, plugin_name: String, entrypoint_id: EntrypointId, @@ -55,29 +58,35 @@ impl FrontendApi { container, }; - let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await?; + let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await? else { + unreachable!() + }; Ok(()) } - pub async fn clear_inline_view(&mut self, plugin_id: PluginId) -> Result<(), FrontendApiError> { + pub async fn clear_inline_view(&self, plugin_id: PluginId) -> Result<(), FrontendApiError> { let request = UiRequestData::ClearInlineView { plugin_id, }; - let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await?; + let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await? else { + unreachable!() + }; Ok(()) } pub async fn show_window(&self) -> Result<(), FrontendApiError> { - let UiResponseData::Nothing = self.frontend_sender.send_receive(UiRequestData::ShowWindow).await?; + let UiResponseData::Nothing = self.frontend_sender.send_receive(UiRequestData::ShowWindow).await? else { + unreachable!() + }; Ok(()) } pub async fn show_preference_required_view( - &mut self, + &self, plugin_id: PluginId, entrypoint_id: EntrypointId, plugin_preferences_required: bool, @@ -90,13 +99,15 @@ impl FrontendApi { entrypoint_preferences_required, }; - let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await?; + let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await? else { + unreachable!() + }; Ok(()) } pub async fn show_plugin_error_view( - &mut self, + &self, plugin_id: PluginId, entrypoint_id: EntrypointId, render_location: UiRenderLocation, @@ -107,21 +118,43 @@ impl FrontendApi { render_location, }; - let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await?; + let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await? else { + unreachable!() + }; Ok(()) } pub async fn show_hud( - &mut self, + &self, display: String, ) -> Result<(), FrontendApiError> { let request = UiRequestData::ShowHud { display, }; - let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await?; + let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await? else { + unreachable!() + }; Ok(()) } + + pub async fn set_global_shortcut( + &self, + shortcut: PhysicalShortcut + ) -> anyhow::Result<()> { + let request = UiRequestData::SetGlobalShortcut { + shortcut, + }; + + let data = self.frontend_sender.send_receive(request) + .await + .map_err(|err| anyhow!("error: {:?}", err))?; + + match data { + UiResponseData::Nothing => Ok(()), + UiResponseData::Err(err) => Err(err) + } + } } \ No newline at end of file diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index e6c66b5..32a4033 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -35,7 +35,6 @@ uuid = "1.8" resvg = { version = "0.41", default-features = false} image = "0.25" arboard = { version = "=3.2.1", features = ["wayland-data-control"] } # TODO update when dependency hell is solved -global-hotkey = "0.4.2" ureq = "2.10.0" bytes = "1.6.0" typed-path = "0.9" diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index b2d77eb..eb7ba4b 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -5,9 +5,7 @@ use std::thread; use std::time::Duration; use anyhow::anyhow; use deno_core::futures::channel::mpsc::Sender; -use global_hotkey::GlobalHotKeyManager; -use global_hotkey::hotkey::HotKey; -use include_dir::{Dir, include_dir}; +use include_dir::{include_dir, Dir}; use tokio::runtime::Handle; use common::model::{DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, UiPropertyValue, UiRequestData, UiResponseData, UiWidgetId}; @@ -17,10 +15,9 @@ use utils::channel::RequestSender; use common::dirs::Dirs; use crate::model::{ActionShortcutKey, JsKeyboardEventOrigin}; use crate::plugins::config_reader::ConfigReader; -use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions}; -use crate::plugins::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; +use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginActionShortcutKind, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginMainSearchBarPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; -use crate::plugins::js::{AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginRuntimeData, start_plugin_runtime}; +use crate::plugins::js::{start_plugin_runtime, AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginRuntimeData}; use crate::plugins::js::clipboard::Clipboard; use crate::plugins::js::permissions::{PluginPermissions, PluginPermissionsClipboard, PluginPermissionsExec, PluginPermissionsFileSystem, PluginPermissionsMainSearchBar}; use crate::plugins::loader::PluginLoader; @@ -37,7 +34,6 @@ mod download_status; mod applications; mod icon_cache; pub(super) mod frecency; -mod global_shortcut; static BUNDLED_PLUGINS: [(&str, Dir); 1] = [ ("gauntlet", include_dir!("$CARGO_MANIFEST_DIR/../../bundled_plugins/gauntlet/dist")), @@ -52,8 +48,6 @@ pub struct ApplicationManager { run_status_holder: RunStatusHolder, icon_cache: IconCache, frontend_api: FrontendApi, - global_hotkey_manager: GlobalHotKeyManager, - current_hotkey: Mutex>, dirs: Dirs, clipboard: Clipboard, } @@ -68,13 +62,10 @@ impl ApplicationManager { let icon_cache = IconCache::new(dirs.clone()); let run_status_holder = RunStatusHolder::new(); let search_index = SearchIndex::create_index(frontend_api.clone())?; - let global_hotkey_manager = GlobalHotKeyManager::new()?; let clipboard = Clipboard::new()?; let (command_broadcaster, _) = tokio::sync::broadcast::channel::(100); - register_listener(frontend_api.clone()); - let manager = Self { config_reader, search_index, @@ -84,9 +75,7 @@ impl ApplicationManager { run_status_holder, icon_cache, frontend_api, - global_hotkey_manager, clipboard, - current_hotkey: Mutex::new(None), dirs }; @@ -290,21 +279,7 @@ impl ApplicationManager { async fn register_global_shortcut(&self) -> anyhow::Result<()> { let shortcut = self.db_repository.get_global_shortcut().await?; - tracing::info!("Registering new global shortcut: {:?}", shortcut); - - let mut hotkey_guard = self.current_hotkey.lock() - .expect("lock is poisoned"); - - if let Some(current_hotkey) = *hotkey_guard { - self.global_hotkey_manager.unregister(current_hotkey)?; - } - - let hotkey = convert_physical_shortcut_to_hotkey(shortcut); - *hotkey_guard = Some(hotkey); - - self.global_hotkey_manager.register(hotkey)?; - - Ok(()) + self.frontend_api.set_global_shortcut(shortcut).await } pub async fn reload_config(&self) -> anyhow::Result<()> { diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index df3bc5b..66e7e55 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -75,7 +75,7 @@ impl BackendServer for BackendServerImpl { tracing::warn!(target = "rpc", "error occurred when handling 'set_global_shortcut' request {:?}", err) } - Ok(()) + result } async fn get_global_shortcut(&self) -> anyhow::Result { diff --git a/rust/utils/src/channel.rs b/rust/utils/src/channel.rs index bf0b2be..6f4ed4c 100644 --- a/rust/utils/src/channel.rs +++ b/rust/utils/src/channel.rs @@ -3,6 +3,7 @@ use std::time::Duration; use tokio::sync::{mpsc, oneshot}; use tokio::time::error::Elapsed; +#[derive(Debug)] pub enum RequestError { TimeoutError, } From 750278dc4800dd10b19fb9b1a536ffdd251a8b60 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 27 Oct 2024 19:33:39 +0100 Subject: [PATCH 152/540] Implement List and Grid search bar --- dev_plugin/src/grid-view.tsx | 37 +++++++++---- dev_plugin/src/list-view.tsx | 21 ++++++-- docs/js/components/search_bar/description.md | 1 + .../components/search_bar/props/onChange.md | 0 .../search_bar/props/placeholder.md | 0 docs/js/components/search_bar/props/value.md | 0 js/api/src/gen/components.tsx | 25 +++++++-- rust/client/src/ui/theme/mod.rs | 13 +++-- rust/client/src/ui/theme/row.rs | 4 ++ rust/client/src/ui/theme/text_input.rs | 24 ++++++--- rust/client/src/ui/widget.rs | 53 +++++++++++++++---- rust/component_model/src/lib.rs | 18 +++++++ 12 files changed, 158 insertions(+), 38 deletions(-) create mode 100644 docs/js/components/search_bar/description.md create mode 100644 docs/js/components/search_bar/props/onChange.md create mode 100644 docs/js/components/search_bar/props/placeholder.md create mode 100644 docs/js/components/search_bar/props/value.md diff --git a/dev_plugin/src/grid-view.tsx b/dev_plugin/src/grid-view.tsx index 4b4b3d0..b4e1a7d 100644 --- a/dev_plugin/src/grid-view.tsx +++ b/dev_plugin/src/grid-view.tsx @@ -1,5 +1,5 @@ -import { Grid, IconAccessory, Icons, TextAccessory } from "@project-gauntlet/api/components"; -import { ReactElement } from "react"; +import { Grid, IconAccessory, Icons, List, TextAccessory } from "@project-gauntlet/api/components"; +import { ReactElement, useState } from "react"; import { useStorage } from "@project-gauntlet/api/hooks"; export default function GridView(): ReactElement { @@ -10,18 +10,33 @@ export default function GridView(): ReactElement { const [val3, setValue3] = useStorage("grid-view-test-3", ""); const [val4, setValue4] = useStorage("grid-view-test-4", ""); + const [searchText, setSearchText] = useState(""); + return ( + { - numbers.map(value => ( - - - - Test Paragraph {value} - - - - )) + numbers.map(value => { + const title = "Title " + value; + + if (title.toLowerCase().includes(searchText?.toLowerCase() ?? "")) { + return ( + + + + Test Paragraph {value} + + + + ) + } else { + return undefined + } + }) } diff --git a/dev_plugin/src/list-view.tsx b/dev_plugin/src/list-view.tsx index fe8e57f..81e0d45 100644 --- a/dev_plugin/src/list-view.tsx +++ b/dev_plugin/src/list-view.tsx @@ -10,12 +10,27 @@ export default function ListView(): ReactElement { setId(id); }; + const [searchText, setSearchText] = useState(""); + return ( + { - numbers.map(value => ( - - )) + numbers.map(value => { + const title = "Title " + value; + + if (title.toLowerCase().includes(searchText?.toLowerCase() ?? "")) { + return ( + + ) + } else { + return undefined + } + }) } diff --git a/docs/js/components/search_bar/description.md b/docs/js/components/search_bar/description.md new file mode 100644 index 0000000..1ecfae1 --- /dev/null +++ b/docs/js/components/search_bar/description.md @@ -0,0 +1 @@ +Adds search bar above the content \ No newline at end of file diff --git a/docs/js/components/search_bar/props/onChange.md b/docs/js/components/search_bar/props/onChange.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/js/components/search_bar/props/placeholder.md b/docs/js/components/search_bar/props/placeholder.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/js/components/search_bar/props/value.md b/docs/js/components/search_bar/props/value.md new file mode 100644 index 0000000..e69de29 diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index 4f4dae1..f608823 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -132,6 +132,11 @@ declare global { icon?: ImageSource | Icons; tooltip?: string; }; + ["gauntlet:search_bar"]: { + value?: string; + placeholder?: string; + onChange?: (value: string | undefined) => void; + }; ["gauntlet:list_item"]: { children?: ElementComponent; title: string; @@ -145,7 +150,7 @@ declare global { subtitle?: string; }; ["gauntlet:list"]: { - children?: ElementComponent; + children?: ElementComponent; isLoading?: boolean; }; ["gauntlet:grid_item"]: { @@ -161,7 +166,7 @@ declare global { columns?: number; }; ["gauntlet:grid"]: { - children?: ElementComponent; + children?: ElementComponent; isLoading?: boolean; columns?: number; }; @@ -660,6 +665,14 @@ export interface TextAccessoryProps { export const TextAccessory: FC = (props: TextAccessoryProps): ReactNode => { return ; }; +export interface SearchBarProps { + value?: string; + placeholder?: string; + onChange?: (value: string | undefined) => void; +} +export const SearchBar: FC = (props: SearchBarProps): ReactNode => { + return ; +}; export interface ListItemProps { title: string; subtitle?: string; @@ -682,11 +695,12 @@ export const ListSection: FC & { }; ListSection.Item = ListItem; export interface ListProps { - children?: ElementComponent; + children?: ElementComponent; actions?: ElementComponent; isLoading?: boolean; } export const List: FC & { + SearchBar: typeof SearchBar; EmptyView: typeof EmptyView; Detail: typeof Detail; Item: typeof ListItem; @@ -694,6 +708,7 @@ export const List: FC & { } = (props: ListProps): ReactNode => { return {props.actions as any}{props.children}; }; +List.SearchBar = SearchBar; List.EmptyView = EmptyView; List.Detail = Detail; List.Item = ListItem; @@ -724,18 +739,20 @@ export const GridSection: FC & { }; GridSection.Item = GridItem; export interface GridProps { - children?: ElementComponent; + children?: ElementComponent; isLoading?: boolean; actions?: ElementComponent; columns?: number; } export const Grid: FC & { + SearchBar: typeof SearchBar; EmptyView: typeof EmptyView; Item: typeof GridItem; Section: typeof GridSection; } = (props: GridProps): ReactNode => { return {props.actions as any}{props.children}; }; +Grid.SearchBar = SearchBar; Grid.EmptyView = EmptyView; Grid.Item = GridItem; Grid.Section = GridSection; diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 6217b56..01f1431 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -25,7 +25,7 @@ mod loading_bar; pub type Element<'a, Message> = iced::Element<'a, Message, GauntletTheme>; const CURRENT_COLOR_THEME_VERSION: u64 = 3; -const CURRENT_THEME_VERSION: u64 = 3; +const CURRENT_THEME_VERSION: u64 = 4; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GauntletColorTheme { @@ -114,7 +114,7 @@ pub struct GauntletTheme { root_bottom_panel_action_toggle_text: ThemePaddingTextColor, root_bottom_panel_primary_action_text: ThemePaddingTextColor, root_content: ThemePaddingOnly, - root_top_panel: ThemePaddingOnly, + root_top_panel: ThemePaddingSpacing, root_top_panel_button: ThemeButton, metadata_link: ThemeLink, separator: ThemeSeparator, @@ -380,8 +380,9 @@ impl GauntletTheme { metadata_separator: ThemePaddingOnly { padding: padding_axis(8.0, 0.0), }, - root_top_panel: ThemePaddingOnly { + root_top_panel: ThemePaddingSpacing { padding: padding_all(12.0), + spacing: 12.0, }, root_top_panel_button: ThemeButton { padding: padding_axis(3.0, 5.0), @@ -907,6 +908,12 @@ pub struct ThemePaddingTextColorSpacing { spacing: f32, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThemePaddingSpacing { + padding: ThemePadding, + spacing: f32, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExternalThemeSize { width: f32, diff --git a/rust/client/src/ui/theme/row.rs b/rust/client/src/ui/theme/row.rs index fdfdbad..12ad44a 100644 --- a/rust/client/src/ui/theme/row.rs +++ b/rust/client/src/ui/theme/row.rs @@ -9,6 +9,7 @@ pub enum RowStyle { GridSectionTitle, GridItemTitle, RootBottomPanel, + RootTopPanel, } impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletTheme, Renderer> { @@ -38,6 +39,9 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletT RowStyle::RootBottomPanel => { self.spacing(theme.root_bottom_panel.spacing) } + RowStyle::RootTopPanel => { + self.spacing(theme.root_top_panel.spacing) + } }.into() } } \ No newline at end of file diff --git a/rust/client/src/ui/theme/text_input.rs b/rust/client/src/ui/theme/text_input.rs index dce6cde..3f0eebe 100644 --- a/rust/client/src/ui/theme/text_input.rs +++ b/rust/client/src/ui/theme/text_input.rs @@ -1,8 +1,8 @@ -use iced::{Border, Color, Renderer}; use iced::widget::{text_input, TextInput}; +use iced::{Border, Color, Renderer}; use text_input::Appearance; -use crate::ui::theme::{Element, GauntletTheme, NOT_INTENDED_TO_BE_USED, ThemableWidget}; +use crate::ui::theme::{Element, GauntletTheme, ThemableWidget, NOT_INTENDED_TO_BE_USED}; #[derive(Default)] pub enum TextInputStyle { @@ -10,6 +10,7 @@ pub enum TextInputStyle { ShouldNotBeUsed, MainSearch, + PluginSearchBar, FormInput, } @@ -42,7 +43,7 @@ impl text_input::StyleSheet for GauntletTheme { icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), } }, - TextInputStyle::MainSearch => { + TextInputStyle::MainSearch | TextInputStyle::PluginSearchBar => { Appearance { background: Color::TRANSPARENT.into(), border: Border { @@ -80,7 +81,7 @@ impl text_input::StyleSheet for GauntletTheme { icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), } }, - TextInputStyle::MainSearch => { + TextInputStyle::MainSearch | TextInputStyle::PluginSearchBar => { Appearance { background: Color::TRANSPARENT.into(), border: Border { @@ -126,8 +127,17 @@ impl<'a, Message: 'a + Clone> ThemableWidget<'a, Message> for TextInput<'a, Mess type Kind = TextInputStyle; fn themed(self, kind: TextInputStyle) -> Element<'a, Message> { - self.style(kind) - // .padding() // TODO - .into() + match kind { + TextInputStyle::PluginSearchBar => { + self.style(kind) + .padding(0) + .into() + } + _ => { + self.style(kind) + // .padding() // TODO + .into() + } + } } } \ No newline at end of file diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 46c2332..1f0a816 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -10,10 +10,10 @@ use common_ui::shortcut_to_text; use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; use iced::widget::image::Handle; +use iced::widget::text::Shaping; use iced::widget::tooltip::Position; use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, mouse_area, pick_list, row, scrollable, text, text_input, tooltip, vertical_rule, Space}; use iced::{Alignment, Font, Length}; -use iced::widget::text::Shaping; use iced_aw::core::icons; use iced_aw::date_picker::Date; use iced_aw::floating_element::Offset; @@ -22,7 +22,6 @@ use iced_aw::{floating_element, GridRow}; use itertools::Itertools; use crate::model::UiViewEvent; -use crate::ui::AppMsg; use crate::ui::custom_widgets::loading_bar::LoadingBar; use crate::ui::scroll_handle::ScrollHandle; use crate::ui::state::PluginViewState; @@ -38,6 +37,7 @@ use crate::ui::theme::text::TextStyle; use crate::ui::theme::text_input::TextInputStyle; use crate::ui::theme::tooltip::TooltipStyle; use crate::ui::theme::{Element, ThemableWidget}; +use crate::ui::AppMsg; #[derive(Clone, Debug)] pub struct ComponentWidgetWrapper { @@ -65,6 +65,9 @@ pub enum ComponentWidgetState { Select { state_value: Option }, + SearchBar { + state_value: String + }, Detail { show_action_panel: bool, }, @@ -108,6 +111,9 @@ impl ComponentWidgetState { ComponentWidget::Select { value, .. } => ComponentWidgetState::Select { state_value: value.to_owned() }, + ComponentWidget::SearchBar { value, .. } => ComponentWidgetState::SearchBar { + state_value: value.to_owned().unwrap_or("".to_owned()) + }, ComponentWidget::Detail { .. } => ComponentWidgetState::Detail { show_action_panel: false, }, @@ -1034,7 +1040,7 @@ impl ComponentWidgetWrapper { items.push(child.render_widget(ComponentRenderContext::List { widget_id })) }, - ComponentWidget::EmptyView { .. } | ComponentWidget::Detail { .. } => {}, + ComponentWidget::EmptyView { .. } | ComponentWidget::Detail { .. } | ComponentWidget::SearchBar { .. } => {}, _ => panic!("unexpected widget kind {:?}", widget) } } @@ -1183,7 +1189,7 @@ impl ComponentWidgetWrapper { items.push(child.render_widget(ComponentRenderContext::Grid { widget_id })) }, - ComponentWidget::EmptyView { .. } => {}, + ComponentWidget::EmptyView { .. } | ComponentWidget::SearchBar { .. } => {}, _ => panic!("unexpected widget kind {:?}", widget) } } @@ -1211,6 +1217,15 @@ impl ComponentWidgetWrapper { render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) } + ComponentWidget::SearchBar { placeholder, .. } => { + let ComponentWidgetState::SearchBar { state_value } = state else { + panic!("unexpected state kind {:?}", state) + }; + + text_input(placeholder.as_deref().unwrap_or_default(), state_value) + .on_input(move |value| ComponentWidgetEvent::OnChangeSearchBar { widget_id, value }) + .themed(TextInputStyle::PluginSearchBar) + } } } @@ -1223,7 +1238,7 @@ impl ComponentWidgetWrapper { } } -fn create_top_panel<'a>() -> Element<'a, ComponentWidgetEvent> { +fn create_top_panel<'a>(children: &[ComponentWidgetWrapper]) -> Element<'a, ComponentWidgetEvent> { let icon = text(icons::Bootstrap::ArrowLeft) .font(icons::BOOTSTRAP_FONT); @@ -1231,12 +1246,13 @@ fn create_top_panel<'a>() -> Element<'a, ComponentWidgetEvent> { .on_press(ComponentWidgetEvent::PreviousView) .themed(ButtonStyle::RootTopPanelBackButton); - let space = Space::with_width(Length::FillPortion(3)) - .into(); + let search_bar_element = render_child_by_type(children, |widget| matches!(widget, ComponentWidget::SearchBar { .. }), ComponentRenderContext::None) + .ok() + .unwrap_or_else(|| Space::with_width(Length::FillPortion(3)).into()); - let top_panel: Element<_> = row(vec![back_button, space]) + let top_panel: Element<_> = row(vec![back_button, search_bar_element]) .align_items(Alignment::Center) - .into(); + .themed(RowStyle::RootTopPanel); let top_panel: Element<_> = container(top_panel) .width(Length::Fill) @@ -1356,7 +1372,7 @@ fn render_plugin_root<'a>( panic!("not supposed to be passed to root item: {:?}", context) }; - let top_panel = create_top_panel(); + let top_panel = create_top_panel(children); let top_separator = if is_loading { LoadingBar::new() @@ -1984,6 +2000,10 @@ pub enum ComponentWidgetEvent { widget_id: UiWidgetId, value: String }, + OnChangeSearchBar { + widget_id: UiWidgetId, + value: String + }, SubmitDatePicker { widget_id: UiWidgetId, value: String @@ -2106,6 +2126,18 @@ impl ComponentWidgetEvent { Some(create_password_field_on_change_event(widget_id, Some(value))) } + ComponentWidgetEvent::OnChangeSearchBar { widget_id, value } => { + { + let (widget, ref mut state) = &mut *widget.get_mut(); + let ComponentWidgetState::SearchBar { state_value } = state else { + panic!("unexpected state kind, widget: {:?} state: {:?}", widget, state) + }; + + *state_value = value.clone(); + } + + Some(create_search_bar_on_change_event(widget_id, Some(value))) + } ComponentWidgetEvent::ToggleActionPanel { .. } => { Some(UiViewEvent::AppEvent { event: AppMsg::ToggleActionPanel { keyboard: false } @@ -2141,6 +2173,7 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::SelectPickList { widget_id, .. } => widget_id, ComponentWidgetEvent::OnChangeTextField { widget_id, .. } => widget_id, ComponentWidgetEvent::OnChangePasswordField { widget_id, .. } => widget_id, + ComponentWidgetEvent::OnChangeSearchBar { widget_id, .. } => widget_id, ComponentWidgetEvent::ToggleActionPanel { widget_id } => widget_id, ComponentWidgetEvent::ListItemClick { widget_id, .. } => widget_id, ComponentWidgetEvent::GridItemClick { widget_id, .. } => widget_id, diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index 04161ad..20b5faf 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -1011,6 +1011,20 @@ pub fn create_component_model() -> Vec { children_none(), ); + let search_bar_component = component( + "search_bar", + mark_doc!("/search_bar/description.md"), + "SearchBar", + [ + property("value", mark_doc!("/search_bar/props/value.md"),true, PropertyType::String), + property("placeholder", mark_doc!("/search_bar/props/placeholder.md"),true, PropertyType::String), + event("onChange", mark_doc!("/search_bar/props/onChange.md"),true, [ + property("value", "".to_string(), true, PropertyType::String) + ]) + ], + children_none(), + ); + let list_item_component = component( "list_item", mark_doc!("/list_item/description.md"), @@ -1047,6 +1061,7 @@ pub fn create_component_model() -> Vec { property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), ], children_members([ + member("SearchBar", &search_bar_component), member("EmptyView", &empty_view_component), member("Detail", &detail_component), member("Item", &list_item_component), @@ -1099,6 +1114,7 @@ pub fn create_component_model() -> Vec { // inset ], children_members([ + member("SearchBar", &search_bar_component), member("EmptyView", &empty_view_component), member("Item", &grid_item_component), member("Section", &grid_section_component), @@ -1224,6 +1240,8 @@ pub fn create_component_model() -> Vec { accessory_icon_component, accessory_text_component, + search_bar_component, + list_item_component, list_section_component, list_component, From ec8511a5a44354ec1e931d5b0c28864daeafaf6c Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Wed, 30 Oct 2024 21:05:03 +0100 Subject: [PATCH 153/540] Work in progress keyboard navigation for List and Grid --- rust/client/src/ui/client_context.rs | 12 ++++ rust/client/src/ui/mod.rs | 4 ++ rust/client/src/ui/scroll_handle.rs | 61 ++++++++++++++--- rust/client/src/ui/state/mod.rs | 92 +++++++++++++++++++------- rust/client/src/ui/theme/button.rs | 14 +++- rust/client/src/ui/widget.rs | 66 ++++++++++++++++-- rust/client/src/ui/widget_container.rs | 11 +++ 7 files changed, 218 insertions(+), 42 deletions(-) diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index ab6b7d3..3dc39b8 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -109,4 +109,16 @@ impl ClientContext { pub fn get_action_ids(&self) -> Vec { self.view.get_action_ids() } + + pub fn keyboard_navigation_width(&self) -> Option { + self.view.keyboard_navigation_width() + } + + pub fn keyboard_navigation_total(&self) -> usize { + self.view.keyboard_navigation_total() + } + + pub fn has_search_bar(&self) -> bool { + self.view.has_search_bar() + } } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 8b73b0b..a1a42f8 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -555,7 +555,11 @@ impl Application for AppModel { match key { Key::Named(Named::ArrowUp) => self.global_state.up(&self.search_results), Key::Named(Named::ArrowDown) => self.global_state.down(&self.search_results), + Key::Named(Named::ArrowLeft) => self.global_state.left(&self.search_results), + Key::Named(Named::ArrowRight) => self.global_state.right(&self.search_results), Key::Named(Named::Escape) => self.global_state.back(), + Key::Named(Named::Tab) if !modifiers.shift() => self.global_state.next(), + Key::Named(Named::Tab) if modifiers.shift() => self.global_state.previous(), Key::Named(Named::Enter) => { if modifiers.logo() || modifiers.alt() || modifiers.control() { Command::none() // to avoid not wanted "enter" presses diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs index ca0dea7..3301186 100644 --- a/rust/client/src/ui/scroll_handle.rs +++ b/rust/client/src/ui/scroll_handle.rs @@ -42,16 +42,17 @@ impl ScrollHandle { } } - pub fn focus_next(&mut self, total_item_amount: usize) -> Command { - self.offset = if self.offset < 8 { + pub fn focus_next_row(&mut self, total_items: usize, total_columns: usize) -> Command { + self.offset = if self.offset < 7 { self.offset + 1 } else { - 8 + 7 }; match self.index.as_mut() { None => { - if total_item_amount > 0 { + // focus first + if total_items > 0 { self.index = Some(0); self.scroll_to(0) @@ -60,8 +61,10 @@ impl ScrollHandle { } } Some(index) => { - if *index < total_item_amount - 1 { - *index = *index + 1; + // focus next row only if there is an item on it + let new_index = *index + total_columns; + if new_index < total_items { + *index = new_index; let index = *index; @@ -73,7 +76,7 @@ impl ScrollHandle { } } - pub fn focus_previous(&mut self) -> Command { + pub fn focus_previous_row(&mut self, total_columns: usize) -> Command { self.offset = if self.offset > 1 { self.offset - 1 } else { @@ -83,13 +86,49 @@ impl ScrollHandle { match self.index.as_mut() { None => Command::none(), Some(index) => { - if *index > 0 { - *index = *index - 1; + match index.checked_sub(total_columns) { // basically a check if result is >= 0 + Some(new_index) => { + *index = new_index; - let index = *index; + let index = *index; - self.scroll_to(index) + self.scroll_to(index) + } + None => Command::none() + } + } + } + } + + pub fn focus_next_column(&mut self, total_items: usize, total_columns: usize) -> Command { + match self.index.as_mut() { + None => Command::none(), + Some(index) => { + // put focus on next column only if it doesn't put it on next row + // and if there is an item there + let new_index = *index + 1; + if *index % total_columns != 0 && new_index <= total_items { + *index = new_index; + } + + Command::none() + } + } + } + + pub fn focus_previous_column(&mut self, total_columns: usize) -> Command { + match self.index.as_mut() { + None => Command::none(), + Some(index) => { + // put focus on previous column only if it doesn't put it on previous row + if *index == 0 { + Command::none() } else { + let new_index = *index - 1; + if new_index % total_columns != 0 { + *index = new_index; + } + Command::none() } } diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 56458ae..fde8f71 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -7,7 +7,7 @@ use crate::ui::scroll_handle::ScrollHandle; pub use crate::ui::state::main_view::MainViewState; pub use crate::ui::state::plugin_view::PluginViewState; use crate::ui::AppMsg; -use common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult}; +use common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult, UiWidgetId}; use iced::widget::text_input; use iced::widget::text_input::focus; use iced::Command; @@ -31,7 +31,13 @@ pub enum GlobalState { error_view: ErrorViewData, }, PluginView { + // logic client_context: Arc>, + + // ephemeral state + focused_item: ScrollHandle, + + // state plugin_view_data: PluginViewData, sub_state: PluginViewState, }, @@ -84,6 +90,7 @@ impl GlobalState { pub fn new_plugin(plugin_view_data: PluginViewData, client_context: Arc>) -> GlobalState { GlobalState::PluginView { client_context, + focused_item: ScrollHandle::new(false), // TODO first focused? plugin_view_data, sub_state: PluginViewState::new(), } @@ -101,19 +108,13 @@ impl GlobalState { } pub fn error(prev_global_state: &mut GlobalState, error_view_data: ErrorViewData) -> Command { - *prev_global_state = GlobalState::ErrorView { - error_view: error_view_data, - }; + *prev_global_state = GlobalState::new_error(error_view_data); Command::none() } pub fn plugin(prev_global_state: &mut GlobalState, plugin_view_data: PluginViewData, client_context: Arc>) -> Command { - *prev_global_state = GlobalState::PluginView { - client_context, - plugin_view_data, - sub_state: PluginViewState::new(), - }; + *prev_global_state = GlobalState::new_plugin(plugin_view_data, client_context); Command::none() } @@ -262,7 +263,8 @@ impl Focus for GlobalState { .. }, sub_state, - client_context + client_context, + .. } => { match sub_state { PluginViewState::None => { @@ -310,22 +312,29 @@ impl Focus for GlobalState { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { MainViewState::None => { - focused_search_result.focus_previous() + focused_search_result.focus_previous_row(1) } MainViewState::SearchResultActionPanel { focused_action_item } => { - focused_action_item.focus_previous() + focused_action_item.focus_previous_row(1) } MainViewState::InlineViewActionPanel { focused_action_item } => { - focused_action_item.focus_previous() + focused_action_item.focus_previous_row(1) } } } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { sub_state, .. } => { + GlobalState::PluginView { sub_state, client_context, focused_item, .. } => { match sub_state { - PluginViewState::None => Command::none(), + PluginViewState::None => { + let client_context = client_context.read().expect("lock is poisoned"); + + match client_context.keyboard_navigation_width() { + None => Command::none(), + Some(width) => focused_item.focus_previous_row(width) + } + }, PluginViewState::ActionPanel { focused_action_item } => { - focused_action_item.focus_previous() + focused_action_item.focus_previous_row(1) } } }, @@ -337,7 +346,7 @@ impl Focus for GlobalState { match sub_state { MainViewState::None => { if focus_list.len() != 0 { - focused_search_result.focus_next(focus_list.len()) + focused_search_result.focus_next_row(focus_list.len(), 1) } else { Command::none() } @@ -345,7 +354,7 @@ impl Focus for GlobalState { MainViewState::SearchResultActionPanel { focused_action_item } => { if let Some(search_item) = focused_search_result.get(focus_list) { if search_item.entrypoint_actions.len() != 0 { - focused_action_item.focus_next(search_item.entrypoint_actions.len() + 1) + focused_action_item.focus_next_row(search_item.entrypoint_actions.len() + 1, 1) } else { Command::none() } @@ -357,7 +366,7 @@ impl Focus for GlobalState { match inline_view_action_panel(client_context.clone()) { Some(action_panel) => { if action_panel.action_count() != 0 { - focused_action_item.focus_next(action_panel.action_count()) + focused_action_item.focus_next_row(action_panel.action_count(), 1) } else { Command::none() } @@ -368,16 +377,25 @@ impl Focus for GlobalState { } } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { sub_state, client_context, .. } => { + GlobalState::PluginView { sub_state, client_context, focused_item, .. } => { match sub_state { - PluginViewState::None => Command::none(), + PluginViewState::None => { + let client_context = client_context.read().expect("lock is poisoned"); + + let total = client_context.keyboard_navigation_total(); + + match client_context.keyboard_navigation_width() { + None => Command::none(), + Some(width) => focused_item.focus_next_row(total, width) + } + }, PluginViewState::ActionPanel { focused_action_item } => { let client_context = client_context.read().expect("lock is poisoned"); let action_ids = client_context.get_action_ids(); if action_ids.len() != 0 { - focused_action_item.focus_next(action_ids.len()) + focused_action_item.focus_next_row(action_ids.len(), 1) } else { Command::none() } @@ -388,15 +406,41 @@ impl Focus for GlobalState { } fn left(&mut self, _focus_list: &[SearchResult]) -> Command { match self { + GlobalState::PluginView { client_context, sub_state, focused_item, .. } => { + match sub_state { + PluginViewState::None => { + let client_context = client_context.read().expect("lock is poisoned"); + + match client_context.keyboard_navigation_width() { + None => Command::none(), + Some(width) => focused_item.focus_previous_column(width) + } + } + PluginViewState::ActionPanel { .. } => Command::none() + } + }, GlobalState::MainView { .. } => Command::none(), - GlobalState::PluginView { .. } => Command::none(), GlobalState::ErrorView { .. } => Command::none(), } } fn right(&mut self, _focus_list: &[SearchResult]) -> Command { match self { + GlobalState::PluginView { client_context, sub_state, focused_item, .. } => { + match sub_state { + PluginViewState::None => { + let client_context = client_context.read().expect("lock is poisoned"); + + let total = client_context.keyboard_navigation_total(); + + match client_context.keyboard_navigation_width() { + None => Command::none(), + Some(width) => focused_item.focus_next_column(total, width) + } + } + PluginViewState::ActionPanel { .. } => Command::none() + } + }, GlobalState::MainView { .. } => Command::none(), - GlobalState::PluginView { .. } => Command::none(), GlobalState::ErrorView { .. } => Command::none(), } } diff --git a/rust/client/src/ui/theme/button.rs b/rust/client/src/ui/theme/button.rs index df94ce8..8ca3168 100644 --- a/rust/client/src/ui/theme/button.rs +++ b/rust/client/src/ui/theme/button.rs @@ -14,7 +14,9 @@ pub enum ButtonStyle { Action, ActionFocused, GridItem, + GridItemFocused, ListItem, + ListItemFocused, MainListItem, MainListItemFocused, MetadataLink, @@ -49,7 +51,7 @@ impl ButtonStyle { theme.padding.to_iced() }, - ButtonStyle::GridItem => { + ButtonStyle::GridItem | ButtonStyle::GridItemFocused => { let theme = &theme.grid_item; theme.padding.to_iced() @@ -59,7 +61,7 @@ impl ButtonStyle { theme.padding.to_iced() } - ButtonStyle::ListItem => { + ButtonStyle::ListItem | ButtonStyle::ListItemFocused => { let theme = &theme.list_item; theme.padding.to_iced() @@ -99,6 +101,10 @@ impl ButtonStyle { let theme = &theme.grid_item; (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } + ButtonStyle::GridItemFocused => { + let theme = &theme.grid_item; + (Some(&theme.background_color_hovered), Some(&theme.background_color_hovered), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + } ButtonStyle::Action => { let theme = &theme.action; (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) @@ -111,6 +117,10 @@ impl ButtonStyle { let theme = &theme.list_item; (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } + ButtonStyle::ListItemFocused => { + let theme = &theme.list_item; + (Some(&theme.background_color_hovered), Some(&theme.background_color_hovered), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + } ButtonStyle::MainListItem => { let theme = &theme.main_list_item; (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 1f0a816..8a44140 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -267,6 +267,46 @@ impl ComponentWidgetWrapper { .collect() } + pub fn keyboard_navigation_width(&self) -> Option { + self.get_all_widgets() + .into_iter() + .find_map(|component| { + let (widget, _) = &*component.get(); + + match widget { + ComponentWidget::Grid { columns, .. } => Some(grid_width(columns)), + ComponentWidget::List { .. } => Some(1), + _ => None + } + }) + } + + pub fn keyboard_navigation_total(&self) -> usize { + self.get_all_widgets() + .into_iter() + .filter(|component| { + let (widget, _) = &*component.get(); + + // TODO this will produce incorrect results if grid uses item list and vice versa + matches!(widget, ComponentWidget::GridItem { .. } | ComponentWidget::ListItem { .. }) + }) + .count() + } + + pub fn has_search_bar(&self) -> bool { + self.get_all_widgets() + .into_iter() + .find(|component| { + let (widget, _) = &*component.get(); + + match widget { + ComponentWidget::SearchBar { .. } => true, + _ => false + } + }) + .is_some() + } + fn get(&self) -> RwLockReadGuard<'_, (ComponentWidget, ComponentWidgetState)> { self.inner.read().expect("lock is poisoned") } @@ -1000,10 +1040,16 @@ impl ComponentWidgetWrapper { .align_items(Alignment::Center) .into(); + let style = if false { + ButtonStyle::ListItemFocused + } else { + ButtonStyle::ListItem + }; + button(content) .on_press(ComponentWidgetEvent::ListItemClick { widget_id }) .width(Length::Fill) - .themed(ButtonStyle::ListItem) + .themed(style) } ComponentWidget::ListSection { children, title, subtitle } => { let content = render_children(children, context); @@ -1109,10 +1155,16 @@ impl ComponentWidgetWrapper { .height(130) // TODO dynamic height .into(); + let style = if false { + ButtonStyle::GridItemFocused + } else { + ButtonStyle::GridItem + }; + let content: Element<_> = button(content) .on_press(ComponentWidgetEvent::GridItemClick { widget_id }) .width(Length::Fill) - .themed(ButtonStyle::GridItem); + .themed(style); let mut sub_content_left = vec![]; @@ -1289,6 +1341,10 @@ fn render_metadata_item<'a>(label: &str, value: Element<'a, ComponentWidgetEvent .into() } +fn grid_width(columns: &Option) -> usize { + columns.map(|value| value.trunc() as usize).unwrap_or(5) +} + fn render_grid<'a>(children: &[ComponentWidgetWrapper], /*aspect_ratio: Option<&str>,*/ columns: &Option, context: ComponentRenderContext) -> Element<'a, ComponentWidgetEvent> { // let (width, height) = match aspect_ratio { // None => (1, 1), @@ -1302,15 +1358,15 @@ fn render_grid<'a>(children: &[ComponentWidgetWrapper], /*aspect_ratio: Option<& // Some(value) => panic!("unsupported aspect_ratio {:?}", value) // }; - let row_length = columns.map(|value| value.trunc() as usize).unwrap_or(5); + let grid_width = grid_width(columns); let rows: Vec> = render_children(children, context) .into_iter() - .chunks(row_length) + .chunks(grid_width) .into_iter() .map(|row_items| { let mut row_items: Vec<_> = row_items.collect(); - row_items.resize_with(row_length, || horizontal_space().into()); + row_items.resize_with(grid_width, || horizontal_space().into()); grid_row(row_items).into() }) diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 00ddc01..8e26131 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -91,4 +91,15 @@ impl PluginWidgetContainer { pub fn get_action_ids(&self) -> Vec { self.root_widget.get_action_ids() } + pub fn keyboard_navigation_width(&self) -> Option { + self.root_widget.keyboard_navigation_width() + } + + pub fn keyboard_navigation_total(&self) -> usize { + self.root_widget.keyboard_navigation_total() + } + + pub fn has_search_bar(&self) -> bool { + self.root_widget.has_search_bar() + } } From b415c905195bf6a2e43285515bf3898707e6b153 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 3 Nov 2024 11:19:47 +0100 Subject: [PATCH 154/540] Add clarification about binary location on macOS to theme docs --- docs/THEME.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/THEME.md b/docs/THEME.md index eb50c6c..3d61706 100644 --- a/docs/THEME.md +++ b/docs/THEME.md @@ -20,13 +20,20 @@ Theming is only applied to main window and doesn't affect settings ### Creating a custom theme -Gauntlet provides 2 CLI commands to generate sample. Sample is just a default theme that has been saved to file. -- `gauntlet generate-sample-color-theme` -- `gauntlet generate-sample-theme` +Gauntlet provides 2 CLI commands to generate sample: `generate-sample-color-theme` and `generate-sample-color-theme`. Sample is just a default theme that has been saved to file. Running the command will create sample file, print location of that sample file and will print location to which theme file will need to be saved to be detected by application Currently, theme change is only applied after application restart -Any errors in theme parsing will be shown in application logs \ No newline at end of file +Any errors in theme parsing will be shown in application logs + +#### Linux +- `gauntlet generate-sample-color-theme` +- `gauntlet generate-sample-theme` + +#### macOS +Note: the binary is not on the PATH +- `/Applications/Gauntlet.app/Contents/MacOS/Gauntlet generate-sample-color-theme` +- `/Applications/Gauntlet.app/Contents/MacOS/Gauntlet generate-sample-theme` From 8e0ec1d7b84569a8c57eb95527b61303288208e7 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 3 Nov 2024 14:15:48 +0100 Subject: [PATCH 155/540] Rework command generators to allow updating list of generated entrypoints while plugin is running --- .npmrc | 1 + Cargo.lock | 137 +++++++----------- bundled_plugins/gauntlet/gauntlet.toml | 7 + bundled_plugins/gauntlet/package.json | 6 +- bundled_plugins/gauntlet/src/applications.ts | 120 ++++++++++++--- js/api/src/helpers.ts | 6 +- js/core/src/command-generator.ts | 103 +++++++++---- js/core/src/init.tsx | 6 +- js/typings/index.d.ts | 1 + package-lock.json | 24 ++- rust/server/Cargo.toml | 2 +- rust/server/src/plugins/applications/mod.rs | 37 ----- rust/server/src/plugins/applications/other.rs | 5 - .../src/plugins/js/command_generators.rs | 7 +- rust/server/src/plugins/js/mod.rs | 14 +- .../src/plugins/js/plugins/applications.rs | 83 +++++++++-- .../{ => js/plugins}/applications/linux.rs | 121 +++++++--------- .../{ => js/plugins}/applications/macos.rs | 0 rust/server/src/plugins/mod.rs | 1 - rust/server/src/search.rs | 30 ++-- 20 files changed, 422 insertions(+), 289 deletions(-) create mode 100644 .npmrc delete mode 100644 rust/server/src/plugins/applications/mod.rs delete mode 100644 rust/server/src/plugins/applications/other.rs rename rust/server/src/plugins/{ => js/plugins}/applications/linux.rs (60%) rename rust/server/src/plugins/{ => js/plugins}/applications/macos.rs (100%) diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..41583e3 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/Cargo.lock b/Cargo.lock index 8370229..b0cba24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -678,9 +678,9 @@ dependencies = [ [[package]] name = "bitpacking" -version = "0.8.4" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" dependencies = [ "crunchy", ] @@ -2938,17 +2938,6 @@ dependencies = [ "zune-inflate", ] -[[package]] -name = "fail" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5e43d0f78a42ad591453aedb1d7ae631ce7ee445c7643691055a9ed8d3b01c" -dependencies = [ - "log", - "once_cell", - "rand", -] - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -3253,12 +3242,12 @@ dependencies = [ [[package]] name = "fs4" -version = "0.6.6" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" +checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" dependencies = [ "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3765,7 +3754,7 @@ checksum = "6a62d0338e4056db6a73221c2fb2e30619452f6ea9651bac4110f51b0f7a7581" dependencies = [ "cosmic-text", "etagere", - "lru 0.12.3", + "lru", "wgpu", ] @@ -3951,15 +3940,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.14.3" @@ -5196,15 +5176,6 @@ dependencies = [ "imgref", ] -[[package]] -name = "lru" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718e8fae447df0c7e1ba7f5189829e63fd536945c8988d61444c19039f16b670" -dependencies = [ - "hashbrown 0.13.2", -] - [[package]] name = "lru" version = "0.12.3" @@ -5225,9 +5196,9 @@ dependencies = [ [[package]] name = "lz4_flex" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b8c72594ac26bfd34f2d99dfced2edfaddfe8a476e3ff2ca0eb293d925c4f83" +checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" [[package]] name = "malloc_buf" @@ -5338,15 +5309,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memmap2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" -dependencies = [ - "libc", -] - [[package]] name = "memmap2" version = "0.8.0" @@ -6196,9 +6158,9 @@ dependencies = [ [[package]] name = "ownedbytes" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c718e498b20704d5fb5d51d07f414a22f61c19254c1708e117b93fd76860739c" +checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558" dependencies = [ "stable_deref_trait", ] @@ -7089,6 +7051,16 @@ dependencies = [ "getrandom 0.2.14", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "range-alloc" version = "0.1.3" @@ -9322,32 +9294,30 @@ dependencies = [ [[package]] name = "tantivy" -version = "0.20.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec540e9cebc88f523f67f596dee213e491f0c55961de013566f267a0c31f5e9" +checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856" dependencies = [ "aho-corasick", "arc-swap", - "async-trait", - "base64 0.21.7", + "base64 0.22.0", "bitpacking", "byteorder", "census", "crc32fast", "crossbeam-channel", "downcast-rs", - "fail", "fastdivide", + "fnv", "fs4", "htmlescape", - "itertools 0.10.5", + "itertools 0.12.1", "levenshtein_automata", "log", - "lru 0.10.1", + "lru", "lz4_flex", "measure_time", - "memmap2 0.6.2", - "murmurhash32", + "memmap2 0.9.4", "num_cpus", "once_cell", "oneshot", @@ -9375,22 +9345,22 @@ dependencies = [ [[package]] name = "tantivy-bitpacker" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099e96f0ede682084469b80d6909dc170aa2b11d2a45538b5b36b2a90090b9" +checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df" dependencies = [ "bitpacking", ] [[package]] name = "tantivy-columnar" -version = "0.1.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e32b024b26eab93eb8648faf08004356bf9d47376557ee4409f4b210163656" +checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e" dependencies = [ + "downcast-rs", "fastdivide", - "fnv", - "itertools 0.10.5", + "itertools 0.12.1", "serde", "tantivy-bitpacker", "tantivy-common", @@ -9400,9 +9370,9 @@ dependencies = [ [[package]] name = "tantivy-common" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7d12fdd6ec0f7e0962f129c03c696a85ec567734950cbb2b89af4a293ce342f" +checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4" dependencies = [ "async-trait", "byteorder", @@ -9413,32 +9383,31 @@ dependencies = [ [[package]] name = "tantivy-fst" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" +checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" dependencies = [ "byteorder", - "regex-syntax 0.6.29", + "regex-syntax 0.8.5", "utf8-ranges", ] [[package]] name = "tantivy-query-grammar" -version = "0.20.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106d8f78ad1da4f0fdd526a0760c326c0573510d4dedabeb1962d35a35879797" +checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82" dependencies = [ - "combine", - "once_cell", - "regex", + "nom", ] [[package]] name = "tantivy-sstable" -version = "0.1.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda34243d3ee64bd8f9ba74a3b0d05f4d07beff7767a727212e9b5a19c13dde7" +checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e" dependencies = [ + "tantivy-bitpacker", "tantivy-common", "tantivy-fst", "zstd", @@ -9446,19 +9415,20 @@ dependencies = [ [[package]] name = "tantivy-stacker" -version = "0.1.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b9e9470301b026ad3b95f79a791a2a3ee81f3ab16fbe412a9dd81ff834acf5" +checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8" dependencies = [ "murmurhash32", + "rand_distr", "tantivy-common", ] [[package]] name = "tantivy-tokenizer-api" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64186801b6e06b3a1c4275e23b517835ff4ecbb707318b838dc9de457c062200" +checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04" dependencies = [ "serde", ] @@ -11581,20 +11551,19 @@ dependencies = [ [[package]] name = "zstd" -version = "0.12.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "6.0.6" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" dependencies = [ - "libc", "zstd-sys", ] diff --git a/bundled_plugins/gauntlet/gauntlet.toml b/bundled_plugins/gauntlet/gauntlet.toml index 734f0f7..035b002 100644 --- a/bundled_plugins/gauntlet/gauntlet.toml +++ b/bundled_plugins/gauntlet/gauntlet.toml @@ -27,6 +27,13 @@ description = 'Calculator right under search bar' main_search_bar = ["read"] clipboard = ["write"] +[permissions.filesystem] +read = [ + # technically only uses locations defined by XDG Desktop Entry Specification, but + # the spec allows for customization via XDG_DATA_DIRS and XDG_DATA_HOME env vars so it can be any path + "/" +] + [[supported_system]] os = 'linux' diff --git a/bundled_plugins/gauntlet/package.json b/bundled_plugins/gauntlet/package.json index e212687..58d1505 100644 --- a/bundled_plugins/gauntlet/package.json +++ b/bundled_plugins/gauntlet/package.json @@ -6,12 +6,14 @@ "dev": "gauntlet dev" }, "dependencies": { - "@project-gauntlet/api": "file:../../js/api" + "@project-gauntlet/api": "file:../../js/api", + "@std/async": "npm:@jsr/std__async@^1.0.8", + "@std/fs": "npm:@jsr/std__fs@^1.0.5" }, "devDependencies": { - "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../tools", + "@types/react": "^18.2.14", "typescript": "^5.3.3" } } diff --git a/bundled_plugins/gauntlet/src/applications.ts b/bundled_plugins/gauntlet/src/applications.ts index 3c5c260..78aab57 100644 --- a/bundled_plugins/gauntlet/src/applications.ts +++ b/bundled_plugins/gauntlet/src/applications.ts @@ -1,28 +1,108 @@ -import {GeneratedCommand} from "@project-gauntlet/api/helpers"; - -interface DesktopEntry { - name: string, - icon: ArrayBuffer | undefined, - command: string[], -} +import { GeneratedCommand, GeneratorProps } from "@project-gauntlet/api/helpers"; +import { walk } from "@std/fs/walk"; +import { debounce } from "@std/async/debounce"; // @ts-expect-error const denoCore: DenoCore = Deno[Deno.internal].core; const InternalApi: InternalApi = denoCore.ops; -interface InternalApi { - list_applications(): Promise - open_application(command: string[]): void +type DesktopApplicationData = { + name: string + icon: ArrayBuffer | undefined, } -export default async function Applications(): Promise { - return (await InternalApi.list_applications()) - .map(value => ({ - id: `${value.name}-${value.command.join("-")}`, - name: value.name, - icon: value.icon, - fn: () => { - InternalApi.open_application(value.command) - } - })); +type DesktopPathAction = DesktopPathActionAdd | DesktopPathActionRemove + +type DesktopPathActionAdd = { + type: "add", + id: string, + data: DesktopApplicationData } + +type DesktopPathActionRemove = { + type: "remove" + id: string +} + +interface InternalApi { + linux_open_application(desktop_id: string): void + linux_application_dirs(): string[] + linux_app_from_path(path: string): undefined | DesktopPathAction +} + +export default async function Applications({ add, remove }: GeneratorProps): Promise<() => void> { + const paths = InternalApi.linux_application_dirs() + .filter(path => { + try { + Deno.lstatSync(path) + return true + } catch (err) { + // most frequent error here is NotFound + return false + } + }); + + for (const path of paths) { + for await (const dirEntry of walk(path)) { + const app = InternalApi.linux_app_from_path(dirEntry.path); + if (app) { + switch (app.type) { + case "add": { + add(app.id, commandFromApplication(app.id, app.data)) + break; + } + } + } + } + } + + const watcher = Deno.watchFs(paths); + + const handle = debounce( + (event: Deno.FsEvent) => { + switch (event.kind) { + case "create": + case "modify": + case "remove": { + for (const path of event.paths) { + const app = InternalApi.linux_app_from_path(path); + if (app) { + switch (app.type) { + case "remove": { + remove(app.id) + break; + } + case "add": { + add(app.id, commandFromApplication(app.id, app.data)) + break; + } + } + } + } + } + } + }, + 200 + ); + + // noinspection ES6MissingAwait + (async () => { + for await (const event of watcher) { + handle(event) + } + })() + + return () => { + watcher.close() + } +} + +function commandFromApplication(id: string, app: DesktopApplicationData): GeneratedCommand { + return { + name: app.name, + fn: () => { + InternalApi.linux_open_application(id) + }, + icon: app.icon, // TODO lazy icons + }; +} \ No newline at end of file diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index 0f78fe1..b6567ca 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -22,7 +22,6 @@ export function showHud(display: string): void { } export interface GeneratedCommand { - id: string name: string icon?: ArrayBuffer fn: () => void @@ -35,6 +34,11 @@ export interface GeneratedCommandAction { fn: () => void } +export type GeneratorProps = { + add: (id: string, data: GeneratedCommand) => void, + remove: (id: string) => void, +}; + export const Clipboard: Clipboard = { read: async function (): Promise<{ "text/plain"?: string | undefined; "image/png"?: Blob | undefined; }> { const data = await InternalApi.clipboard_read(); diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index fe99d57..4033a52 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -1,64 +1,103 @@ +import { reloadSearchIndex } from "./search-index"; + // @ts-expect-error does typescript support such symbol declarations? const denoCore: DenoCore = Deno[Deno.internal].core; const InternalApi = denoCore.ops; interface GeneratedCommand { // TODO is it possible to import api here - id: string name: string icon?: ArrayBuffer fn: () => void actions?: GeneratedCommandAction[] } -export interface GeneratedCommandAction { +interface GeneratedCommandAction { ref?: string label: string fn: () => void } -type ProcessedGeneratedCommand = GeneratedCommand & { generatorEntrypointId: string, lookupId: string, uuid: string }; +type GeneratorProps = { + add: (id: string, data: GeneratedCommand) => void, + remove: (id: string) => void, +}; -let storedGeneratedCommands: ProcessedGeneratedCommand[] = [] +type Generator = (props: GeneratorProps) => void | (() => (void | Promise)) | Promise (void | Promise))> + +type ProcessedGeneratedCommand = { generatorEntrypointId: string, uuid: string, command: GeneratedCommand }; + +type ProcessedGeneratedCommands = { [lookupEntrypointId: string]: ProcessedGeneratedCommand }; +type GeneratorCleanups = { [generatorEntrypointId: string]: () => (void | Promise) }; + +let storedGeneratedCommands: ProcessedGeneratedCommands = {} +let generatorCleanups: GeneratorCleanups = {} export async function runCommandGenerators(): Promise { - let localGeneratedCommands: ProcessedGeneratedCommand[] = [] + for (let [generatorEntrypointId, cleanup] of Object.entries(generatorCleanups)) { + try { + await cleanup() + } catch (err) { + console.error(`Error occurred when calling cleanup function of generator entrypoint: ${generatorEntrypointId}`, err) + } + } + + storedGeneratedCommands = {} + generatorCleanups = {} const entrypointIds = await InternalApi.get_command_generator_entrypoint_ids(); for (const generatorEntrypointId of entrypointIds) { try { - const generator: () => Promise | GeneratedCommand[] = (await import(`gauntlet:entrypoint?${generatorEntrypointId}`)).default; + const generator: Generator = (await import(`gauntlet:entrypoint?${generatorEntrypointId}`)).default; - InternalApi.op_log_info("command_generator", `Running command generator for entrypoint ${generatorEntrypointId}`) + InternalApi.op_log_info("command_generator", `Running command generator entrypoint ${generatorEntrypointId}`) - const generatedCommands = (await generator()) - .map(value => { - return { - generatorEntrypointId: generatorEntrypointId, - lookupId: generatorEntrypointId + ":" + value.id, - uuid: crypto.randomUUID(), - ...value + const add = (id: string, data: GeneratedCommand) => { + InternalApi.op_log_info("command_generator", `Adding entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) + + const lookupId = generatorEntrypointId + ":" + id; + + storedGeneratedCommands[lookupId] = { + generatorEntrypointId: generatorEntrypointId, + uuid: crypto.randomUUID(), + command: data + } + + reloadSearchIndex(true) + } + const remove = (id: string) => { + InternalApi.op_log_info("command_generator", `Removing entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) + const lookupId = generatorEntrypointId + ":" + id; + + delete storedGeneratedCommands[lookupId] + + reloadSearchIndex(true) + } + + // noinspection ES6MissingAwait + (async () => { + try { + let cleanup = await generator({ add, remove }) + if (typeof cleanup === "function") { + generatorCleanups[generatorEntrypointId] = cleanup } - }); - - InternalApi.op_log_info("command_generator", `Finished running command generator for entrypoint ${generatorEntrypointId}, amount: ${generatedCommands.length}`) - - localGeneratedCommands.push(...generatedCommands) + } catch (e) { + console.error(`Error occurred when calling command generator for entrypoint: ${generatorEntrypointId}`, e) + } + })() } catch (e) { - console.error("Error occurred when calling command generator for entrypoint: " + generatorEntrypointId, e) + console.error(`Error occurred when importing command generator for entrypoint: ${generatorEntrypointId}`, e) } } - - storedGeneratedCommands = localGeneratedCommands } export function generatedCommandSearchIndex(): AdditionalSearchItem[] { - return storedGeneratedCommands.map(value => ({ + return Object.entries(storedGeneratedCommands).map(([entrypointLookupId, value]) => ({ generator_entrypoint_id: value.generatorEntrypointId, - entrypoint_id: value.lookupId, + entrypoint_id: entrypointLookupId, entrypoint_uuid: value.uuid, - entrypoint_name: value.name, - entrypoint_icon: value.icon, - entrypoint_actions: (value.actions || []) + entrypoint_name: value.command.name, + entrypoint_icon: value.command.icon, + entrypoint_actions: (value.command.actions || []) .map(action => ({ id: action.ref, label: action.label @@ -67,12 +106,12 @@ export function generatedCommandSearchIndex(): AdditionalSearchItem[] { } export async function runGeneratedCommandAction(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean) { - const command = storedGeneratedCommands.find(value => value.lookupId == entrypointId); + const command = storedGeneratedCommands[entrypointId]; if (command) { const id = await InternalApi.fetch_action_id_for_shortcut(command.generatorEntrypointId, key, modifierShift, modifierControl, modifierAlt, modifierMeta); if (id) { - const action = command.actions?.find(value => value.ref == id); + const action = command.command.actions?.find(value => value.ref == id); if (action) { action.fn() } @@ -81,18 +120,18 @@ export async function runGeneratedCommandAction(entrypointId: string, key: strin } export function runGeneratedCommand(entrypointId: string, action_index: number | undefined) { - const generatedCommand = storedGeneratedCommands.find(value => value.lookupId === entrypointId); + const generatedCommand = storedGeneratedCommands[entrypointId]; if (generatedCommand) { if (typeof action_index == "number") { - const actions = generatedCommand.actions; + const actions = generatedCommand.command.actions; if (actions) { actions[action_index].fn() } else { throw new Error("Generated command with entrypoint id '" + entrypointId + "' doesn't have actions, action index: " + action_index) } } else { - generatedCommand.fn() + generatedCommand.command.fn() } } else { throw new Error("Generated command with entrypoint id '" + entrypointId + "' not found") diff --git a/js/core/src/init.tsx b/js/core/src/init.tsx index 71de6ee..9300b17 100644 --- a/js/core/src/init.tsx +++ b/js/core/src/init.tsx @@ -224,7 +224,6 @@ async function runLoop() { } case "ReloadSearchIndex": { runCommandGenerators() - .then(() => reloadSearchIndex(false)); break; } case "RefreshSearchIndex": { @@ -240,8 +239,9 @@ denoCore.setPromiseRejectCallback((_type, _promise, reason) => { console.error("Rejected promise", reason) }) -runCommandGenerators() - .then(() => reloadSearchIndex(true)); +reloadSearchIndex(true) + +runCommandGenerators(); (async () => { await runLoop() diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 895cd1b..cb6543d 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -110,6 +110,7 @@ interface InternalApi { clear_inline_view(): void; get_command_generator_entrypoint_ids(): Promise + get_plugin_preferences(): Record; get_entrypoint_preferences(entrypointId: string): Record; plugin_preferences_required(): Promise; diff --git a/package-lock.json b/package-lock.json index 59cae4d..438baee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,9 @@ "bundled_plugins/gauntlet": { "name": "@project-gauntlet/bundled-plugin", "dependencies": { - "@project-gauntlet/api": "file:../../js/api" + "@project-gauntlet/api": "file:../../js/api", + "@std/async": "npm:@jsr/std__async@^1.0.8", + "@std/fs": "npm:@jsr/std__fs@^1.0.5" }, "devDependencies": { "@project-gauntlet/deno": "file:../../js/deno", @@ -313,6 +315,11 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@jsr/std__path": { + "version": "1.0.8", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__path/1.0.8.tgz", + "integrity": "sha512-eNBGlh/8ZVkMxtFH4bwIzlAeKoHYk5in4wrBZhi20zMdOiuX4QozP4+19mIXBT2lzHDjhuVLyECbhFeR304iDg==" + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -1441,6 +1448,21 @@ "win32" ] }, + "node_modules/@std/async": { + "name": "@jsr/std__async", + "version": "1.0.8", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__async/1.0.8.tgz", + "integrity": "sha512-veKSzQ5lY6uJL60IxPFD2P1uVssdOtb8LmApllO2HzwzgY3ZDsjD4ZIaDjUykXwBNfUY2tHAlE7d62OGsbzdAg==" + }, + "node_modules/@std/fs": { + "name": "@jsr/std__fs", + "version": "1.0.5", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.5.tgz", + "integrity": "sha512-2ihx5BjO2IxpJ1aHy+joER4l1tJSktyaNaoDb9HOVK5IRToUY5OwstLe3+yhZnSn2KXlCo5DBS1mfAgrtu10aw==", + "dependencies": { + "@jsr/std__path": "^1.0.7" + } + }, "node_modules/@types/aws-lambda": { "version": "8.10.138", "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.138.tgz", diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 32a4033..eee1cad 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -9,7 +9,7 @@ deno_runtime = { version = "0.126.0" } tokio = "1.28.1" tokio-util = "0.7.11" toml = "0.8.10" -tantivy = "0.20.2" +tantivy = "0.22.0" zstd-sys = "=2.0.9" # TODO REMOVE https://github.com/gyscos/zstd-rs/issues/270 regex = "1.9.3" once_cell = "1.18.0" diff --git a/rust/server/src/plugins/applications/mod.rs b/rust/server/src/plugins/applications/mod.rs deleted file mode 100644 index bba4251..0000000 --- a/rust/server/src/plugins/applications/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -use image::ImageFormat; -use image::imageops::FilterType; - -#[cfg(target_os = "linux")] -mod linux; -#[cfg(target_os = "linux")] -pub use linux::get_apps; - -#[cfg(target_os = "macos")] -mod macos; -#[cfg(target_os = "macos")] -pub use macos::get_apps; - -#[cfg(all(not(target_os = "linux"), not(target_os = "macos")))] -mod other; -#[cfg(all(not(target_os = "linux"), not(target_os = "macos")))] -pub use other::get_apps; - -use serde::Serialize; - -#[derive(Debug, Serialize)] -pub struct DesktopEntry { - pub name: String, - pub icon: Option>, - pub command: Vec, -} - -pub(in crate::plugins::applications) fn resize_icon(data: Vec) -> anyhow::Result> { - let data = image::load_from_memory_with_format(&data, ImageFormat::Png)?; - let data = image::imageops::resize(&data, 48, 48, FilterType::Lanczos3); - - let mut buffer = std::io::Cursor::new(vec![]); - - data.write_to(&mut buffer, ImageFormat::Png)?; - - Ok(buffer.into_inner()) -} \ No newline at end of file diff --git a/rust/server/src/plugins/applications/other.rs b/rust/server/src/plugins/applications/other.rs deleted file mode 100644 index dea8800..0000000 --- a/rust/server/src/plugins/applications/other.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::plugins::applications::DesktopEntry; - -pub fn get_apps() -> Vec { - vec![] -} \ No newline at end of file diff --git a/rust/server/src/plugins/js/command_generators.rs b/rust/server/src/plugins/js/command_generators.rs index 56645ef..43a0f07 100644 --- a/rust/server/src/plugins/js/command_generators.rs +++ b/rust/server/src/plugins/js/command_generators.rs @@ -1,8 +1,9 @@ +use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginEntrypointType}; +use crate::plugins::js::PluginData; +use deno_core::{op, OpState}; use std::cell::RefCell; use std::rc::Rc; -use deno_core::{op, OpState}; -use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginEntrypointType}; -use crate::plugins::js::PluginData; + #[op] async fn get_command_generator_entrypoint_ids(state: Rc>) -> anyhow::Result> { diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 1aa246b..541e999 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -14,12 +14,12 @@ use deno_core::futures::executor::block_on; use deno_core::futures::{FutureExt, Stream, StreamExt}; use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; use deno_core::{futures, op, serde_v8, v8, FastString, ModuleLoader, ModuleSource, ModuleSourceFuture, ModuleType, OpState, ResolutionKind, StaticModuleLoader}; -use deno_runtime::BootstrapOptions; use deno_runtime::deno_core::ModuleSpecifier; use deno_runtime::deno_io::{Stdio, StdioPipe}; use deno_runtime::permissions::{Descriptor, EnvDescriptor, NetDescriptor, Permissions, PermissionsContainer, PermissionsOptions, ReadDescriptor, UnaryPermission, WriteDescriptor}; use deno_runtime::worker::MainWorker; use deno_runtime::worker::WorkerOptions; +use deno_runtime::BootstrapOptions; use indexmap::IndexMap; use once_cell::sync::Lazy; use regex::Regex; @@ -32,8 +32,7 @@ use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, Se use common::rpc::frontend_api::FrontendApi; use component_model::{create_component_model, Children, Component, Property, PropertyType, SharedType}; -use crate::model::{IntermediateUiEvent, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData, JsUiWidget, JsKeyboardEventOrigin, PreferenceUserData}; -use crate::plugins::applications::{get_apps, DesktopEntry}; +use crate::model::{IntermediateUiEvent, JsKeyboardEventOrigin, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData, JsUiWidget, PreferenceUserData}; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; @@ -41,7 +40,6 @@ use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_r use crate::plugins::js::command_generators::get_command_generator_entrypoint_ids; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; use crate::plugins::js::permissions::{permissions_to_deno, PluginPermissions, PluginPermissionsClipboard}; -use crate::plugins::js::plugins::applications::{list_applications, open_application}; use crate::plugins::js::plugins::numbat::{run_numbat, NumbatContext}; use crate::plugins::js::plugins::settings::open_settings; use crate::plugins::js::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; @@ -532,8 +530,12 @@ deno_core::extension!( run_numbat, // plugins applications - list_applications, - open_application, + #[cfg(target_os = "linux")] + crate::plugins::js::plugins::applications::linux_app_from_path, + #[cfg(target_os = "linux")] + crate::plugins::js::plugins::applications::linux_application_dirs, + #[cfg(target_os = "linux")] + crate::plugins::js::plugins::applications::linux_open_application, // plugins settings open_settings, diff --git a/rust/server/src/plugins/js/plugins/applications.rs b/rust/server/src/plugins/js/plugins/applications.rs index a47de89..ff7876c 100644 --- a/rust/server/src/plugins/js/plugins/applications.rs +++ b/rust/server/src/plugins/js/plugins/applications.rs @@ -1,24 +1,76 @@ use deno_core::op; -use tokio::task::spawn_blocking; -use crate::plugins::applications::{DesktopEntry, get_apps}; +use std::path::PathBuf; +use image::ImageFormat; +use image::imageops::FilterType; +use serde::Serialize; -#[op] -async fn list_applications() -> anyhow::Result> { - Ok(spawn_blocking(|| get_apps()).await?) +#[cfg(target_os = "linux")] +mod linux; + +// TODO macos +// #[cfg(target_os = "macos")] +// mod macos; + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +pub enum DesktopPathAction { + #[serde(rename = "add")] + Add { + id: String, + data: DesktopApplication + }, + #[serde(rename = "remove")] + Remove { + id: String + } } +#[cfg(target_os = "linux")] +#[derive(Debug, Serialize)] +pub struct DesktopApplication { + name: String, + icon: Option>, +} + +#[cfg(target_os = "macos")] +#[derive(Debug, Serialize)] +pub struct DesktopApplication { + name: String, + icon: Option>, +} + +#[cfg(all(not(target_os = "linux"), not(target_os = "macos")))] +#[derive(Debug, Serialize)] +pub struct DesktopApplication { + +} + +#[cfg(target_os = "linux")] #[op] -fn open_application(command: Vec) -> anyhow::Result<()> { - let path = &command[0]; - let args = &command[1..]; +pub fn linux_app_from_path(path: String) -> Option { + linux::linux_app_from_path(PathBuf::from(path)) +} + +#[cfg(target_os = "linux")] +#[op] +pub fn linux_application_dirs() -> Vec { + linux::linux_application_dirs() + .into_iter() + .map(|path| path.to_str().expect("non-utf8 paths are not supported").to_string()) + .collect() +} + +#[cfg(target_os = "linux")] +#[op] +pub fn linux_open_application(desktop_file_id: String) -> anyhow::Result<()> { #[cfg(not(windows))] - spawn_detached(path, args)?; + spawn_detached("gtk-launch", &[desktop_file_id])?; Ok(()) } -#[cfg(not(windows))] +#[cfg(unix)] pub fn spawn_detached( path: &str, args: I, @@ -58,4 +110,15 @@ where .wait() .map(|_| ()) } +} + +pub(in crate::plugins::js::plugins::applications) fn resize_icon(data: Vec) -> anyhow::Result> { + let data = image::load_from_memory_with_format(&data, ImageFormat::Png)?; + let data = image::imageops::resize(&data, 48, 48, FilterType::Lanczos3); + + let mut buffer = std::io::Cursor::new(vec![]); + + data.write_to(&mut buffer, ImageFormat::Png)?; + + Ok(buffer.into_inner()) } \ No newline at end of file diff --git a/rust/server/src/plugins/applications/linux.rs b/rust/server/src/plugins/js/plugins/applications/linux.rs similarity index 60% rename from rust/server/src/plugins/applications/linux.rs rename to rust/server/src/plugins/js/plugins/applications/linux.rs index 5fe02f2..3b9f01d 100644 --- a/rust/server/src/plugins/applications/linux.rs +++ b/rust/server/src/plugins/js/plugins/applications/linux.rs @@ -1,19 +1,17 @@ -use std::collections::hash_map::Entry::Vacant; -use std::collections::HashMap; -use std::{env, fs}; +use std::collections::{HashMap, HashSet}; use std::fs::Metadata; -use std::path::{PathBuf}; +use std::path::{Path, PathBuf}; +use std::{env, fs}; +use crate::plugins::js::plugins::applications::{resize_icon, DesktopApplication, DesktopPathAction}; +use common::dirs::Dirs; use freedesktop_entry_parser::parse_entry; use freedesktop_icons::lookup; -use image::ImageFormat; use image::imageops::FilterType; -use serde::Serialize; +use image::ImageFormat; use walkdir::WalkDir; -use common::dirs::Dirs; -use crate::plugins::applications::{DesktopEntry, resize_icon}; -fn find_application_dirs() -> Option> { +pub fn linux_application_dirs() -> Vec { let data_home = match env::var_os("XDG_DATA_HOME") { Some(val) => { PathBuf::from(val) @@ -50,79 +48,65 @@ fn find_application_dirs() -> Option> { res.push(flatpak); res.append(&mut extra_data_dirs); - let res = res.into_iter() + res.into_iter() .map(|d| d.join("applications")) - .collect(); - - Some(res) + .collect() } -pub fn get_apps() -> Vec { - let app_dirs = find_application_dirs() - .unwrap_or_default() +pub fn linux_app_from_path(path: PathBuf) -> Option { + let app_directories = linux_application_dirs(); + + let relative_to_app_dir = app_directories .into_iter() - .filter(|dir| dir.exists()) - .collect::>(); + .find_map(|app_path| path.strip_prefix(app_path).ok()); - let mut result: HashMap = HashMap::new(); + let Some(relative_to_app_dir) = relative_to_app_dir else { + return None; + }; - for app_dir in app_dirs { - let found_desktop_entries = WalkDir::new(app_dir.clone()) - .into_iter() - .filter_map(|dir_entry| dir_entry.ok()) - .filter_map(|path| { - let path = path.path(); + let Some(relative_to_app_dir) = relative_to_app_dir.to_str() else { + return None; + }; - tracing::debug!("Found application at: {:?}", path); + let Some(extension) = path.extension() else { + return None; + }; - // follows symlinks needed for flatpak - let Ok(metadata) = fs::metadata(path) else { - return None; - }; + let Some("desktop") = extension.to_str() else { + return None; + }; - if !metadata.is_file() { - return None; - } + let desktop_file_id = relative_to_app_dir.replace("/", "-"); - let Some(extension) = path.extension() else { - return None; - }; + if !path.exists() { + tracing::debug!("Removing application at: {:?}", path); + Some(DesktopPathAction::Remove { id: desktop_file_id }) + } else { + // follows symlinks needed for flatpak + let Ok(metadata) = fs::metadata(&path) else { + return None; + }; - match extension.to_str() { - Some("desktop") => { - let desktop_id = path.strip_prefix(&app_dir) - .ok()? - .to_str()? - .to_owned(); + if !metadata.is_file() { + return None; + } - let entry = create_app_entry(path.to_path_buf())?; + if let Some(entry) = create_app_entry(&path) { + tracing::debug!("Adding application at: {:?}", path); - Some((desktop_id, entry)) - }, - _ => None, - } + Some(DesktopPathAction::Add { + id: desktop_file_id, + data: entry }) - .collect::>(); - - for (path, desktop_entry) in found_desktop_entries { - if let Vacant(entry) = result.entry(path) { - entry.insert(desktop_entry); - } + } else { + None } } - - result.into_values().collect() } -fn create_app_entry(path: PathBuf) -> Option { - let desktop_filename = path.file_name() - .expect("desktop file doesn't have filename") - .to_os_string() - .into_string() - .ok()?; - - let entry = parse_entry(&path) - .inspect_err(|err| tracing::warn!("error parsing .desktop file at path {:?}: {:?}", &path, err)) +fn create_app_entry(desktop_file_path: &Path) -> Option { + let entry = parse_entry(desktop_file_path) + .inspect_err(|err| tracing::warn!("error parsing .desktop file at path {:?}: {:?}", desktop_file_path, err)) .ok()?; let entry = entry.section("Desktop Entry"); @@ -132,13 +116,11 @@ fn create_app_entry(path: PathBuf) -> Option { let no_display = entry.attr("NoDisplay").map(|val| val == "true").unwrap_or(false); let hidden = entry.attr("Hidden").map(|val| val == "true").unwrap_or(false); // TODO NotShowIn, OnlyShowIn https://wiki.archlinux.org/title/desktop_entries - // TODO DBusActivatable + if no_display || hidden { return None } - let command = vec!["gtk-launch".to_string(), desktop_filename]; - let icon = icon .map(|icon| { let icon_path = PathBuf::from(&icon); @@ -186,14 +168,13 @@ fn create_app_entry(path: PathBuf) -> Option { }) .map(|res| { res - .inspect_err(|err| tracing::warn!("error processing icon {:?}: {:?}", &path, err)) + .inspect_err(|err| tracing::warn!("error processing icon of {:?}: {:?}", desktop_file_path, err)) .ok() }) .flatten(); - Some(DesktopEntry { + Some(DesktopApplication { name: name.to_string(), icon, - command, }) } diff --git a/rust/server/src/plugins/applications/macos.rs b/rust/server/src/plugins/js/plugins/applications/macos.rs similarity index 100% rename from rust/server/src/plugins/applications/macos.rs rename to rust/server/src/plugins/js/plugins/applications/macos.rs diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index eb7ba4b..1215013 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -31,7 +31,6 @@ mod config_reader; mod loader; mod run_status; mod download_status; -mod applications; mod icon_cache; pub(super) mod frecency; diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index e7dade9..49a22b5 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use tantivy::{doc, Index, IndexReader, ReloadPolicy, Searcher}; +use tantivy::{doc, Index, IndexReader, IndexWriter, ReloadPolicy, Searcher}; use tantivy::collector::TopDocs; use tantivy::query::{AllQuery, BooleanQuery, FuzzyTermQuery, Query, RegexQuery, TermQuery}; use tantivy::schema::*; @@ -74,7 +74,7 @@ impl SearchIndex { let index_reader = index .reader_builder() - .reload_policy(ReloadPolicy::OnCommit) + .reload_policy(ReloadPolicy::Manual) .try_into()?; Ok(Self { @@ -95,12 +95,13 @@ impl SearchIndex { let _guard = self.index_writer_mutex.lock().expect("lock is poisoned"); let mut entrypoint_data = self.entrypoint_data.lock().expect("lock is poisoned"); - let mut index_writer = self.index.writer(5_000_000)?; + let mut index_writer = self.index.writer::(15_000_000)?; index_writer.delete_query(Box::new( TermQuery::new(Term::from_field_text(self.plugin_id, &plugin_id.to_string()), IndexRecordOption::Basic) ))?; index_writer.commit()?; + self.index_reader.reload()?; entrypoint_data.remove(&plugin_id); @@ -114,7 +115,7 @@ impl SearchIndex { let _guard = self.index_writer_mutex.lock().expect("lock is poisoned"); let mut entrypoint_data = self.entrypoint_data.lock().expect("lock is poisoned"); - let mut index_writer = self.index.writer(3_000_000)?; + let mut index_writer = self.index.writer::(15_000_000)?; index_writer.delete_query(Box::new( TermQuery::new(Term::from_field_text(self.plugin_id, &plugin_id.to_string()), IndexRecordOption::Basic) @@ -130,6 +131,7 @@ impl SearchIndex { } index_writer.commit()?; + self.index_reader.reload()?; let data = search_items.iter() .map(|item| { @@ -171,6 +173,8 @@ impl SearchIndex { } pub fn search(&self, query: &str) -> anyhow::Result> { + let entrypoint_data = self.entrypoint_data.lock().expect("lock is poisoned"); + let searcher = self.index_reader.searcher(); let query_parser = QueryParser::new( @@ -184,7 +188,7 @@ impl SearchIndex { let mut index = 0; let fetch = std::iter::from_fn(|| -> Option>> { - let result = self.fetch(&query, TopDocs::with_limit(20).and_offset(index * 20), &searcher); + let result = self.fetch(&entrypoint_data, &query, TopDocs::with_limit(20).and_offset(index * 20), &searcher); index += 1; @@ -208,22 +212,22 @@ impl SearchIndex { .flatten() .collect::>(); - result.sort_by(|(_, score_a), (_, score_b)| score_b.partial_cmp(score_a).unwrap_or(Ordering::Less)); + result.sort_by(|(_, score_a), (_, score_b)| score_b.total_cmp(score_a)); let result = result.into_iter() .map(|(item, _)| item) .collect::>(); + drop(entrypoint_data); + Ok(result) } - fn fetch(&self, query: &dyn Query, collector: TopDocs, searcher: &Searcher) -> anyhow::Result> { - let entrypoint_data = self.entrypoint_data.lock().expect("lock is poisoned"); - - let get_str_field = |retrieved_doc: &Document, field: Field| -> String { + fn fetch(&self, entrypoint_data: &HashMap>, query: &dyn Query, collector: TopDocs, searcher: &Searcher) -> anyhow::Result> { + let get_str_field = |retrieved_doc: &TantivyDocument, field: Field| -> String { retrieved_doc.get_first(field) .unwrap_or_else(|| panic!("there should be a field with name {:?}", searcher.schema().get_field_name(field))) - .as_text() + .as_str() .unwrap_or_else(|| panic!("field with name {:?} should contain string", searcher.schema().get_field_name(field))) .to_owned() }; @@ -231,7 +235,7 @@ impl SearchIndex { let result = searcher.search(query, &collector)? .into_iter() .map(|(_score, doc_address)| { - let retrieved_doc = searcher.doc(doc_address) + let retrieved_doc = searcher.doc::(doc_address) .expect("index should contain just searched results"); let entrypoint_id = EntrypointId::from_string(get_str_field(&retrieved_doc, self.entrypoint_id)); @@ -243,7 +247,7 @@ impl SearchIndex { .get(&plugin_id) .expect("Plugin should always exist in entrypoint data") .get(&entrypoint_id) - .expect("Plugin should always exist in entrypoint data"); + .expect("Entrypoint should always exist in plugin in entrypoint data"); let entrypoint_actions = entrypoint_data.actions.iter() .map(|data| SearchResultEntrypointAction { From 47547083d4d108e2b0dee50ae063f14ee5dd4926 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 3 Nov 2024 18:38:35 +0100 Subject: [PATCH 156/540] Update applications plugin for macOS after command generator rework --- bundled_plugins/gauntlet/src/applications.ts | 165 +++++-- rust/server/src/plugins/js/mod.rs | 25 ++ .../src/plugins/js/plugins/applications.rs | 116 ++++- .../plugins/js/plugins/applications/macos.rs | 425 +++++++++--------- rust/server/src/search.rs | 2 +- 5 files changed, 491 insertions(+), 242 deletions(-) diff --git a/bundled_plugins/gauntlet/src/applications.ts b/bundled_plugins/gauntlet/src/applications.ts index 78aab57..0c09f50 100644 --- a/bundled_plugins/gauntlet/src/applications.ts +++ b/bundled_plugins/gauntlet/src/applications.ts @@ -1,22 +1,28 @@ import { GeneratedCommand, GeneratorProps } from "@project-gauntlet/api/helpers"; -import { walk } from "@std/fs/walk"; +import { walk, WalkOptions } from "@std/fs/walk"; import { debounce } from "@std/async/debounce"; // @ts-expect-error const denoCore: DenoCore = Deno[Deno.internal].core; const InternalApi: InternalApi = denoCore.ops; -type DesktopApplicationData = { +type LinuxDesktopApplicationData = { name: string icon: ArrayBuffer | undefined, } -type DesktopPathAction = DesktopPathActionAdd | DesktopPathActionRemove +type MacOSDesktopApplicationData = { + name: string + path: string, + icon: ArrayBuffer | undefined, +} -type DesktopPathActionAdd = { +type DesktopPathAction = DesktopPathActionAdd | DesktopPathActionRemove + +type DesktopPathActionAdd = { type: "add", id: string, - data: DesktopApplicationData + data: DATA } type DesktopPathActionRemove = { @@ -24,14 +30,129 @@ type DesktopPathActionRemove = { id: string } -interface InternalApi { - linux_open_application(desktop_id: string): void - linux_application_dirs(): string[] - linux_app_from_path(path: string): undefined | DesktopPathAction +type MacOSDesktopSettingsPre13Data = { + name: string + path: string, + icon: ArrayBuffer | undefined, } -export default async function Applications({ add, remove }: GeneratorProps): Promise<() => void> { - const paths = InternalApi.linux_application_dirs() +type MacOSDesktopSettings13AndPostData = { + name: string + preferences_id: string + icon: ArrayBuffer | undefined, +} + +interface InternalApi { + current_os(): string + + linux_open_application(desktop_id: string): void + linux_application_dirs(): string[] + linux_app_from_path(path: string): undefined | DesktopPathAction + + macos_major_version(): number + + macos_settings_pre_13(): MacOSDesktopSettingsPre13Data[] + macos_settings_13_and_post(): MacOSDesktopSettings13AndPostData[] + macos_open_setting_13_and_post(preferences_id: String): void + macos_open_setting_pre_13(setting_path: String): void + + macos_system_applications(): string[] + macos_application_dirs(): string[] + macos_app_from_path(path: string): undefined | DesktopPathAction + macos_app_from_arbitrary_path(path: string): undefined | DesktopPathAction + macos_open_application(app_path: String): void +} + +export default async function Applications({ add, remove }: GeneratorProps): Promise void)> { + switch (InternalApi.current_os()) { + case "linux": { + return await genericGenerator( + InternalApi.linux_application_dirs(), + path => InternalApi.linux_app_from_path(path), + (id, data) => ({ + name: data.name, + fn: () => { + InternalApi.linux_open_application(id) + }, + icon: data.icon, // TODO lazy icons + }), + add, + remove + ); + } + case "macos": { + const majorVersion = InternalApi.macos_major_version(); + + if (majorVersion >= 13) { + for (const setting of InternalApi.macos_settings_13_and_post()) { + add(`settings:${setting.preferences_id}`, { + name: setting.name, + fn: () => { + InternalApi.macos_open_setting_13_and_post(setting.preferences_id) + }, + icon: setting.icon, + }) + } + } else { + for (const setting of InternalApi.macos_settings_pre_13()) { + add(`settings:${setting.path}`, { + name: setting.name, + fn: () => { + InternalApi.macos_open_setting_pre_13(setting.path) + }, + icon: setting.icon, + }) + } + } + + for (const path of InternalApi.macos_system_applications()) { + const app = InternalApi.macos_app_from_path(path) + if (app) { + switch (app.type) { + case "add": { + let data = app.data; + add(data.path, { + name: data.name, + fn: () => { + InternalApi.macos_open_application(data.path) + }, + icon: data.icon, + }) + break; + } + } + } else { + console.error(`System application '${path}' was not loaded`) + } + } + + return await genericGenerator( + InternalApi.macos_application_dirs(), + path => InternalApi.macos_app_from_arbitrary_path(path), + (_id, data) => ({ + name: data.name, + fn: () => { + InternalApi.macos_open_application(data.path) + }, + icon: data.icon, + }), + add, + remove, + { exts: ["app"], maxDepth: 2 } + ); + } + } +} + +async function genericGenerator( + directoriesToWatch: string[], + appFromPath: (path: string) => undefined | DesktopPathAction, + commandFromApp: (id: string, data: DATA) => GeneratedCommand, + add: (id: string, data: GeneratedCommand) => void, + remove: (id: string) => void, + walkOpts?: WalkOptions +): Promise<() => void> { + const paths = directoriesToWatch .filter(path => { try { Deno.lstatSync(path) @@ -43,12 +164,12 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro }); for (const path of paths) { - for await (const dirEntry of walk(path)) { - const app = InternalApi.linux_app_from_path(dirEntry.path); + for await (const dirEntry of walk(path, walkOpts)) { + const app = appFromPath(dirEntry.path); if (app) { switch (app.type) { case "add": { - add(app.id, commandFromApplication(app.id, app.data)) + add(app.id, commandFromApp(app.id, app.data)) break; } } @@ -65,7 +186,7 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro case "modify": case "remove": { for (const path of event.paths) { - const app = InternalApi.linux_app_from_path(path); + const app = appFromPath(path); if (app) { switch (app.type) { case "remove": { @@ -73,7 +194,7 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro break; } case "add": { - add(app.id, commandFromApplication(app.id, app.data)) + add(app.id, commandFromApp(app.id, app.data)) break; } } @@ -82,7 +203,7 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro } } }, - 200 + 1000 ); // noinspection ES6MissingAwait @@ -96,13 +217,3 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro watcher.close() } } - -function commandFromApplication(id: string, app: DesktopApplicationData): GeneratedCommand { - return { - name: app.name, - fn: () => { - InternalApi.linux_open_application(id) - }, - icon: app.icon, // TODO lazy icons - }; -} \ No newline at end of file diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 541e999..327c16a 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -530,6 +530,9 @@ deno_core::extension!( run_numbat, // plugins applications + crate::plugins::js::plugins::applications::current_os, + + // plugins applications linux #[cfg(target_os = "linux")] crate::plugins::js::plugins::applications::linux_app_from_path, #[cfg(target_os = "linux")] @@ -537,6 +540,28 @@ deno_core::extension!( #[cfg(target_os = "linux")] crate::plugins::js::plugins::applications::linux_open_application, + // plugins applications macos + #[cfg(target_os = "macos")] + crate::plugins::js::plugins::applications::macos_major_version, + #[cfg(target_os = "macos")] + crate::plugins::js::plugins::applications::macos_settings_pre_13, + #[cfg(target_os = "macos")] + crate::plugins::js::plugins::applications::macos_settings_13_and_post, + #[cfg(target_os = "macos")] + crate::plugins::js::plugins::applications::macos_open_setting_13_and_post, + #[cfg(target_os = "macos")] + crate::plugins::js::plugins::applications::macos_open_setting_pre_13, + #[cfg(target_os = "macos")] + crate::plugins::js::plugins::applications::macos_system_applications, + #[cfg(target_os = "macos")] + crate::plugins::js::plugins::applications::macos_application_dirs, + #[cfg(target_os = "macos")] + crate::plugins::js::plugins::applications::macos_app_from_arbitrary_path, + #[cfg(target_os = "macos")] + crate::plugins::js::plugins::applications::macos_app_from_path, + #[cfg(target_os = "macos")] + crate::plugins::js::plugins::applications::macos_open_application, + // plugins settings open_settings, ], diff --git a/rust/server/src/plugins/js/plugins/applications.rs b/rust/server/src/plugins/js/plugins/applications.rs index ff7876c..3e1cf41 100644 --- a/rust/server/src/plugins/js/plugins/applications.rs +++ b/rust/server/src/plugins/js/plugins/applications.rs @@ -7,9 +7,8 @@ use serde::Serialize; #[cfg(target_os = "linux")] mod linux; -// TODO macos -// #[cfg(target_os = "macos")] -// mod macos; +#[cfg(target_os = "macos")] +mod macos; #[derive(Debug, Serialize)] #[serde(tag = "type")] @@ -36,6 +35,7 @@ pub struct DesktopApplication { #[derive(Debug, Serialize)] pub struct DesktopApplication { name: String, + path: String, icon: Option>, } @@ -45,6 +45,28 @@ pub struct DesktopApplication { } +#[cfg(target_os = "macos")] +#[derive(Debug, Serialize)] +pub struct DesktopSettingsPre13Data { + name: String, + path: String, + icon: Option>, +} + +#[cfg(target_os = "macos")] +#[derive(Debug, Serialize)] +pub struct DesktopSettings13AndPostData { + name: String, + preferences_id: String, + icon: Option>, +} + + +#[op] +pub fn current_os() -> &'static str { + std::env::consts::OS +} + #[cfg(target_os = "linux")] #[op] pub fn linux_app_from_path(path: String) -> Option { @@ -64,12 +86,98 @@ pub fn linux_application_dirs() -> Vec { #[op] pub fn linux_open_application(desktop_file_id: String) -> anyhow::Result<()> { - #[cfg(not(windows))] spawn_detached("gtk-launch", &[desktop_file_id])?; Ok(()) } +#[cfg(target_os = "macos")] +#[op] +pub fn macos_major_version() -> u8 { + macos::macos_major_version() +} + +#[cfg(target_os = "macos")] +#[op] +pub fn macos_app_from_path(path: String) -> Option { + macos::macos_app_from_path(&PathBuf::from(path)) +} + +#[cfg(target_os = "macos")] +#[op] +pub fn macos_app_from_arbitrary_path(path: String) -> Option { + macos::macos_app_from_arbitrary_path(PathBuf::from(path)) +} + +#[cfg(target_os = "macos")] +#[op] +pub fn macos_system_applications() -> Vec { + macos::macos_system_applications() + .into_iter() + .map(|path| path.to_str().expect("non-utf8 paths are not supported").to_string()) + .collect() +} + +#[cfg(target_os = "macos")] +#[op] +pub fn macos_application_dirs() -> Vec { + macos::macos_application_dirs() + .into_iter() + .map(|path| path.to_str().expect("non-utf8 paths are not supported").to_string()) + .collect() +} + +#[cfg(target_os = "macos")] +#[op] +pub fn macos_open_application(app_path: String) -> anyhow::Result<()> { + + spawn_detached("open", &[app_path])?; + + Ok(()) +} + +#[cfg(target_os = "macos")] +#[op] +pub fn macos_settings_pre_13() -> Vec { + macos::macos_settings_pre_13() +} + +#[cfg(target_os = "macos")] +#[op] +pub fn macos_settings_13_and_post() -> Vec { + macos::macos_settings_13_and_post() +} + +#[cfg(target_os = "macos")] +#[op] +pub fn macos_open_setting_13_and_post(preferences_id: String) -> anyhow::Result<()> { + + spawn_detached( + "open", + &[ + format!("x-apple.systempreferences:{}", preferences_id) + ] + )?; + + Ok(()) +} + +#[cfg(target_os = "macos")] +#[op] +pub fn macos_open_setting_pre_13(setting_path: String) -> anyhow::Result<()> { + + spawn_detached( + "open", + &[ + "-b", + "com.apple.systempreferences", + &setting_path, + ] + )?; + + Ok(()) +} + #[cfg(unix)] pub fn spawn_detached( path: &str, diff --git a/rust/server/src/plugins/js/plugins/applications/macos.rs b/rust/server/src/plugins/js/plugins/applications/macos.rs index 2751771..ad465fb 100644 --- a/rust/server/src/plugins/js/plugins/applications/macos.rs +++ b/rust/server/src/plugins/js/plugins/applications/macos.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::error::Error; use std::ffi::OsStr; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use anyhow::{anyhow, Context}; use cacao::filesystem::{FileManager, SearchPathDirectory, SearchPathDomainMask}; @@ -12,91 +12,10 @@ use objc2_foundation::{CGFloat, CGPoint, CGRect, NSDictionary, NSInteger, NSPoin use plist::Dictionary; use regex::Regex; use serde::Deserialize; +use crate::plugins::js::plugins::applications::{DesktopApplication, DesktopPathAction, DesktopSettings13AndPostData, DesktopSettingsPre13Data}; -use crate::plugins::applications::{DesktopEntry, resize_icon}; -pub fn get_apps() -> Vec { - let file_manager = FileManager::default(); - - let all_items = [ - get_applications(&file_manager), - get_settings(&file_manager), - ]; - - all_items - .into_iter() - .flatten() - .collect() -} - -fn get_applications(file_manager: &FileManager) -> Vec { - - let finder_application = vec![PathBuf::from("/System/Library/CoreServices/Finder.app")]; - let finder_applications = get_applications_in_dir(PathBuf::from("/System/Library/CoreServices/Finder.app/Contents/Applications")); - - let core_services_applications = get_applications_in_dir(PathBuf::from("/System/Library/CoreServices/Applications")); - - // these are covered by recursion on SearchPathDirectory::Applications - // let user_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::User); - // let local_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::Local); - // let system_admin_applications_dir = get_applications_with_kind(&file_manager, SearchPathDirectory::AdminApplications, SearchPathDomainMask::Domain); - - let user_applications_dir = get_applications_with_kind(file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::User); - let local_applications_dir = get_applications_with_kind(file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::Local); - let system_applications_dir = get_applications_with_kind(file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::Domain); - - let all_applications = [ - finder_application, - finder_applications, - core_services_applications, - // user_admin_applications_dir, - // local_admin_applications_dir, - // system_admin_applications_dir, - user_applications_dir, - local_applications_dir, - system_applications_dir - ]; - - let all_applications: Vec<_> = all_applications - .into_iter() - .flatten() - .collect(); - - tracing::debug!("Found following macOS applications: {:?}", all_applications); - - let all_applications = all_applications - .into_iter() - .map(|path| { - let name = path.file_stem() - .expect(&format!("invalid path: {:?}", path)) - .to_string_lossy() - .to_string(); - - let info_path = path.join("Contents").join("Info.plist"); - - let info: Option = plist::from_file(info_path) - .ok(); - - let name = info.as_ref() - .and_then(|info| info.bundle_display_name.clone().or_else(|| info.bundle_name.clone())) - .unwrap_or(name); - - let icon = get_application_icon(&path) - .inspect_err(|err| tracing::error!("error while reading application icon for {:?}: {:?}", path, err)) - .ok(); - - DesktopEntry { - name, - icon, - command: vec!["open".to_string(), path.to_string_lossy().to_string()], - } - }) - .collect::>(); - - all_applications -} - -fn get_settings(file_manager: &FileManager) -> Vec { +pub fn macos_major_version() -> u8 { let system_version: SystemVersion = plist::from_file("/System/Library/CoreServices/SystemVersion.plist") .expect("SystemVersion.plist doesn't follow expected format"); @@ -110,119 +29,220 @@ fn get_settings(file_manager: &FileManager) -> Vec { .parse() .expect("SystemVersion.plist ProductVersion major doesn't match expected format"); - tracing::debug!("Indexing settings for macOS version: {}", major_version); + tracing::debug!("macOS version: {}", major_version); - if major_version >= 13 { // Ventura and higher - let sidebar: Vec = plist::from_file("/System/Applications/System Settings.app/Contents/Resources/Sidebar.plist") - .expect("Sidebar.plist doesn't follow expected format"); + major_version +} - let preferences_ids: Vec<_> = sidebar.into_iter() - .flat_map(|section| match section { - SidebarSection::Content { content } => content, - SidebarSection::Title { .. } => vec![] - }) - .collect(); +pub fn macos_system_applications() -> Vec { + let finder_application = vec![PathBuf::from("/System/Library/CoreServices/Finder.app")]; + let finder_applications = get_applications_in_dir(PathBuf::from("/System/Library/CoreServices/Finder.app/Contents/Applications")); - tracing::debug!("Found following macOS setting preference ids: {:?}", &preferences_ids); + let core_services_applications = get_applications_in_dir(PathBuf::from("/System/Library/CoreServices/Applications")); - let extensions: HashMap<_, _> = get_extensions_in_dir(PathBuf::from("/System/Library/ExtensionKit/Extensions")) - .into_iter() - .filter_map(|path| { - fn read_plist(path: &Path) -> anyhow::Result<(String, (String, PathBuf))> { - let name = path.file_stem() - .expect(&format!("invalid path: {:?}", path)) - .to_string_lossy() - .to_string(); + let all_applications = [ + finder_application, + finder_applications, + core_services_applications, + ]; - let info_path = path.join("Contents").join("Info.plist"); + let all_applications: Vec<_> = all_applications + .into_iter() + .flatten() + .collect(); - let info = plist::from_file::<_, Info>(info_path.as_path()) - .context(format!("Unexpected Info.plist for System Extensions: {}", &info_path.display()))?; + all_applications +} - let name = info.bundle_display_name - .clone() - .or_else(|| info.bundle_name.clone()) - .unwrap_or(name); - Ok((info.bundle_id, (name, path.to_path_buf()))) - } +pub fn macos_application_dirs() -> Vec { + let file_manager = FileManager::default(); - read_plist(&path) - .inspect_err(|err| tracing::error!("error while reading system extension Info.plist {:?}: {:?}", path, err)) - .ok() - }) - .collect(); + let user_applications_dir = get_path(&file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::User); + let local_applications_dir = get_path(&file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::Local); + let system_applications_dir = get_path(&file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::Domain); - tracing::debug!("Found following macOS setting extensions: {:?}", &extensions); + let all_applications = [ + user_applications_dir, + local_applications_dir, + system_applications_dir, + ]; - preferences_ids.into_iter() - .filter_map(|preferences_id| { - match extensions.get(&preferences_id) { - None => { - // todo some settings panel items return none here - tracing::debug!("Unknown preference id found: {}", &preferences_id); + let all_applications: Vec<_> = all_applications + .into_iter() + .flatten() + .collect(); - None - } - Some((name, path)) => { - let icon = get_application_icon(&path) - .inspect_err(|err| tracing::error!("error while reading application icon for {:?}: {:?}", path, err)) - .ok(); + all_applications +} - Some( - DesktopEntry { - name: name.to_string(), - icon, - command: vec![ - "open".to_string(), - format!("x-apple.systempreferences:{}", preferences_id) - ], - } - ) - } - } - }) - .collect() - } else { - let user_pref_panes_dir = get_pref_panes_with_kind(file_manager, SearchPathDirectory::Library, SearchPathDomainMask::User); - let local_pref_panes_dir = get_pref_panes_with_kind(file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Local); - let system_pref_panes_dir = get_pref_panes_with_kind(file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Domain); +pub fn macos_app_from_arbitrary_path(path: PathBuf) -> Option { + let path = path.ancestors() + .into_iter() + .collect::>() + .into_iter() + .rev() + .find_map(|path| { + let Some(extension) = path.extension() else { + return None; + }; - let all_settings = [ - user_pref_panes_dir, - local_pref_panes_dir, - system_pref_panes_dir, - ]; + let Some("app") = extension.to_str() else { + return None; + }; - let all_settings: Vec<_> = all_settings - .into_iter() - .flatten() - .collect(); + Some(path) + }); - tracing::debug!("Found following macOS settings: {:?}", all_settings); + let Some(path) = path else { + return None; + }; - let all_settings = all_settings.into_iter() - .map(|path| { - let name = path.file_stem() // TODO is there a proper way? + macos_app_from_path(path) +} + +pub fn macos_app_from_path(path: &Path) -> Option { + if !path.is_dir() { + return None + } + + let name = path.file_stem() + .expect(&format!("invalid path: {:?}", path)) + .to_str() + .expect("non-uft8 paths are not supported") + .to_string(); + + let info_path = path.join("Contents").join("Info.plist"); + + let info: Option = plist::from_file(info_path) + .ok(); + + let name = info.as_ref() + .and_then(|info| info.bundle_display_name.clone().or_else(|| info.bundle_name.clone())) + .unwrap_or(name); + + let icon = get_application_icon(&path) + .inspect_err(|err| tracing::error!("error while reading application icon for {:?}: {:?}", path, err)) + .ok(); + + let path = path.to_str().expect("non-uft8 paths are not supported").to_string(); + + Some(DesktopPathAction::Add { + id: path.clone(), + data: DesktopApplication { + name, + path, + icon, + }, + }) +} + +pub fn macos_settings_pre_13() -> Vec { + let file_manager = FileManager::default(); + + let user_pref_panes_dir = get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::User); + let local_pref_panes_dir = get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Local); + let system_pref_panes_dir = get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Domain); + + let all_settings = [ + user_pref_panes_dir, + local_pref_panes_dir, + system_pref_panes_dir, + ]; + + let all_settings: Vec<_> = all_settings + .into_iter() + .flatten() + .collect(); + + tracing::debug!("Found following macOS settings: {:?}", all_settings); + + let all_settings = all_settings.into_iter() + .map(|path| { + let name = path.file_stem() // TODO is there a proper way got get the name? + .expect(&format!("invalid path: {:?}", &path)) + .to_string_lossy() + .to_string(); + + DesktopSettingsPre13Data { + name, + path: path.to_str().expect("non-uft8 paths are not supported").to_string(), + icon: None, + } + }) + .collect(); + + all_settings +} + +pub fn macos_settings_13_and_post() -> Vec { + let sidebar: Vec = plist::from_file("/System/Applications/System Settings.app/Contents/Resources/Sidebar.plist") + .expect("Sidebar.plist doesn't follow expected format"); + + let preferences_ids: Vec<_> = sidebar.into_iter() + .flat_map(|section| match section { + SidebarSection::Content { content } => content, + SidebarSection::Title { .. } => vec![] + }) + .collect(); + + tracing::debug!("Found following macOS setting preference ids: {:?}", &preferences_ids); + + let extensions: HashMap<_, _> = get_extensions_in_dir(PathBuf::from("/System/Library/ExtensionKit/Extensions")) + .into_iter() + .filter_map(|path| { + fn read_plist(path: &Path) -> anyhow::Result<(String, (String, PathBuf))> { + let name = path.file_stem() .expect(&format!("invalid path: {:?}", path)) .to_string_lossy() .to_string(); - DesktopEntry { - name, - icon: None, - command: vec![ - "open".to_string(), - "-b".to_string(), - "com.apple.systempreferences".to_string(), - path.to_string_lossy().to_string() - ], - } - }) - .collect(); + let info_path = path.join("Contents").join("Info.plist"); - all_settings - } + let info = plist::from_file::<_, Info>(info_path.as_path()) + .context(format!("Unexpected Info.plist for System Extensions: {}", &info_path.display()))?; + + let name = info.bundle_display_name + .clone() + .or_else(|| info.bundle_name.clone()) + .unwrap_or(name); + + Ok((info.bundle_id, (name, path.to_path_buf()))) + } + + read_plist(&path) + .inspect_err(|err| tracing::error!("error while reading system extension Info.plist {:?}: {:?}", path, err)) + .ok() + }) + .collect(); + + tracing::debug!("Found following macOS setting extensions: {:?}", &extensions); + + preferences_ids.into_iter() + .filter_map(|preferences_id| { + match extensions.get(&preferences_id) { + None => { + // todo some settings panel items return none here + tracing::debug!("Unknown preference id found: {}", &preferences_id); + + None + } + Some((name, path)) => { + let icon = get_application_icon(&path) + .inspect_err(|err| tracing::error!("error while reading application icon for {:?}: {:?}", path, err)) + .ok(); + + Some( + DesktopSettings13AndPostData { + name: name.to_string(), + preferences_id, + icon, + } + ) + } + } + }) + .collect() } fn get_pref_panes_with_kind(file_manager: &FileManager, directory: SearchPathDirectory, mask: SearchPathDomainMask) -> Vec { @@ -263,6 +283,26 @@ fn get_items_with_kind( } } +fn get_path( + file_manager: &FileManager, + directory: SearchPathDirectory, + mask: SearchPathDomainMask, +) -> Option { + match file_manager.get_directory(directory.clone(), mask.clone()) { + Ok(url) => { + let applications_dir = url.to_file_path() + .expect("returned application url is not a file path"); + + Some(applications_dir) + } + Err(err) => { + tracing::error!("error reading {:?} {:?} directory: {:?}", directory, mask, err); + + None + } + } +} + fn get_pref_panes_in_dir(path: PathBuf) -> Vec { get_items_in_dir(path, "prefPane") } @@ -357,41 +397,6 @@ fn get_application_icon(app_path: &Path) -> anyhow::Result> { } } -fn get_png_from_icon_path(icon_path: PathBuf) -> Option> { - let icon_file = std::fs::File::open(icon_path.clone()) - .inspect_err(|err| tracing::debug!("error while opening icns {:?}: {:?}", icon_path, err)) - .ok()?; - - let icon_family = icns::IconFamily::read(icon_file) - .inspect_err(|err| tracing::debug!("error while reading icns {:?}: {:?}", icon_path, err)) - .ok()?; - - let bytes = icon_family.available_icons() - .into_iter() - .max_by_key(|icon_type| icon_type.screen_width()) - .and_then(|icon_type| { - icon_family.get_icon_with_type(icon_type) - .inspect_err(|err| tracing::debug!("error while extracting image from icns {:?}: {:?}", icon_path, err)) - .ok() - }) - .and_then(|image| { - let mut buffer = vec![]; - - let result = image.write_png(&mut buffer); - - match result { - Ok(_) => { - resize_icon(buffer) - .inspect_err(|err| tracing::debug!("error while resizing image {:?}: {:?}", icon_path, err)) - .ok() - }, - Err(_) => None, - } - }); - - bytes -} - #[derive(Deserialize)] struct Info { #[serde(rename = "CFBundleIdentifier")] diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index 49a22b5..51cf117 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -109,7 +109,7 @@ impl SearchIndex { } pub fn save_for_plugin(&mut self, plugin_id: PluginId, plugin_name: String, search_items: Vec, refresh_search_list: bool) -> tantivy::Result<()> { - tracing::debug!("Reloading search index for plugin {:?} {:?} using following data: {:?}", plugin_id, plugin_name, search_items); + tracing::debug!("Reloading search index for plugin {:?}", plugin_id); // writer panics if another writer exists let _guard = self.index_writer_mutex.lock().expect("lock is poisoned"); From 6a70c4ecbaf2754b092dfc3b6a8e4d30ba3b9608 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 10 Nov 2024 16:53:53 +0100 Subject: [PATCH 157/540] Rework react renderer parsing to use non-recursive data structures --- Cargo.lock | 34 +- dev_plugin/src/grid-view.tsx | 9 + js/api/src/gen/components.tsx | 57 +- js/api_build/src/index.ts | 82 +- js/react_renderer/src/renderer.ts | 29 +- js/typings/index.d.ts | 28 +- rust/client/Cargo.toml | 1 - rust/client/build.rs | 451 +-- rust/client/src/global_shortcut.rs | 1 - rust/client/src/ui/client_context.rs | 4 +- rust/client/src/ui/inline_view_container.rs | 7 +- rust/client/src/ui/mod.rs | 77 +- rust/client/src/ui/theme/mod.rs | 1 - rust/client/src/ui/view_container.rs | 15 +- rust/client/src/ui/widget.rs | 3281 +++++++++---------- rust/client/src/ui/widget_container.rs | 130 +- rust/common/Cargo.toml | 5 + rust/common/build.rs | 484 +++ rust/common/src/model.rs | 75 +- rust/common/src/rpc/frontend_api.rs | 4 +- rust/component_model/src/lib.rs | 389 ++- rust/server/src/model.rs | 20 +- rust/server/src/plugins/js/mod.rs | 2 +- rust/server/src/plugins/js/ui.rs | 281 +- 24 files changed, 2644 insertions(+), 2823 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b0cba24..72a6b03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1109,7 +1109,6 @@ dependencies = [ "once_cell", "serde", "serde_json", - "strum 0.26.2", "thiserror", "tokio", "tonic", @@ -1278,10 +1277,15 @@ dependencies = [ "anyhow", "base64 0.22.0", "bytes", + "component_model", + "convert_case 0.6.0", "directories", "gix-url", + "indexmap 2.2.6", + "itertools 0.10.5", "prost 0.12.4", "serde", + "serde-value", "serde_json", "thiserror", "tokio", @@ -2183,8 +2187,8 @@ dependencies = [ "proc-macro2 1.0.80", "quote 1.0.36", "regex", - "strum 0.25.0", - "strum_macros 0.25.3", + "strum", + "strum_macros", "syn 1.0.109", "syn 2.0.59", "thiserror", @@ -8789,16 +8793,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ - "strum_macros 0.25.3", -] - -[[package]] -name = "strum" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" -dependencies = [ - "strum_macros 0.26.2", + "strum_macros", ] [[package]] @@ -8814,19 +8809,6 @@ dependencies = [ "syn 2.0.59", ] -[[package]] -name = "strum_macros" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" -dependencies = [ - "heck 0.4.1", - "proc-macro2 1.0.80", - "quote 1.0.36", - "rustversion", - "syn 2.0.59", -] - [[package]] name = "subtle" version = "2.5.0" diff --git a/dev_plugin/src/grid-view.tsx b/dev_plugin/src/grid-view.tsx index b4e1a7d..6bb9abd 100644 --- a/dev_plugin/src/grid-view.tsx +++ b/dev_plugin/src/grid-view.tsx @@ -19,6 +19,15 @@ export default function GridView(): ReactElement { value={searchText} onChange={setSearchText} /> + + + + + Test Paragraph Section 1 1 + + + + { numbers.map(value => { const title = "Title " + value; diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index f608823..20ddebd 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -42,7 +42,7 @@ declare global { children?: ElementComponent; }; ["gauntlet:image"]: { - source: ImageSource | Icons; + source: Image; }; ["gauntlet:h1"]: { children?: StringComponent; @@ -121,15 +121,15 @@ declare global { ["gauntlet:empty_view"]: { title: string; description?: string; - image?: ImageSource | Icons; + image?: Image; }; ["gauntlet:accessory_icon"]: { - icon: ImageSource | Icons; + icon: Image; tooltip?: string; }; ["gauntlet:accessory_text"]: { text: string; - icon?: ImageSource | Icons; + icon?: Image; tooltip?: string; }; ["gauntlet:search_bar"]: { @@ -141,7 +141,7 @@ declare global { children?: ElementComponent; title: string; subtitle?: string; - icon?: ImageSource | Icons; + icon?: Image; onClick?: () => void; }; ["gauntlet:list_section"]: { @@ -150,7 +150,7 @@ declare global { subtitle?: string; }; ["gauntlet:list"]: { - children?: ElementComponent; + children?: ElementComponent; isLoading?: boolean; }; ["gauntlet:grid_item"]: { @@ -166,7 +166,7 @@ declare global { columns?: number; }; ["gauntlet:grid"]: { - children?: ElementComponent; + children?: ElementComponent; isLoading?: boolean; columns?: number; }; @@ -180,11 +180,6 @@ export type EmptyNode = boolean | null | undefined; export type ElementComponent> = Element | EmptyNode | Iterable>; export type StringComponent = StringNode | EmptyNode | Iterable; export type StringOrElementComponent> = StringNode | EmptyNode | Element | Iterable>; -export type ImageSource = { - asset: string; -} | { - url: string; -}; export enum Icons { PersonAdd = "PersonAdd", Airplane = "Airplane", @@ -358,6 +353,14 @@ export enum Icons { Indent = "Indent", Unindent = "Unindent" } +export type ImageSourceUrl = { + url: string; +}; +export type ImageSourceAsset = { + asset: string; +}; +export type ImageSource = ImageSourceUrl | ImageSourceAsset; +export type Image = ImageSource | Icons; export interface ActionProps { id?: string; label: string; @@ -448,7 +451,7 @@ Metadata.Value = MetadataValue; Metadata.Icon = MetadataIcon; Metadata.Separator = MetadataSeparator; export interface ImageProps { - source: ImageSource | Icons; + source: Image; } export const Image: FC = (props: ImageProps): ReactNode => { return ; @@ -645,13 +648,13 @@ Inline.Center = Content; export interface EmptyViewProps { title: string; description?: string; - image?: ImageSource | Icons; + image?: Image; } export const EmptyView: FC = (props: EmptyViewProps): ReactNode => { return ; }; export interface IconAccessoryProps { - icon: ImageSource | Icons; + icon: Image; tooltip?: string; } export const IconAccessory: FC = (props: IconAccessoryProps): ReactNode => { @@ -659,7 +662,7 @@ export const IconAccessory: FC = (props: IconAccessoryProps) }; export interface TextAccessoryProps { text: string; - icon?: ImageSource | Icons; + icon?: Image; tooltip?: string; } export const TextAccessory: FC = (props: TextAccessoryProps): ReactNode => { @@ -676,7 +679,7 @@ export const SearchBar: FC = (props: SearchBarProps): ReactNode export interface ListItemProps { title: string; subtitle?: string; - icon?: ImageSource | Icons; + icon?: Image; accessories?: (ElementComponent | ElementComponent)[]; onClick?: () => void; } @@ -695,24 +698,24 @@ export const ListSection: FC & { }; ListSection.Item = ListItem; export interface ListProps { - children?: ElementComponent; + children?: ElementComponent; actions?: ElementComponent; isLoading?: boolean; } export const List: FC & { + Item: typeof ListItem; + Section: typeof ListSection; SearchBar: typeof SearchBar; EmptyView: typeof EmptyView; Detail: typeof Detail; - Item: typeof ListItem; - Section: typeof ListSection; } = (props: ListProps): ReactNode => { return {props.actions as any}{props.children}; }; +List.Item = ListItem; +List.Section = ListSection; List.SearchBar = SearchBar; List.EmptyView = EmptyView; List.Detail = Detail; -List.Item = ListItem; -List.Section = ListSection; export interface GridItemProps { children?: ElementComponent; title?: string; @@ -739,20 +742,20 @@ export const GridSection: FC & { }; GridSection.Item = GridItem; export interface GridProps { - children?: ElementComponent; + children?: ElementComponent; isLoading?: boolean; actions?: ElementComponent; columns?: number; } export const Grid: FC & { - SearchBar: typeof SearchBar; - EmptyView: typeof EmptyView; Item: typeof GridItem; Section: typeof GridSection; + SearchBar: typeof SearchBar; + EmptyView: typeof EmptyView; } = (props: GridProps): ReactNode => { return {props.actions as any}{props.children}; }; -Grid.SearchBar = SearchBar; -Grid.EmptyView = EmptyView; Grid.Item = GridItem; Grid.Section = GridSection; +Grid.SearchBar = SearchBar; +Grid.EmptyView = EmptyView; diff --git a/js/api_build/src/index.ts b/js/api_build/src/index.ts index 99af186..615fd16 100644 --- a/js/api_build/src/index.ts +++ b/js/api_build/src/index.ts @@ -241,31 +241,6 @@ function makeComponents(modelInput: Component[]): ts.SourceFile { const root = modelInput.find((component): component is RootComponent => component.type === "root"); if (root != null) { - // image special case - // export type ImageSource = { asset: string } | { url: string }; - - const imageSourceDeclaration = ts.factory.createTypeAliasDeclaration( - [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier("ImageSource"), - undefined, - ts.factory.createUnionTypeNode([ - ts.factory.createTypeLiteralNode([ts.factory.createPropertySignature( - undefined, - ts.factory.createIdentifier("asset"), - undefined, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) - )]), - ts.factory.createTypeLiteralNode([ts.factory.createPropertySignature( - undefined, - ts.factory.createIdentifier("url"), - undefined, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) - )]) - ]) - ); - - publicDeclarations.push(imageSourceDeclaration) - for (const [name, sharedType] of Object.entries(root.sharedTypes)) { switch (sharedType.type) { @@ -304,6 +279,19 @@ function makeComponents(modelInput: Component[]): ts.SourceFile { publicDeclarations.push(declaration) break; } + case "union": { + const declaration = ts.factory.createTypeAliasDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier(name), + undefined, + ts.factory.createUnionTypeNode( + sharedType.items.map(type => makeType(type)) + ) + ) + + publicDeclarations.push(declaration) + break; + } default: { throw new Error("unreachable"); } @@ -407,10 +395,11 @@ function makeComponents(modelInput: Component[]): ts.SourceFile { let componentType: ts.TypeReferenceNode | ts.IntersectionTypeNode; if (component.children.type == "members" || component.children.type == "string_or_members") { + const members = { ...component.children.ordered_members, ...component.children.per_type_members } componentType = ts.factory.createIntersectionTypeNode([ componentFCType, ts.factory.createTypeLiteralNode( - Object.entries(component.children.members).map(([memberName, member]) => { + Object.entries(members).map(([memberName, member]) => { return ts.factory.createPropertySignature( undefined, ts.factory.createIdentifier(memberName), @@ -432,7 +421,8 @@ function makeComponents(modelInput: Component[]): ts.SourceFile { switch (component.children.type) { case "string_or_members": case "members": { - memberAssignments = Object.entries(component.children.members).map(([memberName, member]) => { + const members = { ...component.children.ordered_members, ...component.children.per_type_members } + memberAssignments = Object.entries(members).map(([memberName, member]) => { return ts.factory.createExpressionStatement(ts.factory.createBinaryExpression( ts.factory.createPropertyAccessExpression( ts.factory.createIdentifier(component.name), @@ -581,11 +571,13 @@ function makePropertyTypes(component: StandardComponent, componentPropsInChildre function makeChildrenType(type: Children, additionalComponentRefs: ComponentRef[]): ts.TypeNode { switch (type.type) { case "members": { + const members = { ...type.ordered_members, ...type.per_type_members } + return ts.factory.createTypeReferenceNode( ts.factory.createIdentifier("ElementComponent"), [ ts.factory.createUnionTypeNode( - [...additionalComponentRefs, ...Object.values(type.members)].map(member => ( + [...additionalComponentRefs, ...Object.values(members)].map(member => ( ts.factory.createTypeQueryNode( ts.factory.createIdentifier(member.componentName), undefined @@ -596,11 +588,13 @@ function makeChildrenType(type: Children, additionalComponentRefs: ComponentRef[ ) } case "string_or_members": { + const members = { ...type.ordered_members, ...type.ordered_members } + return ts.factory.createTypeReferenceNode( ts.factory.createIdentifier("StringOrElementComponent"), [ ts.factory.createUnionTypeNode( - [...additionalComponentRefs, ...Object.values(type.members)].map(member => ( + [...additionalComponentRefs, ...Object.values(members)].map(member => ( ts.factory.createTypeQueryNode( ts.factory.createIdentifier(member.componentName), undefined @@ -679,22 +673,10 @@ function makeType(type: PropertyType): ts.TypeNode { ] ) } - case "image_source": { - return ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("ImageSource"), - undefined - ) - } case "array": { return ts.factory.createArrayTypeNode(makeType(type.item)) } - case "enum": { - return ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier(type.name), - undefined - ) - } - case "object": { + case "shared_type_ref": { return ts.factory.createTypeReferenceNode( ts.factory.createIdentifier(type.name), undefined @@ -727,16 +709,10 @@ function isInProperty(propertyType: PropertyType) { case "component": { return false } - case "image_source": { - return true - } case "array": { return isInProperty(propertyType.item) } - case "enum": { - return true - } - case "object": { + case "shared_type_ref": { return true } case "union": { @@ -771,16 +747,10 @@ function collectAllComponentRefs(propertyType: PropertyType): ComponentRef[] { case "component": { return [propertyType.reference] } - case "image_source": { - return [] - } case "array": { return collectAllComponentRefs(propertyType.item) } - case "enum": { - return [] - } - case "object": { + case "shared_type_ref": { return [] } case "union": { diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index 5aee8b6..cdfc2f7 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -326,12 +326,15 @@ export const createHostConfig = (): HostConfig< }, replaceContainerChildren(container: RootUiWidget, newChildren: ChildSet): void { - // TODO Deno.inspect is always executed - InternalApi.op_log_trace("renderer_js_persistence", `replaceContainerChildren is called, container: ${Deno.inspect(container)}, newChildren: ${Deno.inspect(newChildren, { depth: Number.MAX_VALUE })}`) + // InternalApi.op_log_info("renderer_js_persistence", `replaceContainerChildren is called, container: ${Deno.inspect(container)}, newChildren: ${Deno.inspect(newChildren, { depth: Number.MAX_VALUE })}`) container.widgetChildren = newChildren - InternalApi.op_react_replace_view(gauntletContextValue.renderLocation(), gauntletContextValue.isBottommostView(), gauntletContextValue.entrypointId(), container) + const containerComponent = { content: newChildren.map(value => convertComponents(value)) } + + // InternalApi.op_log_info("renderer_js_persistence", `Converted container: ${Deno.inspect(containerComponent, { depth: Number.MAX_VALUE })}`) + + InternalApi.op_react_replace_view(gauntletContextValue.renderLocation(), gauntletContextValue.isBottommostView(), gauntletContextValue.entrypointId(), containerComponent) }, cloneHiddenInstance( @@ -353,6 +356,26 @@ export const createHostConfig = (): HostConfig< supportsHydration: false }); + +function convertComponents(widget: UiWidget): any { + const component: any = { + __id__: widget.widgetId, + __type__: widget.widgetType, + ...widget.widgetProperties + } + + for (const [name, value] of Object.entries(component)) { + if (typeof value === "function") { + delete component[name] + } + } + + component.content = widget.widgetChildren + .map(child => convertComponents(child)) + + return component +} + function shallowDiff(oldObj: Record, newObj: Record): string[] | null { const uniqueProps = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]); const diff = Array.from(uniqueProps) diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index cb6543d..e378b47 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -121,7 +121,7 @@ interface InternalApi { show_hud(display: string): void; - op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, entrypoint_id: string, container: UiWidget): void; + op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, entrypoint_id: string, container: any): void; show_plugin_error_view(entrypoint_id: string, render_location: RenderLocation): void; fetch_action_id_for_shortcut(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean): Promise; @@ -152,7 +152,7 @@ type RootComponent = { sharedTypes: Record } -type SharedType = SharedTypeEnum | SharedTypeObject +type SharedType = SharedTypeEnum | SharedTypeObject | SharedTypeUnion type SharedTypeEnum = { type: "enum", @@ -164,6 +164,11 @@ type SharedTypeObject = { items: Record } +type SharedTypeUnion = { + type: "union", + items: PropertyType[] +} + type TextPartComponent = { type: "text_part", internalName: string, @@ -178,12 +183,14 @@ type Children = ChildrenMembers | ChildrenString | ChildrenNone | ChildrenString type ChildrenMembers = { type: "members", - members: Record + ordered_members: Record + per_type_members: Record } type ChildrenStringOrMembers = { type: "string_or_members", textPartInternalName: string, - members: Record + ordered_members: Record + per_type_members: Record } type ChildrenString = { type: "string" @@ -198,7 +205,7 @@ type ComponentRef = { componentName: string, } -type PropertyType = TypeString | TypeNumber | TypeBoolean | TypeComponent | TypeFunction | TypeImageSource | TypeImageEnum | TypeImageArray | TypeImageUnion | TypeImageObject +type PropertyType = TypeString | TypeNumber | TypeBoolean | TypeComponent | TypeFunction | TypeSharedTypeRef | TypeImageArray | TypeImageUnion type TypeString = { type: "string" @@ -217,11 +224,8 @@ type TypeFunction = { type: "function" arguments: Property[] } -type TypeImageSource = { - type: "image_source" -} -type TypeImageEnum = { - type: "enum" +type TypeSharedTypeRef = { + type: "shared_type_ref" name: string } type TypeImageUnion = { @@ -232,7 +236,3 @@ type TypeImageArray = { type: "array" item: PropertyType } -type TypeImageObject = { - type: "object" - name: string -} \ No newline at end of file diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index 116fd8b..b5a779a 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -15,7 +15,6 @@ utils = { path = "../utils" } tonic = "0.11.0" itertools = "0.12.1" component_model = { path = "../component_model" } -strum = { version = "0.26", features = ["derive"] } image = "0.25" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/rust/client/build.rs b/rust/client/build.rs index a87b674..39c63d2 100644 --- a/rust/client/build.rs +++ b/rust/client/build.rs @@ -5,7 +5,7 @@ use std::path::Path; use convert_case::{Case, Casing}; -use component_model::{Children, Component, ComponentName, create_component_model, Property, PropertyType, SharedType}; +use component_model::{create_component_model, Component, ComponentName, Property, PropertyType}; fn main() -> anyhow::Result<()> { let out_dir = env::var("OUT_DIR")?; @@ -15,433 +15,6 @@ fn main() -> anyhow::Result<()> { let components = create_component_model(); - output.push_str("#[derive(Debug)]\n"); - output.push_str("enum ComponentWidget {\n"); - - for component in &components { - match component { - Component::Standard { name, props, children, .. } => { - output.push_str(&format!(" {}", name)); - - let has_children = !matches!(children, Children::None) || props.iter().any(|prop| prop.property_type.is_in_children()); - - if !props.is_empty() || has_children { - output.push_str(" {\n"); - - if has_children { - let string = match children { - Children::StringOrMembers { .. } => "Vec".to_owned(), - Children::Members { .. } => "Vec".to_owned(), - Children::String { .. } => "Vec".to_owned(), - Children::None => { - if props.iter().any(|prop| prop.property_type.is_in_children()) { - "Vec".to_owned() - } else { - panic!("cannot create type for Children::None") - } - } - }; - - output.push_str(&format!(" children: {},\n", string)); - } - - for prop in props { - if prop.property_type.is_in_property() { - output.push_str(&format!(" {}: {},\n", prop.name, generate_type(&prop, name))); - } - } - output.push_str(" },\n"); - } else { - output.push_str(",\n"); - } - } - Component::Root { .. } => { - output.push_str(" Root {\n"); - output.push_str(" children: Vec,\n"); - output.push_str(" },\n"); - } - Component::TextPart { .. } => { - output.push_str(" TextPart {\n"); - output.push_str(" value: String,\n"); - output.push_str(" },\n"); - } - } - } - - output.push_str("}\n"); - output.push_str("\n"); - - for component in &components { - match component { - Component::Standard { name: component_name, props, .. } => { - for prop in props { - match &prop.property_type { - PropertyType::Union { items } => { - output.push_str("#[derive(Debug)]\n"); - output.push_str(&format!("enum {}{} {{\n", component_name, prop.name.to_case(Case::Pascal))); - - for (index, property_type) in items.iter().enumerate() { - if !property_type.is_in_property() { - continue - } - - match property_type { - PropertyType::Union { .. } => panic!("nested union should not be used"), - _ => { - output.push_str(&format!(" _{}({}),\n", index, generate_required_type(&property_type, "SHOULD NOT BE USED".to_owned()))); - } - } - } - - output.push_str("}\n"); - output.push_str("\n"); - } - _ => {} - } - } - } - Component::Root { shared_types, .. } => { - for (type_name, shared_type) in shared_types { - match shared_type { - SharedType::Enum { items } => { - output.push_str("#[derive(Debug, strum::EnumString)]\n"); - output.push_str(&format!("enum {} {{\n", type_name)); - - for item in items { - output.push_str(&format!(" {},\n", &item)); - } - - output.push_str("}\n"); - output.push_str("\n"); - } - SharedType::Object { items } => { - output.push_str("#[derive(Debug)]\n"); - output.push_str(&format!("struct {} {{\n", type_name)); - - for (property_name, property_type) in items { - output.push_str(&format!(" {}: {},\n", &property_name, generate_required_type(&property_type, format!("{}{}", type_name, property_name)))); - } - - output.push_str("}\n"); - output.push_str("\n"); - } - } - } - } - Component::TextPart { .. } => {} - } - } - - - output.push_str("fn create_component_widget(component_internal_name: &str, properties: HashMap, children: Vec) -> anyhow::Result {\n"); - output.push_str(" let widget = match component_internal_name {\n"); - - for component in &components { - match component { - Component::Standard { internal_name, name, props, children, .. } => { - output.push_str(&format!(" \"gauntlet:{}\" => ComponentWidget::{}", internal_name, name)); - - let has_children = !matches!(children, Children::None) || props.iter().any(|prop| prop.property_type.is_in_children()); - - if !props.is_empty() || has_children { - output.push_str(&" {\n"); - - if has_children { - output.push_str(" children,\n"); - } - - for prop in props { - if !prop.property_type.is_in_property() { - continue - } - - match &prop.property_type { - PropertyType::String => { - if prop.optional { - output.push_str(&format!(" {}: parse_optional_string(&properties, \"{}\")?,\n", prop.name, prop.name)); - } else { - output.push_str(&format!(" {}: parse_string(&properties, \"{}\")?,\n", prop.name, prop.name)); - } - }, - PropertyType::Number => { - if prop.optional { - output.push_str(&format!(" {}: parse_optional_number(&properties, \"{}\")?,\n", prop.name, prop.name)); - } else { - output.push_str(&format!(" {}: parse_number(&properties, \"{}\")?,\n", prop.name, prop.name)); - } - }, - PropertyType::Boolean => { - if prop.optional { - output.push_str(&format!(" {}: parse_optional_boolean(&properties, \"{}\")?,\n", prop.name, prop.name)); - } else { - output.push_str(&format!(" {}: parse_boolean(&properties, \"{}\")?,\n", prop.name, prop.name)); - } - }, - PropertyType::ImageSource => { - if prop.optional { - output.push_str(&format!(" {}: parse_bytes_optional(&properties, \"{}\")?,\n", prop.name, prop.name)); - } else { - output.push_str(&format!(" {}: parse_bytes(&properties, \"{}\")?,\n", prop.name, prop.name)); - } - } - PropertyType::Enum { .. } => { - if prop.optional { - output.push_str(&format!(" {}: parse_enum_optional(&properties, \"{}\")?,\n", prop.name, prop.name)); - } else { - output.push_str(&format!(" {}: parse_enum(&properties, \"{}\")?,\n", prop.name, prop.name)); - } - } - PropertyType::Union { .. } => { - if prop.optional { - output.push_str(&format!(" {}: parse_union_optional(&properties, \"{}\")?,\n", prop.name, prop.name)); - } else { - output.push_str(&format!(" {}: parse_union(&properties, \"{}\")?,\n", prop.name, prop.name)); - } - } - PropertyType::Object { .. } => { - if prop.optional { - output.push_str(&format!(" {}: parse_object_optional(&properties, \"{}\")?,\n", prop.name, prop.name)); - } else { - output.push_str(&format!(" {}: parse_object(&properties, \"{}\")?,\n", prop.name, prop.name)); - } - } - PropertyType::Array { .. } => { - if prop.optional { - output.push_str(&format!(" {}: parse_array_optional(&properties, \"{}\")?,\n", prop.name, prop.name)); - } else { - output.push_str(&format!(" {}: parse_array(&properties, \"{}\")?,\n", prop.name, prop.name)); - } - } - _ => { - unreachable!() - } - }; - } - output.push_str(" },\n"); - } else { - output.push_str(",\n"); - } - } - Component::Root { .. } => {} - Component::TextPart { internal_name, props } => { - let name = match &props[..] { - [Property { property_type: PropertyType::String, optional: false, name, .. }] => name, - _ => panic!("text_part should have single string not optional prop") - }; - output.push_str(&format!(" \"gauntlet:{}\" => ComponentWidget::TextPart {{\n", internal_name)); - output.push_str(&format!(" {}: parse_string(&properties, \"{}\")?,\n", name, name)); - output.push_str(" },\n"); - } - } - } - - output.push_str(" _ => Err(anyhow::anyhow!(\"cannot create {} type\", component_internal_name))?\n"); - output.push_str(" };\n"); - output.push_str(" Ok(widget)\n"); - output.push_str("}\n"); - output.push_str("\n"); - - - output.push_str("fn append_component_widget_child(parent: &ComponentWidgetWrapper, child: ComponentWidgetWrapper) -> anyhow::Result<()> {\n"); - output.push_str(" let (ref mut parent, _) = &mut *parent.get_mut();\n"); - output.push_str(" match parent {\n"); - - for component in &components { - match component { - Component::Standard { name, children, .. } => { - let has_children = !matches!(children, Children::None); - - if has_children { - output.push_str(&format!(" ComponentWidget::{} {{ ref mut children, .. }} => {{\n", name)); - output.push_str(" match get_component_widget_type(&child) {\n"); - - match children { - Children::StringOrMembers { members, text_part_internal_name } => { - output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", text_part_internal_name)); - for (_, member) in members { - output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", member.component_internal_name)); - } - } - Children::Members { members } => { - for (_, member) in members { - output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", member.component_internal_name)); - } - } - Children::String { text_part_internal_name, .. } => { - output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", text_part_internal_name)); - } - Children::None => {} - } - - output.push_str(&format!(" (_, name) => Err(anyhow::anyhow!(\"{} cannot have {{}} child\", name))?\n", name)); - output.push_str(" };\n"); - output.push_str(" children.push(child)\n"); - output.push_str(" }\n"); - } else { - output.push_str(&format!(" ComponentWidget::{} {{ .. }} => {{\n", name)); - output.push_str(&format!(" Err(anyhow::anyhow!(\"{} cannot have children\"))?\n", name)); - output.push_str(" }\n"); - } - } - Component::Root { children, .. } => { - output.push_str(" ComponentWidget::Root { ref mut children, .. } => {\n"); - output.push_str(" match get_component_widget_type(&child) {\n"); - - for child in children { - output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", child.component_internal_name)); - } - - output.push_str(" (_, name) => Err(anyhow::anyhow!(\"root cannot have {} child\", name))?\n"); - output.push_str(" };\n"); - output.push_str(" children.push(child)\n"); - output.push_str(" }\n"); - - } - Component::TextPart { .. } => { - output.push_str(" ComponentWidget::TextPart { .. } => {\n"); - output.push_str(" Err(anyhow::anyhow!(\"text_part cannot have children\"))?\n"); - output.push_str(" }\n"); - } - } - } - - output.push_str(" };\n"); - output.push_str(" Ok(())\n"); - output.push_str("}\n"); - output.push_str("\n"); - - - output.push_str("fn get_component_widget_children(widget: &ComponentWidgetWrapper) -> anyhow::Result> {\n"); - output.push_str(" let (widget, _) = &*widget.get();\n"); - output.push_str(" let children = match widget {\n"); - - for component in &components { - match component { - Component::Standard { name, children, .. } => { - let has_children = !matches!(children, Children::None); - if has_children { - output.push_str(&format!(" ComponentWidget::{} {{ ref children, .. }} => {{\n", name)); - output.push_str(" children\n"); - output.push_str(" }\n"); - } else { - output.push_str(&format!(" ComponentWidget::{} {{ .. }} => {{\n", name)); - output.push_str(&format!(" Err(anyhow::anyhow!(\"{} cannot have children\"))?\n", name)); - output.push_str(" }\n"); - } - } - Component::Root { .. } => { - output.push_str(" ComponentWidget::Root { ref children, .. } => {\n"); - output.push_str(" children\n"); - output.push_str(" }\n"); - } - Component::TextPart { .. } => { - output.push_str(" ComponentWidget::TextPart { .. } => {\n"); - output.push_str(" Err(anyhow::anyhow!(\"text part cannot have children\"))?\n"); - output.push_str(" }\n"); - } - } - } - - output.push_str(" };\n"); - output.push_str(" Ok(children.iter().cloned().collect())\n"); - output.push_str("}\n"); - output.push_str("\n"); - - - output.push_str("fn set_component_widget_children(widget: &ComponentWidgetWrapper, new_children: Vec) -> anyhow::Result<()> {\n"); - output.push_str(" let (ref mut widget, _) = &mut *widget.get_mut();\n"); - output.push_str(" match widget {\n"); - - for component in &components { - match component { - Component::Standard { name, children, .. } => { - let has_children = !matches!(children, Children::None); - - if has_children { - output.push_str(&format!(" ComponentWidget::{} {{ ref mut children, .. }} => {{\n", name)); - output.push_str(" for new_child in &new_children {\n"); - output.push_str(" match get_component_widget_type(new_child) {\n"); - - match children { - Children::StringOrMembers { members, text_part_internal_name } => { - output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", text_part_internal_name)); - for (_, member) in members { - output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", member.component_internal_name)); - } - } - Children::Members { members } => { - for (_, member) in members { - output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", member.component_internal_name)); - } - } - Children::String { text_part_internal_name, .. } => { - output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", text_part_internal_name)); - } - Children::None => {} - } - - output.push_str(&format!(" (_, name) => Err(anyhow::anyhow!(\"{} cannot have {{}} child\", name))?\n", name)); - output.push_str(" };\n"); - output.push_str(" }\n"); - output.push_str(" *children = new_children\n"); - output.push_str(" }\n"); - } else { - output.push_str(&format!(" ComponentWidget::{} {{ .. }} => {{\n", name)); - output.push_str(&format!(" Err(anyhow::anyhow!(\"{} cannot have children\"))?\n", name)); - output.push_str(" }\n"); - } - } - Component::Root { children, .. } => { - output.push_str(" ComponentWidget::Root { ref mut children, .. } => {\n"); - output.push_str(" for new_child in &new_children {\n"); - output.push_str(" match get_component_widget_type(new_child) {\n"); - - for child in children { - output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", child.component_internal_name)); - } - - output.push_str(" (_, name) => Err(anyhow::anyhow!(\"root cannot have {} child\", name))?\n"); - output.push_str(" };\n"); - output.push_str(" }\n"); - output.push_str(" *children = new_children\n"); - output.push_str(" }\n"); - } - Component::TextPart { .. } => { - output.push_str(" ComponentWidget::TextPart { .. } => {\n"); - output.push_str(" Err(anyhow::anyhow!(\"text_part cannot have children\"))?\n"); - output.push_str(" }\n"); - } - } - } - - output.push_str(" };\n"); - output.push_str(" Ok(())\n"); - output.push_str("}\n"); - output.push_str("\n"); - - - output.push_str("fn get_component_widget_type(widget: &ComponentWidgetWrapper) -> (&str, &str) {\n"); - output.push_str(" let (widget, _) = &*widget.get();\n"); - output.push_str(" match widget {\n"); - - for component in &components { - match component { - Component::Standard { name, internal_name, .. } => { - output.push_str(&format!(" ComponentWidget::{} {{ .. }} => (\"gauntlet:{}\", \"{}\"),\n", name, internal_name, name)); - } - Component::Root { internal_name, .. } => { - output.push_str(&format!(" ComponentWidget::Root {{ .. }} => (\"gauntlet:{}\", \"Root\"),\n", internal_name)); - } - Component::TextPart { internal_name, .. } => { - output.push_str(&format!(" ComponentWidget::TextPart {{ .. }} => (\"gauntlet:{}\", \"TextPart\"),\n", internal_name)); - } - } - } - - output.push_str(" }\n"); - output.push_str("}\n"); - output.push_str("\n"); - for component in &components { match component { Component::Standard { name, props, .. } => { @@ -515,25 +88,29 @@ fn generate_file>(path: P, text: &str) -> std::io::Result<()> { fn generate_type(property: &Property, name: &ComponentName) -> String { match property.optional { true => generate_optional_type(&property.property_type, format!("{}{}", name, &property.name.to_case(Case::Pascal))), - false => generate_required_type(&property.property_type, format!("{}{}", name, &property.name.to_case(Case::Pascal))) + false => generate_required_type(&property.property_type, Some(format!("{}{}", name, &property.name.to_case(Case::Pascal)))) } } fn generate_optional_type(property_type: &PropertyType, union_name: String) -> String { - format!("Option<{}>", generate_required_type(property_type, union_name)) + format!("Option<{}>", generate_required_type(property_type, Some(union_name))) } -fn generate_required_type(property_type: &PropertyType, union_name: String) -> String { +fn generate_required_type(property_type: &PropertyType, union_name: Option) -> String { match property_type { PropertyType::String => "String".to_owned(), PropertyType::Number => "f64".to_owned(), PropertyType::Boolean => "bool".to_owned(), PropertyType::Function { .. } => panic!("client doesn't know about functions in properties"), - PropertyType::Component { .. } => panic!("component should not be present in properties"), - PropertyType::ImageSource => "bytes::Bytes".to_owned(), - PropertyType::Union { .. } => union_name, - PropertyType::Object { name } => name.to_owned(), - PropertyType::Enum { name } => name.to_owned(), - PropertyType::Array { item } => format!("Vec<{}>", generate_required_type(item, "SHOULD NOT BE USED".to_string())) + PropertyType::Component { reference } => format!("{}Widget", reference.component_name.to_string()), + PropertyType::SharedTypeRef { name } => name.to_owned(), + // PropertyType::ImageSource => "bytes::Bytes".to_owned(), + PropertyType::Union { .. } => { + match union_name { + None => panic!("should not be used"), + Some(union_name) => union_name + } + }, + PropertyType::Array { item } => format!("Vec<{}>", generate_required_type(item, union_name)) } } \ No newline at end of file diff --git a/rust/client/src/global_shortcut.rs b/rust/client/src/global_shortcut.rs index f605310..581b6a5 100644 --- a/rust/client/src/global_shortcut.rs +++ b/rust/client/src/global_shortcut.rs @@ -3,7 +3,6 @@ use iced::futures::channel::mpsc::Sender; use iced::futures::SinkExt; use tokio::runtime::Handle; use common::model::{PhysicalKey, PhysicalShortcut}; -use common::rpc::frontend_api::FrontendApi; use crate::ui::AppMsg; pub fn register_listener(msg_sender: Sender) { diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index 3dc39b8..aeff421 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use common::model::{EntrypointId, PhysicalShortcut, PluginId, UiRenderLocation, UiWidget, UiWidgetId}; +use common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiWidgetId}; use crate::model::UiViewEvent; use crate::ui::widget::{ActionPanel, ComponentWidgetEvent}; @@ -74,7 +74,7 @@ impl ClientContext { self.view.get_entrypoint_id() } - pub fn replace_view(&mut self, render_location: UiRenderLocation, container: UiWidget, plugin_id: &PluginId, plugin_name: &str, entrypoint_id: &EntrypointId, entrypoint_name: &str) { + pub fn replace_view(&mut self, render_location: UiRenderLocation, container: RootWidget, plugin_id: &PluginId, plugin_name: &str, entrypoint_id: &EntrypointId, entrypoint_name: &str) { match render_location { UiRenderLocation::InlineView => self.get_mut_inline_view_container(plugin_id).replace_view(container, plugin_id, plugin_name, entrypoint_id, entrypoint_name), UiRenderLocation::View => self.get_mut_view_container().replace_view(container, plugin_id, plugin_name, entrypoint_id, entrypoint_name) diff --git a/rust/client/src/ui/inline_view_container.rs b/rust/client/src/ui/inline_view_container.rs index ae41272..1721851 100644 --- a/rust/client/src/ui/inline_view_container.rs +++ b/rust/client/src/ui/inline_view_container.rs @@ -7,7 +7,7 @@ use common::model::UiRenderLocation; use crate::ui::client_context::ClientContext; use crate::ui::theme::{Element, GauntletTheme}; -use crate::ui::widget::{ActionPanel, ComponentRenderContext, ComponentWidgetEvent}; +use crate::ui::widget::{ActionPanel, ComponentWidgetEvent}; use crate::ui::AppMsg; pub struct InlineViewContainer { @@ -48,10 +48,7 @@ impl Component for InlineViewContainer { match containers.first() { Some((_, container)) => { - container.render_widget(ComponentRenderContext::InlineRoot { - plugin_name: container.get_plugin_name(), - entrypoint_name: container.get_entrypoint_name(), - }) + container.render_inline_root_widget() } None => { horizontal_space() diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index a1a42f8..ff76640 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -1,33 +1,31 @@ +use anyhow::anyhow; +use global_hotkey::hotkey::HotKey; +use global_hotkey::GlobalHotKeyManager; +use iced::advanced::graphics::core::SmolStr; +use iced::advanced::layout::Limits; +use iced::futures::channel::mpsc::Sender; +use iced::futures::SinkExt; +use iced::keyboard::key::Named; +use iced::keyboard::{Key, Modifiers}; +use iced::multi_window::Application; +use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; +use iced::widget::text::Shaping; +use iced::widget::text_input::focus; +use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, Space}; +use iced::window::settings::PlatformSpecific; +use iced::window::{Level, Position, Screenshot}; +use iced::{event, executor, font, futures, keyboard, subscription, window, Alignment, Command, Event, Font, Length, Padding, Pixels, Settings, Size, Subscription}; +use iced_aw::core::icons; use std::collections::HashMap; use std::fs; use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex as StdMutex, Mutex, RwLock as StdRwLock}; -use anyhow::anyhow; -use global_hotkey::GlobalHotKeyManager; -use global_hotkey::hotkey::HotKey; -use iced::{event, executor, font, futures, keyboard, subscription, window, Alignment, Command, Event, Font, Length, Padding, Pixels, Settings, Size, Subscription}; -use iced::advanced::graphics::core::SmolStr; -use iced::advanced::layout::Limits; -use iced::multi_window::Application; -use iced::futures::channel::mpsc::Sender; -use iced::futures::SinkExt; -use iced::keyboard::{Key, Modifiers}; -use iced::keyboard::key::Named; -use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, Space}; -use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; -use iced::widget::text::Shaping; -use iced::widget::text_input::focus; -use iced::window::{Level, Position, Screenshot}; -use iced::window::settings::PlatformSpecific; -use iced_aw::core::icons; -use serde::Deserialize; -use tokio::runtime::Handle; use tokio::sync::{Mutex as TokioMutex, RwLock as TokioRwLock}; use tonic::transport::Server; use client_context::ClientContext; -use common::model::{BackendRequestData, BackendResponseData, EntrypointId, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; +use common::model::{BackendRequestData, BackendResponseData, EntrypointId, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; use common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFrontendApiError}; use common::scenario_convert::{ui_render_location_from_scenario, ui_widget_from_scenario}; use common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; @@ -37,11 +35,11 @@ use utils::channel::{channel, RequestReceiver, RequestSender, Responder}; use crate::model::UiViewEvent; use crate::ui::inline_view_container::{inline_view_action_panel, inline_view_container}; use crate::ui::search_list::search_list; -use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::theme::container::{ContainerStyle, ContainerStyleInner}; use crate::ui::theme::text_input::TextInputStyle; +use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::view_container::view_container; -use crate::ui::widget::{render_root, ActionPanel, ActionPanelItem, ComponentRenderContext, ComponentWidgetEvent}; +use crate::ui::widget::{render_root, ActionPanel, ActionPanelItem, ComponentWidgetEvent}; mod view_container; mod search_list; @@ -57,12 +55,12 @@ mod scroll_handle; mod state; mod hud; -pub use theme::GauntletTheme; use crate::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; use crate::ui::hud::{close_hud_window, show_hud_window}; use crate::ui::scroll_handle::ScrollHandle; use crate::ui::state::{ErrorViewData, Focus, GlobalState, MainViewState, PluginViewData, PluginViewState}; use crate::ui::widget_container::PluginWidgetContainer; +pub use theme::GauntletTheme; pub struct AppModel { // logic @@ -345,14 +343,15 @@ impl Application for AppModel { let ui_widget = ui_widget_from_scenario(container); let has_children = ui_widget.widget_children.len() != 0; - context.replace_view( - render_location, - ui_widget, - &plugin_id, - "Screenshot Plugin", - &entrypoint_id, - "Screenshot Entrypoint", - ); + // TODO + // context.replace_view( + // render_location, + // ui_widget, + // &plugin_id, + // "Screenshot Plugin", + // &entrypoint_id, + // "Screenshot Entrypoint", + // ); let context = Arc::new(StdRwLock::new(context)); @@ -1525,14 +1524,14 @@ impl Application for AppModel { let root = match sub_state { MainViewState::None => { render_root( - false, + &false, input, separator, content, primary_action, action_panel, None::<&ScrollHandle>, - "".to_string(), + "", || AppMsg::ToggleActionPanel { keyboard: false }, |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, @@ -1540,14 +1539,14 @@ impl Application for AppModel { } MainViewState::SearchResultActionPanel { focused_action_item, .. } => { render_root( - true, + &true, input, separator, content, primary_action, action_panel, Some(focused_action_item), - "".to_string(), + "", || AppMsg::ToggleActionPanel { keyboard: false }, |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, @@ -1555,14 +1554,14 @@ impl Application for AppModel { } MainViewState::InlineViewActionPanel { focused_action_item, .. } => { render_root( - true, + &true, input, separator, content, primary_action, action_panel, Some(focused_action_item), - "".to_string(), + "", || AppMsg::ToggleActionPanel { keyboard: false }, |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, @@ -1984,7 +1983,7 @@ async fn request_loop( top_level_view, container } => { - let has_children = container.widget_children.len() != 0; + let has_children = container.content.is_some(); client_context.replace_view( render_location, diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 01f1431..1a7c791 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -3,7 +3,6 @@ use std::path::PathBuf; use iced::{application, Color, Padding}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use serde_json::Error; use common::dirs::Dirs; pub mod button; diff --git a/rust/client/src/ui/view_container.rs b/rust/client/src/ui/view_container.rs index 16fba18..b1a3504 100644 --- a/rust/client/src/ui/view_container.rs +++ b/rust/client/src/ui/view_container.rs @@ -1,16 +1,16 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; -use iced::widget::Component; use iced::widget::component; +use iced::widget::Component; use common::model::{EntrypointId, PhysicalShortcut, PluginId, UiRenderLocation}; -use crate::ui::{AppMsg}; use crate::ui::client_context::ClientContext; use crate::ui::state::PluginViewState; use crate::ui::theme::{Element, GauntletTheme}; -use crate::ui::widget::{ComponentRenderContext, ComponentWidgetEvent}; +use crate::ui::widget::ComponentWidgetEvent; +use crate::ui::AppMsg; pub struct ViewContainer { client_context: Arc>, @@ -61,11 +61,10 @@ impl Component for ViewContainer { fn view(&self, _state: &Self::State) -> Element { let client_context = self.client_context.read().expect("lock is poisoned"); let view_container = client_context.get_view_container(); - view_container.render_widget(ComponentRenderContext::Root { - entrypoint_name: self.entrypoint_name.clone(), - action_shortcuts: self.action_shortcuts.clone(), - plugin_view_state: self.plugin_view_state.clone() - }) + view_container.render_root_widget( + &self.plugin_view_state, + &self.action_shortcuts, + ) } } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 8a44140..79f8ac1 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1,18 +1,14 @@ use std::cell::Cell; use std::collections::HashMap; use std::fmt::{Debug, Display}; -use std::str::FromStr; -use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; -use anyhow::anyhow; -use common::model::{PhysicalKey, PhysicalShortcut, PluginId, UiPropertyValue, UiPropertyValueToEnum, UiPropertyValueToStruct, UiWidgetId}; +use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Icons, Image, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiWidgetId}; use common_ui::shortcut_to_text; use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; -use iced::widget::image::Handle; use iced::widget::text::Shaping; use iced::widget::tooltip::Position; -use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, mouse_area, pick_list, row, scrollable, text, text_input, tooltip, vertical_rule, Space}; +use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, mouse_area, pick_list, row, scrollable, text, text_input, tooltip, vertical_rule, Space}; use iced::{Alignment, Font, Length}; use iced_aw::core::icons; use iced_aw::date_picker::Date; @@ -29,7 +25,6 @@ use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; use crate::ui::theme::date_picker::DatePickerStyle; use crate::ui::theme::grid::GridStyle; -use crate::ui::theme::image::ImageStyle; use crate::ui::theme::pick_list::PickListStyle; use crate::ui::theme::row::RowStyle; use crate::ui::theme::rule::RuleStyle; @@ -39,100 +34,210 @@ use crate::ui::theme::tooltip::TooltipStyle; use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::AppMsg; -#[derive(Clone, Debug)] -pub struct ComponentWidgetWrapper { - id: UiWidgetId, - inner: Arc>, + +#[derive(Debug)] +pub struct ComponentWidgets<'b> { + root_widget: &'b mut Option, + state: &'b mut HashMap } -include!(concat!(env!("OUT_DIR"), "/components.rs")); +impl<'b> ComponentWidgets<'b> { + pub fn new(root_widget: &'b mut Option, state: &'b mut HashMap) -> ComponentWidgets<'b> { + Self { + root_widget, + state + } + } -#[derive(Clone, Debug)] -pub enum ComponentWidgetState { - TextField { - state_value: String - }, - PasswordField { - state_value: String - }, - Checkbox { - state_value: bool - }, - DatePicker { - show_picker: bool, - state_value: Date, - }, - Select { - state_value: Option - }, - SearchBar { - state_value: String - }, - Detail { - show_action_panel: bool, - }, - Form { - show_action_panel: bool, - }, - List { - show_action_panel: bool, - }, - Grid { - show_action_panel: bool, - }, - None -} + fn text_field_state(&self, widget_id: UiWidgetId) -> &TextFieldState { + let state = self.state.get(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); -impl ComponentWidgetState { - fn create(component_widget: &ComponentWidget) -> Self { - match component_widget { - ComponentWidget::TextField { value, .. } => ComponentWidgetState::TextField { - state_value: value.to_owned().unwrap_or("".to_owned()) - }, - ComponentWidget::PasswordField { value, .. } => ComponentWidgetState::PasswordField { - state_value: value.to_owned().unwrap_or("".to_owned()) - }, - ComponentWidget::Checkbox { value, .. } => ComponentWidgetState::Checkbox { - state_value: value.to_owned().unwrap_or(false) - }, - ComponentWidget::DatePicker { value, .. } => { - let value = value - .to_owned() - .map(|value| parse_date(&value)) - .flatten() - .map(|(year, month, day)| Date::from_ymd(year, month, day)) - .unwrap_or(Date::today()); + match state { + ComponentWidgetState::TextField(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state) + } + } - ComponentWidgetState::DatePicker { - state_value: value, - show_picker: false, - } - }, - ComponentWidget::Select { value, .. } => ComponentWidgetState::Select { - state_value: value.to_owned() - }, - ComponentWidget::SearchBar { value, .. } => ComponentWidgetState::SearchBar { - state_value: value.to_owned().unwrap_or("".to_owned()) - }, - ComponentWidget::Detail { .. } => ComponentWidgetState::Detail { - show_action_panel: false, - }, - ComponentWidget::Form { .. } => ComponentWidgetState::Form { - show_action_panel: false, - }, - ComponentWidget::List { .. } => ComponentWidgetState::List { - show_action_panel: false, - }, - ComponentWidget::Grid { .. } => ComponentWidgetState::Grid { - show_action_panel: false, - }, - _ => ComponentWidgetState::None + fn checkbox_state(&self, widget_id: UiWidgetId) -> &CheckboxState { + let state = self.state.get(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + + match state { + ComponentWidgetState::Checkbox(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state) + } + } + + fn date_picker_state(&self, widget_id: UiWidgetId) -> &DatePickerState { + let state = self.state.get(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + + match state { + ComponentWidgetState::DatePicker(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state) + } + } + + fn select_state(&self, widget_id: UiWidgetId) -> &SelectState { + let state = self.state.get(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + + match state { + ComponentWidgetState::Select(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state) + } + } + + fn root_state(&self, widget_id: UiWidgetId) -> &RootState { + let state = self.state.get(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + + match state { + ComponentWidgetState::Root(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state) + } + } + + fn root_state_mut(&mut self, widget_id: UiWidgetId) -> &mut RootState { + let state = self.state.get_mut(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + + match state { + ComponentWidgetState::Root(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state) } } } + +pub fn create_state(root_widget: &RootWidget) -> HashMap { + let mut result = HashMap::new(); + + match &root_widget.content { + None => {} + Some(members) => { + match members { + RootWidgetMembers::Detail(widget) => { + result.insert(widget.__id__, ComponentWidgetState::root()); + } + RootWidgetMembers::Form(widget) => { + result.insert(widget.__id__, ComponentWidgetState::root()); + + for members in &widget.content.ordered_members { + match members { + FormWidgetOrderedMembers::TextField(widget) => { + result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); + } + FormWidgetOrderedMembers::PasswordField(widget) => { + result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); + } + FormWidgetOrderedMembers::Checkbox(widget) => { + result.insert(widget.__id__, ComponentWidgetState::checkbox(&widget.value)); + } + FormWidgetOrderedMembers::DatePicker(widget) => { + result.insert(widget.__id__, ComponentWidgetState::date_picker(&widget.value)); + } + FormWidgetOrderedMembers::Select(widget) => { + result.insert(widget.__id__, ComponentWidgetState::select(&widget.value)); + } + FormWidgetOrderedMembers::Separator(_) => {} + } + } + } + RootWidgetMembers::List(widget) => { + result.insert(widget.__id__, ComponentWidgetState::root()); + + if let Some(widget) = &widget.content.search_bar { + result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); + } + } + RootWidgetMembers::Grid(widget) => { + result.insert(widget.__id__, ComponentWidgetState::root()); + + if let Some(widget) = &widget.content.search_bar { + result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); + } + } + RootWidgetMembers::Inline(_) => {} + } + } + } + + result +} + #[derive(Debug, Clone)] -pub enum ComponentRenderContext { +pub enum ComponentWidgetState { + TextField(TextFieldState), + Checkbox(CheckboxState), + DatePicker(DatePickerState), + Select(SelectState), + Root(RootState), +} + +#[derive(Debug, Clone)] +struct TextFieldState { + state_value: String +} + +#[derive(Debug, Clone)] +struct CheckboxState { + state_value: bool +} + +#[derive(Debug, Clone)] +struct DatePickerState { + show_picker: bool, + state_value: Date, +} + +#[derive(Debug, Clone)] +struct SelectState { + state_value: Option +} + +#[derive(Debug, Clone)] +struct RootState { + show_action_panel: bool, +} + +impl ComponentWidgetState { + fn root() -> ComponentWidgetState { + ComponentWidgetState::Root(RootState { + show_action_panel: false, + }) + } + + fn text_field(value: &Option) -> ComponentWidgetState { + ComponentWidgetState::TextField(TextFieldState { + state_value: value.to_owned().unwrap_or_default() + }) + } + + fn checkbox(value: &Option) -> ComponentWidgetState { + ComponentWidgetState::Checkbox(CheckboxState { + state_value: value.to_owned().unwrap_or(false) + }) + } + + fn date_picker(value: &Option) -> ComponentWidgetState { + let value = value + .to_owned() + .map(|value| parse_date(&value)) + .flatten() + .map(|(year, month, day)| Date::from_ymd(year, month, day)) + .unwrap_or(Date::today()); + + ComponentWidgetState::DatePicker(DatePickerState { + state_value: value, + show_picker: false, + }) + } + + fn select(value: &Option) -> ComponentWidgetState { + ComponentWidgetState::Select(SelectState { + state_value: value.to_owned() + }) + } +} + +#[derive(Debug, Clone)] +pub enum TextRenderType { None, H1, H2, @@ -140,1179 +245,1429 @@ pub enum ComponentRenderContext { H4, H5, H6, - InlineRoot { - plugin_name: String, - entrypoint_name: String, - }, - Inline, - GridItem, - List { - widget_id: UiWidgetId - }, - Grid { - widget_id: UiWidgetId - }, - Root { - plugin_view_state: PluginViewState, - entrypoint_name: String, - action_shortcuts: HashMap, - }, } -impl ComponentRenderContext { - fn is_content_centered(&self) -> bool { - matches!(self, ComponentRenderContext::Inline | ComponentRenderContext::GridItem) - } -} +impl<'b> ComponentWidgets<'b> { + pub fn toggle_action_panel(&mut self) { + let Some(root_widget) = &self.root_widget else { + return; + }; -impl ComponentWidgetWrapper { - pub fn widget( - id: UiWidgetId, - widget_type: impl Into, - properties: HashMap, - children: Vec - ) -> anyhow::Result { - let widget_type = widget_type.into(); - let widget = create_component_widget(&widget_type, properties, children)?; - let widget_state = ComponentWidgetState::create(&widget); - let widget = ComponentWidgetWrapper::new(id, widget, widget_state); + let Some(content) = &root_widget.content else { + return; + }; - Ok(widget) + let widget_id = match content { + RootWidgetMembers::Detail(widget) => widget.__id__, + RootWidgetMembers::Form(widget) => widget.__id__, + RootWidgetMembers::Inline(widget) => widget.__id__, + RootWidgetMembers::List(widget) => widget.__id__, + RootWidgetMembers::Grid(widget) => widget.__id__, + }; + + let state = self.root_state_mut(widget_id); + + state.show_action_panel = !state.show_action_panel; } - pub fn root(id: UiWidgetId) -> Self { - ComponentWidgetWrapper::new(id, ComponentWidget::Root { children: vec![] }, ComponentWidgetState::None) - } + pub fn get_action_ids(&self) -> Vec { + let Some(root_widget) = &self.root_widget else { + return vec![]; + }; - fn new(id: UiWidgetId, widget: ComponentWidget, state: ComponentWidgetState) -> Self { - Self { - id, - inner: Arc::new(RwLock::new((widget, state))), - } - } + let Some(content) = &root_widget.content else { + return vec![]; + }; - pub fn find_child_with_id(&self, widget_id: UiWidgetId) -> Option { - if self.id == widget_id { - return Some(self.clone()) - } + let actions = match content { + RootWidgetMembers::Detail(widget) => &widget.content.actions, + RootWidgetMembers::Form(widget) => &widget.content.actions, + RootWidgetMembers::Inline(widget) => &widget.content.actions, + RootWidgetMembers::List(widget) => &widget.content.actions, + RootWidgetMembers::Grid(widget) => &widget.content.actions, + }; - self.get_children() - .unwrap_or(vec![]) - .into_iter() - .find_map(|child| child.find_child_with_id(widget_id)) - } - - pub fn find_child_by_type(&self, predicate: &dyn Fn(&ComponentWidget) -> bool) -> Option { - self.get_children() - .unwrap_or(vec![]) - .into_iter() - .find_map(|child| { - let (widget, _) = &*child.get(); - if predicate(widget) { - Some(child.clone()) - } else { - child.find_child_by_type(&predicate) + let mut result = vec![]; + match actions { + None => {} + Some(widget) => { + for members in &widget.content.ordered_members { + match members { + ActionPanelWidgetOrderedMembers::Action(widget) => { + result.push(widget.__id__) + } + ActionPanelWidgetOrderedMembers::ActionPanelSection(widget) => { + for members in &widget.content.ordered_members { + match members { + ActionPanelSectionWidgetOrderedMembers::Action(widget) => { + result.push(widget.__id__) + } + } + } + } + } } - }) - } - - pub fn toggle_action_panel(&self) { - { - let (_, ref mut state) = &mut *self.get_mut(); - - match state { - ComponentWidgetState::Detail { show_action_panel, .. } => { - *show_action_panel = !*show_action_panel; - }, - ComponentWidgetState::Form { show_action_panel, .. } => { - *show_action_panel = !*show_action_panel; - }, - ComponentWidgetState::List { show_action_panel, .. } => { - *show_action_panel = !*show_action_panel; - }, - ComponentWidgetState::Grid { show_action_panel, .. } => { - *show_action_panel = !*show_action_panel; - }, - _ => {} - }; + } } - self.get_children() - .unwrap_or(vec![]) - .iter() - .for_each(|child| child.toggle_action_panel()); - } - - pub fn get_all_widgets(&self) -> Vec { - let mut result: Vec<_> = self.get_children() - .unwrap_or(vec![]) - .iter() - .flat_map(|component| component.get_all_widgets()) - .collect(); - - result.push(self.clone()); - result } - pub fn get_action_ids(&self) -> Vec { - self.get_all_widgets() - .into_iter() - .filter(|component| { - let (widget, _) = &*component.get(); - - matches!(widget, ComponentWidget::Action { .. }) - }) - .map(|component| component.id) - .collect() - } - pub fn keyboard_navigation_width(&self) -> Option { - self.get_all_widgets() - .into_iter() - .find_map(|component| { - let (widget, _) = &*component.get(); + let Some(root_widget) = &self.root_widget else { + return None; + }; - match widget { - ComponentWidget::Grid { columns, .. } => Some(grid_width(columns)), - ComponentWidget::List { .. } => Some(1), - _ => None - } - }) + let Some(content) = &root_widget.content else { + return None; + }; + + match content { + RootWidgetMembers::List(_) => Some(1), + RootWidgetMembers::Grid(GridWidget { columns, .. }) => Some(grid_width(columns)), + _ => None + } } pub fn keyboard_navigation_total(&self) -> usize { - self.get_all_widgets() - .into_iter() - .filter(|component| { - let (widget, _) = &*component.get(); + let Some(root_widget) = &self.root_widget else { + return 0; + }; - // TODO this will produce incorrect results if grid uses item list and vice versa - matches!(widget, ComponentWidget::GridItem { .. } | ComponentWidget::ListItem { .. }) - }) - .count() + let Some(content) = &root_widget.content else { + return 0; + }; + + match content { + RootWidgetMembers::List(widget) => { + widget.content.ordered_members + .iter() + .flat_map(|members| { + match members { + ListWidgetOrderedMembers::ListItem(widget) => vec![widget], + ListWidgetOrderedMembers::ListSection(widget) => { + widget.content.ordered_members + .iter() + .map(|members| { + match members { + ListSectionWidgetOrderedMembers::ListItem(widget) => widget, + } + }) + .collect() + } + } + }) + .count() + } + RootWidgetMembers::Grid(widget) => { + widget.content.ordered_members + .iter() + .flat_map(|members| { + match members { + GridWidgetOrderedMembers::GridItem(widget) => vec![widget], + GridWidgetOrderedMembers::GridSection(widget) => { + widget.content.ordered_members + .iter() + .map(|members| { + match members { + GridSectionWidgetOrderedMembers::GridItem(widget) => widget, + } + }) + .collect() + } + } + }) + .count() + } + _ => 0 + } } pub fn has_search_bar(&self) -> bool { - self.get_all_widgets() - .into_iter() - .find(|component| { - let (widget, _) = &*component.get(); + let Some(root_widget) = &self.root_widget else { + return false; + }; - match widget { - ComponentWidget::SearchBar { .. } => true, - _ => false - } - }) - .is_some() - } + let Some(content) = &root_widget.content else { + return false; + }; - fn get(&self) -> RwLockReadGuard<'_, (ComponentWidget, ComponentWidgetState)> { - self.inner.read().expect("lock is poisoned") - } - - fn get_mut(&self) -> RwLockWriteGuard<'_, (ComponentWidget, ComponentWidgetState)> { - self.inner.write().expect("lock is poisoned") + match content { + RootWidgetMembers::List(widget) => widget.content.search_bar.is_some(), + RootWidgetMembers::Grid(widget) => widget.content.search_bar.is_some(), + _ => false + } } pub fn get_action_panel(&self, action_shortcuts: &HashMap) -> Option { - self.find_child_by_type(&|widget| matches!(widget, ComponentWidget::ActionPanel { .. })) - .map(|widget| convert_action_panel(&[widget], action_shortcuts)) - .flatten() + let Some(root_widget) = &self.root_widget else { + return None; + }; + + let Some(content) = &root_widget.content else { + return None; + }; + + match content { + RootWidgetMembers::Detail(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), + RootWidgetMembers::Form(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), + RootWidgetMembers::Inline(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), + RootWidgetMembers::List(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), + RootWidgetMembers::Grid(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), + } } - pub fn render_widget<'a>(&self, context: ComponentRenderContext) -> Element<'a, ComponentWidgetEvent> { - let widget_id = self.id; - let (widget, state) = &*self.get(); - match widget { - ComponentWidget::TextPart { value } => render_text_part(value, context), - ComponentWidget::ActionPanel { .. } | ComponentWidget::Action { .. } | ComponentWidget::ActionPanelSection { .. } => { - unreachable!() - } - ComponentWidget::MetadataTagItem { children } => { - let content: Element<_> = render_children_string(children, ComponentRenderContext::None); + fn render_text<'a>(&self, value: &[String], context: TextRenderType) -> Element<'a, ComponentWidgetEvent> { + let header = match context { + TextRenderType::None => None, + TextRenderType::H1 => Some(34), + TextRenderType::H2 => Some(30), + TextRenderType::H3 => Some(24), + TextRenderType::H4 => Some(20), + TextRenderType::H5 => Some(18), + TextRenderType::H6 => Some(16), + }; - let tag: Element<_> = button(content) - .on_press(ComponentWidgetEvent::TagClick { widget_id }) - .themed(ButtonStyle::MetadataTagItem); + let mut text = text(value.join("")) + .shaping(Shaping::Advanced); - container(tag) - .themed(ContainerStyle::MetadataTagItem) - } - ComponentWidget::MetadataTagList { label, children } => { - let value = wrap_horizontal(render_children(children, ComponentRenderContext::None)) - .into(); + if let Some(size) = header { + text = text + .size(size) + .font(Font { + weight: Weight::Bold, + ..Font::DEFAULT + }) + } - render_metadata_item(label, value) + text.into() + } + + pub fn render_root_widget<'a>( + &self, + plugin_view_state: &PluginViewState, + entrypoint_name: Option<&String>, + action_shortcuts: &HashMap, + ) -> Element<'a, ComponentWidgetEvent> { + match &self.root_widget { + None => { + horizontal_space() .into() } - ComponentWidget::MetadataLink { label, children, href } => { - let content: Element<_> = render_children_string(children, ComponentRenderContext::None); - - let icon: Element<_> = text(icons::Bootstrap::BoxArrowUpRight) - .font(icons::BOOTSTRAP_FONT) - .size(16) - .into(); - - let icon = container(icon) - .themed(ContainerStyle::MetadataLinkIcon); - - let content: Element<_> = row([content, icon]) - .align_items(Alignment::Center) - .into(); - - let link: Element<_> = button(content) - .on_press(ComponentWidgetEvent::LinkClick { widget_id, href: href.to_owned() }) - .themed(ButtonStyle::MetadataLink); - - let content: Element<_> = if href.is_empty() { - link - } else { - let href: Element<_> = text(href) - .shaping(Shaping::Advanced) - .into(); - - tooltip(link, href, Position::Top) - .themed(TooltipStyle::Tooltip) - }; - - render_metadata_item(label, content) - .into() - } - ComponentWidget::MetadataValue { label, children} => { - let value: Element<_> = render_children_string(children, ComponentRenderContext::None); - - render_metadata_item(label, value) - .into() - } - ComponentWidget::MetadataIcon { label, icon} => { - let value = text(icon_to_bootstrap(icon)) - .font(icons::BOOTSTRAP_FONT) - .size(26) - .into(); - - render_metadata_item(label, value) - .into() - } - ComponentWidget::MetadataSeparator => { - let separator: Element<_> = horizontal_rule(1) - .into(); - - container(separator) - .width(Length::Fill) - .themed(ContainerStyle::MetadataSeparator) - } - ComponentWidget::Metadata { children } => { - let metadata: Element<_> = column(render_children(children, ComponentRenderContext::None)) - .into(); - - let metadata = container(metadata) - .width(Length::Fill) - .themed(ContainerStyle::MetadataInner); - - scrollable(metadata) - .width(Length::Fill) - .into() - } - ComponentWidget::Paragraph { children } => { - let centered = context.is_content_centered(); - - let paragraph: Element<_> = render_children_string(children, ComponentRenderContext::None); - - let mut content = container(paragraph) - .width(Length::Fill); - - if centered { - content = content.center_x() - } - - content.themed(ContainerStyle::ContentParagraph) - } - ComponentWidget::Image { source } => { - let centered = context.is_content_centered(); - - let content: Element<_> = match source { - ImageSource::_0(bytes) => { - image(Handle::from_memory(bytes.clone())) - .into() - } - ImageSource::_1(icon) => { - text(icon_to_bootstrap(icon)) - .font(icons::BOOTSTRAP_FONT) // TODO size, height and width - .into() - } - }; - - let mut content = container(content) - .width(Length::Fill); - - if centered { - content = content.center_x() - } - - content.themed(ContainerStyle::ContentImage) - } - ComponentWidget::H1 { children } => { - render_children_string(children, ComponentRenderContext::H1) - } - ComponentWidget::H2 { children } => { - render_children_string(children, ComponentRenderContext::H2) - } - ComponentWidget::H3 { children } => { - render_children_string(children, ComponentRenderContext::H3) - } - ComponentWidget::H4 { children } => { - render_children_string(children, ComponentRenderContext::H4) - } - ComponentWidget::H5 { children } => { - render_children_string(children, ComponentRenderContext::H5) - } - ComponentWidget::H6 { children } => { - render_children_string(children, ComponentRenderContext::H6) - } - ComponentWidget::HorizontalBreak => { - let separator: Element<_> = horizontal_rule(1).into(); - - container(separator) - .width(Length::Fill) - .themed(ContainerStyle::ContentHorizontalBreak) - } - ComponentWidget::CodeBlock { children } => { - let content: Element<_> = render_children_string(children, ComponentRenderContext::None); - - let content = container(content) - .width(Length::Fill) - .themed(ContainerStyle::ContentCodeBlockText); - - container(content) - .width(Length::Fill) - .themed(ContainerStyle::ContentCodeBlock) - } - ComponentWidget::Content { children } => { - let centered = context.is_content_centered(); - - let content: Element<_> = column(render_children(children, context)) - .into(); - - if centered { - container(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } else { - content - } - } - ComponentWidget::Detail { children, isLoading: is_loading } => { - let ComponentWidgetState::Detail { show_action_panel } = *state else { - panic!("unexpected state kind {:?}", state) - }; - - let is_in_list = matches!(context, ComponentRenderContext::List { .. }); - - let metadata_element = render_child_by_type(children, |widget| matches!(widget, ComponentWidget::Metadata { .. }), ComponentRenderContext::None) - .map(|metadata_element| { - container(metadata_element) - .width(if is_in_list { Length::Fill } else { Length::FillPortion(2) }) - .height(if is_in_list { Length::FillPortion(3) } else { Length::Fill }) - .themed(ContainerStyle::DetailMetadata) - }) - .ok(); - - let content_element = render_child_by_type(children, |widget| matches!(widget, ComponentWidget::Content { .. }), ComponentRenderContext::None) - .map(|content_element| { - let content_element: Element<_> = container(content_element) - .width(Length::Fill) - .themed(ContainerStyle::DetailContentInner); - - let content_element: Element<_> = scrollable(content_element) - .width(Length::Fill) - .into(); - - let content_element: Element<_> = container(content_element) - .width(if is_in_list { Length::Fill } else { Length::FillPortion(3) }) - .height(if is_in_list { Length::FillPortion(5) } else { Length::Fill }) - .themed(ContainerStyle::DetailContent); - - content_element - }) - .ok(); - - let separator = if is_in_list { - horizontal_rule(1) - .into() - } else { - vertical_rule(1) - .into() - }; - - let list_fn = |vec| { - if is_in_list { - column(vec) - .into() - } else { - row(vec) - .into() - } - }; - - let content: Element<_> = match (content_element, metadata_element) { - (Some(content_element), Some(metadata_element)) => { - list_fn(vec![content_element, separator, metadata_element]) - } - (Some(content_element), None) => { - list_fn(vec![content_element]) - } - (None, Some(metadata_element)) => { - list_fn(vec![metadata_element]) - } - (None, None) => { - list_fn(vec![]) - } - }; - - if is_in_list { - content - } else { - render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) - } - } - ComponentWidget::Root { children } => { - row(render_children(children, context)) - .into() - } - ComponentWidget::TextField { .. } => { - let ComponentWidgetState::TextField { state_value } = state else { - panic!("unexpected state kind {:?}", state) - }; - - text_input("", state_value) - .on_input(move |value| ComponentWidgetEvent::OnChangeTextField { widget_id, value }) - .themed(TextInputStyle::FormInput) - } - ComponentWidget::PasswordField { .. } => { - let ComponentWidgetState::PasswordField { state_value } = state else { - panic!("unexpected state kind {:?}", state) - }; - - text_input("", state_value) - .secure(true) - .on_input(move |value| ComponentWidgetEvent::OnChangePasswordField { widget_id, value }) - .themed(TextInputStyle::FormInput) - } - ComponentWidget::Checkbox { title, .. } => { - let ComponentWidgetState::Checkbox { state_value } = state else { - panic!("unexpected state kind {:?}", state) - }; - - checkbox(title.clone().unwrap_or_default(), state_value.to_owned()) - .on_toggle(move |value| ComponentWidgetEvent::ToggleCheckbox { widget_id, value }) - .into() - } - ComponentWidget::DatePicker { .. } => { - let ComponentWidgetState::DatePicker { state_value, show_picker } = state else { - panic!("unexpected state kind {:?}", state) - }; - - let button_text = text(state_value.to_string()) - .shaping(Shaping::Advanced); - - let button = button(button_text) - .on_press(ComponentWidgetEvent::ToggleDatePicker { widget_id }); - - // TODO unable to customize buttons here, split to separate button styles - // DatePickerUnderlay, - // DatePickerOverlay, - - date_picker( - show_picker.to_owned(), - state_value.to_owned(), - button, - ComponentWidgetEvent::CancelDatePicker { widget_id }, - move |date| { - ComponentWidgetEvent::SubmitDatePicker { - widget_id, - value: date.to_string(), - } - } - ).themed(DatePickerStyle::Default) - } - ComponentWidget::SelectItem { .. } => { - panic!("parent select component takes care of rendering") - } - ComponentWidget::Select { children, .. } => { - let items: Vec<_> = children.iter() - .map(|child| { - let (widget, _) = &*child.get(); - - let ComponentWidget::SelectItem { children, value } = widget else { - panic!("unexpected widget kind {:?}", widget) - }; - - let label = children.iter() - .map(|child| { - let (widget, _) = &*child.get(); - let ComponentWidget::TextPart { value } = widget else { - panic!("unexpected widget kind {:?}", widget) - }; - - value.to_owned() - }) - .collect::>() - .join(""); - - SelectItem { - value: value.to_owned(), - label - } - }) - .collect(); - - let ComponentWidgetState::Select { state_value } = state else { - panic!("unexpected state kind {:?}", state) - }; - - let state_value = state_value.clone() - .map(|value| items.iter().find(|item| item.value == value)) - .flatten() - .map(|value| value.clone()); - - pick_list( - items, - state_value, - move |item| ComponentWidgetEvent::SelectPickList { widget_id, value: item.value } - ).themed(PickListStyle::Default) - } - ComponentWidget::Separator => { - horizontal_rule(1) - .into() - } - ComponentWidget::Form { children, isLoading: is_loading } => { - let ComponentWidgetState::Form { show_action_panel } = *state else { - panic!("unexpected state kind {:?}", state) - }; - - let items: Vec> = children.iter() - .flat_map(|child| { - let (widget, _) = &*child.get(); - - match widget { - ComponentWidget::Separator => Some(child.render_widget(ComponentRenderContext::None)), - ComponentWidget::ActionPanel { .. } => None, - _ => { - let label = match widget { - ComponentWidget::TextField { label, .. } => label.clone(), - ComponentWidget::PasswordField { label, .. } => label.clone(), - ComponentWidget::Checkbox { label, .. } => label.clone(), - ComponentWidget::DatePicker { label, .. } => label.clone(), - ComponentWidget::Select { label, .. } => label.clone(), - _ => None - }; - - let before_or_label: Element<_> = match label { - None => { - Space::with_width(Length::FillPortion(2)) - .into() - } - Some(label) => { - let label: Element<_> = text(label) - .shaping(Shaping::Advanced) - .horizontal_alignment(Horizontal::Right) - .width(Length::Fill) - .into(); - - container(label) - .width(Length::FillPortion(2)) - .themed(ContainerStyle::FormInputLabel) - } - }; - - let form_input = container(child.render_widget(ComponentRenderContext::None)) - .width(Length::FillPortion(3)) - .into(); - - let after = Space::with_width(Length::FillPortion(2)) - .into(); - - let content = vec![ - before_or_label, - form_input, - after, - ]; - - let row: Element<_> = row(content) - .align_items(Alignment::Center) - .themed(RowStyle::FormInput); - - Some(row) - } - } - }) - .collect(); - - let content: Element<_> = column(items) - .into(); - - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::FormInner); - - let content: Element<_> = scrollable(content) - .width(Length::Fill) - .into(); - - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::Form); - - render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) - } - ComponentWidget::InlineSeparator { icon } => { - match icon { - None => vertical_rule(1).into(), - Some(icon) => { - let top_rule: Element<_> = vertical_rule(1) - .into(); - - let top_rule = container(top_rule) - .center_x() - .into(); - - let icon = text(icon_to_bootstrap(icon)) - .font(icons::BOOTSTRAP_FONT) - .size(45) - .themed(TextStyle::InlineSeparator); - - let bot_rule: Element<_> = vertical_rule(1) - .into(); - - let bot_rule = container(bot_rule) - .center_x() - .into(); - - column([top_rule, icon, bot_rule]) - .align_items(Alignment::Center) - .into() - } - } - } - ComponentWidget::Inline { children } => { - let ComponentRenderContext::InlineRoot { plugin_name, entrypoint_name } = context else { - panic!("not supposed to be passed to root item: {:?}", context) - }; - - let name: Element<_> = text(format!("{} - {}", plugin_name, entrypoint_name)) - .shaping(Shaping::Advanced) - .themed(TextStyle::InlineName); - - let name: Element<_> = container(name) - .themed(ContainerStyle::InlineName); - - let content: Vec> = children - .into_iter() - .filter_map(|child| { - let (widget, _) = &*child.get(); - - match widget { - ComponentWidget::InlineSeparator { .. } => { - Some(child.render_widget(ComponentRenderContext::None)) - } - ComponentWidget::Content { .. } => { - let element = child.render_widget(ComponentRenderContext::Inline); - - let container = container(element) - .width(Length::Fill) - .into(); - - Some(container) - } - _ => None - } - }) - .collect(); - - let content: Element<_> = row(content) - .into(); - - let content: Element<_> = container(content) - .themed(ContainerStyle::InlineInner); - - let content: Element<_> = column(vec![name, content]) - .width(Length::Fill) - .into(); - - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::Inline); - - content - } - ComponentWidget::EmptyView { title, description, image: empty_view_image } => { - let image: Option> = empty_view_image.as_ref() - .map(|empty_view_image| { - match empty_view_image { - EmptyViewImage::_0(bytes) => { - image(Handle::from_memory(bytes.clone())) - .themed(ImageStyle::EmptyViewImage) - } - EmptyViewImage::_1(icon) => { - text(icon_to_bootstrap(icon)) - .font(icons::BOOTSTRAP_FONT) // TODO size, height and width - .into() - } - } - }); - - let title: Element<_> = text(title) - .shaping(Shaping::Advanced) - .into(); - - let subtitle: Element<_> = match description { - None => horizontal_space().into(), - Some(subtitle) => { - text(subtitle) - .shaping(Shaping::Advanced) - .themed(TextStyle::EmptyViewSubtitle) - } - }; - - let mut content = vec![title, subtitle]; - if let Some(image) = image { - - let image: Element<_> = container(image) - .themed(ContainerStyle::EmptyViewImage); - - content.insert(0, image) - } - - let content: Element<_> = column(content) - .align_items(Alignment::Center) - .into(); - - container(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } - ComponentWidget::IconAccessory { icon, tooltip: tooltip_text } => { - let icon: Element<_> = match icon { - IconAccessoryIcon::_0(bytes) => { - image(Handle::from_memory(bytes.clone())) - .into() - }, - IconAccessoryIcon::_1(icon) => { - text(icon_to_bootstrap(icon)) - .font(icons::BOOTSTRAP_FONT) - .themed(TextStyle::IconAccessory) - } - }; - - let content = container(icon) - .center_x() - .center_y() - .themed(ContainerStyle::IconAccessory); - - match tooltip_text.as_ref() { - None => content, - Some(tooltip_text) => { - let tooltip_text: Element<_> = text(tooltip_text) - .shaping(Shaping::Advanced) - .into(); - - tooltip(content, tooltip_text, Position::Top) - .themed(TooltipStyle::Tooltip) - } - } - }, - ComponentWidget::TextAccessory { text: text_value, icon, tooltip: tooltip_text } => { - let icon: Option> = icon.as_ref() - .map(|icon| { - match icon { - TextAccessoryIcon::_0(bytes) => { - image(Handle::from_memory(bytes.clone())) - .into() - }, - TextAccessoryIcon::_1(icon) => { - let icon = icon.to_owned(); - text(icon_to_bootstrap(icon)) - .font(icons::BOOTSTRAP_FONT) - .themed(TextStyle::TextAccessory) - } - } - }); - - let text_content: Element<_> = text(text_value) - .shaping(Shaping::Advanced) - .themed(TextStyle::TextAccessory); - - let mut content: Vec> = vec![]; - - if let Some(icon) = icon { - let icon: Element<_> = container(icon) - .themed(ContainerStyle::TextAccessoryIcon); - - content.push(icon) - } - - content.push(text_content); - - let content: Element<_> = row(content) - .align_items(Alignment::Center) - .into(); - - let content = container(content) - .center_x() - .center_y() - .themed(ContainerStyle::TextAccessory); - - match tooltip_text.as_ref() { - None => content, - Some(tooltip_text) => { - let tooltip_text: Element<_> = text(tooltip_text) - .shaping(Shaping::Advanced) - .into(); - - tooltip(content, tooltip_text, Position::Top) - .themed(TooltipStyle::Tooltip) - } - } - }, - ComponentWidget::ListItem { title, subtitle, icon, children } => { - let accessories = render_children_by_type(children, |widget| matches!(widget, ComponentWidget::TextAccessory { .. } | ComponentWidget::IconAccessory { .. }), ComponentRenderContext::None); - - let icon: Option> = icon.as_ref() - .map(|icon| { - match icon { - ListItemIcon::_0(bytes) => { - image(Handle::from_memory(bytes.clone())) - .into() - }, - ListItemIcon::_1(icon) => { - let icon = icon.to_owned(); - text(icon_to_bootstrap(icon)) - .font(icons::BOOTSTRAP_FONT) - .into() - } - } - }); - - let title: Element<_> = text(title) - .shaping(Shaping::Advanced) - .into(); - let title: Element<_> = container(title) - .themed(ContainerStyle::ListItemTitle); - - let mut content = vec![title]; - - if let Some(icon) = icon { - let icon: Element<_> = container(icon) - .themed(ContainerStyle::ListItemIcon); - - content.insert(0, icon) - } - - if let Some(subtitle) = subtitle { - let subtitle: Element<_> = text(subtitle) - .shaping(Shaping::Advanced) - .themed(TextStyle::ListItemSubtitle); - let subtitle: Element<_> = container(subtitle) - .themed(ContainerStyle::ListItemSubtitle); - - content.push(subtitle) - } - - if accessories.len() > 0 { - let accessories: Element<_> = row(accessories) - .into(); - - let space = horizontal_space() - .into(); - - content.push(space); - content.push(accessories); - } - - let content: Element<_> = row(content) - .align_items(Alignment::Center) - .into(); - - let style = if false { - ButtonStyle::ListItemFocused - } else { - ButtonStyle::ListItem - }; - - button(content) - .on_press(ComponentWidgetEvent::ListItemClick { widget_id }) - .width(Length::Fill) - .themed(style) - } - ComponentWidget::ListSection { children, title, subtitle } => { - let content = render_children(children, context); - - let content = column(content) - .into(); - - render_section(content, Some(title), subtitle, RowStyle::ListSectionTitle, TextStyle::ListSectionTitle, TextStyle::ListSectionSubtitle) - } - ComponentWidget::List { children, isLoading: is_loading } => { - let ComponentWidgetState::List { show_action_panel } = *state else { - panic!("unexpected state kind {:?}", state) - }; - - let mut pending: Vec = vec![]; - let mut items: Vec> = vec![]; - - for child in children { - let (widget, _) = &*child.get(); - - match widget { - ComponentWidget::ListItem { .. } => { - pending.push(child.clone()) - }, - ComponentWidget::ListSection { .. } => { - if !pending.is_empty() { - let content: Element<_> = column(render_children(&pending, ComponentRenderContext::List { widget_id })) - .into(); - - items.push(content); - - pending = vec![]; - } - - items.push(child.render_widget(ComponentRenderContext::List { widget_id })) - }, - ComponentWidget::EmptyView { .. } | ComponentWidget::Detail { .. } | ComponentWidget::SearchBar { .. } => {}, - _ => panic!("unexpected widget kind {:?}", widget) - } - } - - if !pending.is_empty() { - let content: Element<_> = column(render_children(&pending, ComponentRenderContext::List { widget_id })) - .into(); - - items.push(content); - } - - let content = if items.is_empty() { - if let Ok(empty_view) = render_child_by_type(children, |child| matches!(child, ComponentWidget::EmptyView { .. }), ComponentRenderContext::None) { - empty_view - } else { + Some(root) => { + match &root.content { + None => { horizontal_space() .into() } - } else { - let content: Element<_> = column(items) - .width(Length::Fill) - .into(); + Some(content) => { + let entrypoint_name = entrypoint_name.expect("entrypoint name should always exist after render"); - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::ListInner); + match content { + RootWidgetMembers::Detail(widget) => { + let RootState { show_action_panel } = self.root_state(widget.__id__); - let content: Element<_> = scrollable(content) - .width(Length::Fill) - .into(); + let content = self.render_detail_widget(widget, false); - let content: Element<_> = container(content) - .width(Length::FillPortion(3)) - .themed(ContainerStyle::List); - - content - }; - - let mut elements = vec![content]; - - if let Ok(detail) = render_child_by_type(children, |child| matches!(child, ComponentWidget::Detail { .. }), ComponentRenderContext::List { widget_id }) { - - let detail: Element<_> = container(detail) - .width(Length::FillPortion(5)) - .into(); - - let separator: Element<_> = vertical_rule(1) - .into(); - - elements.push(separator); - - elements.push(detail); - } - - let content: Element<_> = row(elements) - .height(Length::Fill) - .into(); - - render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) - } - ComponentWidget::GridItem { children, title, subtitle } => { - // TODO should be just one - let accessories = render_children_by_type(children, |widget| matches!(widget, ComponentWidget::IconAccessory { .. }), ComponentRenderContext::None); - - let content: Element<_> = column(render_children_by_type(children, |widget| matches!(widget, ComponentWidget::Content { .. }), ComponentRenderContext::GridItem)) - .height(130) // TODO dynamic height - .into(); - - let style = if false { - ButtonStyle::GridItemFocused - } else { - ButtonStyle::GridItem - }; - - let content: Element<_> = button(content) - .on_press(ComponentWidgetEvent::GridItemClick { widget_id }) - .width(Length::Fill) - .themed(style); - - let mut sub_content_left = vec![]; - - if let Some(title) = title { - // TODO text truncation when iced supports it - let title = text(title) - .shaping(Shaping::Advanced) - .themed(TextStyle::GridItemTitle); - - sub_content_left.push(title); - } - - if let Some(subtitle) = subtitle { - let subtitle = text(subtitle) - .shaping(Shaping::Advanced) - .themed(TextStyle::GridItemSubTitle); - - sub_content_left.push(subtitle); - } - - let mut sub_content_right = vec![]; - if accessories.len() > 0 { - let accessories: Element<_> = row(accessories) - .into(); - - sub_content_right.push(accessories); - } - - let sub_content_left: Element<_> = column(sub_content_left) - .width(Length::Fill) - .into(); - - let sub_content_right: Element<_> = column(sub_content_right) - .width(Length::Shrink) - .into(); - - let sub_content: Element<_> = row(vec![sub_content_left, sub_content_right]) - .themed(RowStyle::GridItemTitle); - - let content: Element<_> = column(vec![content, sub_content]) - .width(Length::Fill) - .into(); - - content - } - ComponentWidget::GridSection { children, title, subtitle, columns } => { - let content = render_grid(children, columns, context); - - render_section(content, Some(title), subtitle, RowStyle::GridSectionTitle, TextStyle::GridSectionTitle, TextStyle::GridSectionSubtitle) - } - ComponentWidget::Grid { children, columns, isLoading: is_loading } => { - let ComponentWidgetState::Grid { show_action_panel } = *state else { - panic!("unexpected state kind {:?}", state) - }; - - let mut pending: Vec = vec![]; - let mut items: Vec> = vec![]; - - for child in children { - let (widget, _) = &*child.get(); - - match widget { - ComponentWidget::GridItem { .. } => { - pending.push(child.clone()) - }, - ComponentWidget::GridSection { .. } => { - if !pending.is_empty() { - let content = render_grid(&pending, columns, ComponentRenderContext::Grid { widget_id }); - - items.push(content); - - pending = vec![]; + self.render_plugin_root( + show_action_panel, + widget.__id__, + &None, + &widget.content.actions, + content, + widget.is_loading.unwrap_or(false), + plugin_view_state, + entrypoint_name, + action_shortcuts, + ) + }, + RootWidgetMembers::Form(widget) => self.render_form_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts), + RootWidgetMembers::List(widget) => self.render_list_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts), + RootWidgetMembers::Grid(widget) => self.render_grid_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts), + _ => { + panic!("used inline widget in non-inline place") } - - items.push(child.render_widget(ComponentRenderContext::Grid { widget_id })) - }, - ComponentWidget::EmptyView { .. } | ComponentWidget::SearchBar { .. } => {}, - _ => panic!("unexpected widget kind {:?}", widget) + } } } - - if !pending.is_empty() { - let content = render_grid(&pending, columns, ComponentRenderContext::Grid { widget_id }); - - items.push(content); - } - - let content: Element<_> = column(items) - .into(); - - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::GridInner); - - let content: Element<_> = scrollable(content) - .width(Length::Fill) - .into(); - - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::Grid); - - render_plugin_root(show_action_panel, widget_id, children, content, context, is_loading.unwrap_or(false)) - } - ComponentWidget::SearchBar { placeholder, .. } => { - let ComponentWidgetState::SearchBar { state_value } = state else { - panic!("unexpected state kind {:?}", state) - }; - - text_input(placeholder.as_deref().unwrap_or_default(), state_value) - .on_input(move |value| ComponentWidgetEvent::OnChangeSearchBar { widget_id, value }) - .themed(TextInputStyle::PluginSearchBar) } } } - pub fn get_children(&self) -> anyhow::Result> { - get_component_widget_children(&self) + pub fn render_root_inline_widget<'a>(&self, plugin_name: Option<&String>, entrypoint_name: Option<&String>) -> Element<'a, ComponentWidgetEvent> { + match &self.root_widget { + None => { + horizontal_space() + .into() + } + Some(root) => { + match &root.content { + None => { + horizontal_space() + .into() + } + Some(content) => { + match content { + RootWidgetMembers::Inline(widget) => { + let entrypoint_name = entrypoint_name.expect("entrypoint name should always exist after render"); + let plugin_name = plugin_name.expect("entrypoint name should always exist after render"); + + self.render_inline_widget(widget, plugin_name, entrypoint_name) + }, + _ => { + panic!("used non-inline widget in inline place") + } + } + } + } + } + } } - pub fn set_children(&self, new_children: Vec) -> anyhow::Result<()> { - set_component_widget_children(&self, new_children) + fn render_metadata_tag_item_widget<'a>(&self, widget: &MetadataTagItemWidget) -> Element<'a, ComponentWidgetEvent> { + let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); + + let tag: Element<_> = button(content) + .on_press(ComponentWidgetEvent::TagClick { widget_id: widget.__id__ }) + .themed(ButtonStyle::MetadataTagItem); + + container(tag) + .themed(ContainerStyle::MetadataTagItem) + } + + fn render_metadata_tag_list_widget<'a>(&self, widget: &MetadataTagListWidget) -> Element<'a, ComponentWidgetEvent> { + let content = widget.content.ordered_members + .iter() + .map(|members| { + match members { + MetadataTagListWidgetOrderedMembers::MetadataTagItem(content) => self.render_metadata_tag_item_widget(&content) + } + }) + .collect(); + + let value = wrap_horizontal(content) + .into(); + + render_metadata_item(&widget.label, value) + .into() + } + + fn render_metadata_link_widget<'a>(&self, widget: &MetadataLinkWidget) -> Element<'a, ComponentWidgetEvent> { + let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); + + let icon: Element<_> = text(icons::Bootstrap::BoxArrowUpRight) + .font(icons::BOOTSTRAP_FONT) + .size(16) + .into(); + + let icon = container(icon) + .themed(ContainerStyle::MetadataLinkIcon); + + let content: Element<_> = row([content, icon]) + .align_items(Alignment::Center) + .into(); + + let link: Element<_> = button(content) + .on_press(ComponentWidgetEvent::LinkClick { widget_id: widget.__id__, href: widget.href.to_owned() }) + .themed(ButtonStyle::MetadataLink); + + let content: Element<_> = if widget.href.is_empty() { + link + } else { + let href: Element<_> = text(&widget.href) + .shaping(Shaping::Advanced) + .into(); + + tooltip(link, href, Position::Top) + .themed(TooltipStyle::Tooltip) + }; + + render_metadata_item(&widget.label, content) + .into() + } + + fn render_metadata_value_widget<'a>(&self, widget: &MetadataValueWidget) -> Element<'a, ComponentWidgetEvent> { + let value: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); + + render_metadata_item(&widget.label, value) + .into() + } + + fn render_metadata_icon_widget<'a>(&self, widget: &MetadataIconWidget) -> Element<'a, ComponentWidgetEvent> { + let value = text(icon_to_bootstrap(&widget.icon)) + .font(icons::BOOTSTRAP_FONT) + .size(26) + .into(); + + render_metadata_item(&widget.label, value) + .into() + } + + fn render_metadata_separator_widget<'a>(&self, _widget: &MetadataSeparatorWidget) -> Element<'a, ComponentWidgetEvent> { + let separator: Element<_> = horizontal_rule(1) + .into(); + + container(separator) + .width(Length::Fill) + .themed(ContainerStyle::MetadataSeparator) + } + + fn render_metadata_widget<'a>(&self, widget: &MetadataWidget) -> Element<'a, ComponentWidgetEvent> { + let content: Vec> = widget.content.ordered_members + .iter() + .map(|members| { + match members { + MetadataWidgetOrderedMembers::MetadataTagList(content) => self.render_metadata_tag_list_widget(content), + MetadataWidgetOrderedMembers::MetadataLink(content) => self.render_metadata_link_widget(content), + MetadataWidgetOrderedMembers::MetadataValue(content) => self.render_metadata_value_widget(content), + MetadataWidgetOrderedMembers::MetadataIcon(content) => self.render_metadata_icon_widget(content), + MetadataWidgetOrderedMembers::MetadataSeparator(content) => self.render_metadata_separator_widget(content), + } + }) + .collect(); + + let metadata: Element<_> = column(content) + .into(); + + let metadata = container(metadata) + .width(Length::Fill) + .themed(ContainerStyle::MetadataInner); + + scrollable(metadata) + .width(Length::Fill) + .into() + } + + fn render_paragraph_widget<'a>(&self, widget: &ParagraphWidget, centered: bool) -> Element<'a, ComponentWidgetEvent> { + let paragraph: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); + + let mut content = container(paragraph) + .width(Length::Fill); + + if centered { + content = content.center_x() + } + + content.themed(ContainerStyle::ContentParagraph) + } + + fn render_image_widget<'a>(&self, widget: &ImageWidget, centered: bool) -> Element<'a, ComponentWidgetEvent> { + // TODO image size, height and width + let content: Element<_> = render_image(&widget.source, None); + + let mut content = container(content) + .width(Length::Fill); + + if centered { + content = content.center_x() + } + + content.themed(ContainerStyle::ContentImage) + } + + fn render_h1_widget<'a>(&self, widget: &H1Widget) -> Element<'a, ComponentWidgetEvent> { + self.render_text(&widget.content.text, TextRenderType::H1) + } + + fn render_h2_widget<'a>(&self, widget: &H2Widget) -> Element<'a, ComponentWidgetEvent> { + self.render_text(&widget.content.text, TextRenderType::H2) + } + + fn render_h3_widget<'a>(&self, widget: &H3Widget) -> Element<'a, ComponentWidgetEvent> { + self.render_text(&widget.content.text, TextRenderType::H3) + } + + fn render_h4_widget<'a>(&self, widget: &H4Widget) -> Element<'a, ComponentWidgetEvent> { + self.render_text(&widget.content.text, TextRenderType::H4) + } + + fn render_h5_widget<'a>(&self, widget: &H5Widget) -> Element<'a, ComponentWidgetEvent> { + self.render_text(&widget.content.text, TextRenderType::H5) + } + + fn render_h6_widget<'a>(&self, widget: &H6Widget) -> Element<'a, ComponentWidgetEvent> { + self.render_text(&widget.content.text, TextRenderType::H6) + } + + fn render_horizontal_break_widget<'a>(&self, _widget: &HorizontalBreakWidget) -> Element<'a, ComponentWidgetEvent> { + let separator: Element<_> = horizontal_rule(1).into(); + + container(separator) + .width(Length::Fill) + .themed(ContainerStyle::ContentHorizontalBreak) + } + + fn render_code_block_widget<'a>(&self, widget: &CodeBlockWidget) -> Element<'a, ComponentWidgetEvent> { + let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); + + let content = container(content) + .width(Length::Fill) + .themed(ContainerStyle::ContentCodeBlockText); + + container(content) + .width(Length::Fill) + .themed(ContainerStyle::ContentCodeBlock) + } + + fn render_content_widget<'a>(&self, widget: &ContentWidget, centered: bool) -> Element<'a, ComponentWidgetEvent> { + let content: Vec<_> = widget.content.ordered_members + .iter() + .map(|members| { + match members { + ContentWidgetOrderedMembers::Paragraph(widget) => self.render_paragraph_widget(widget, centered), + ContentWidgetOrderedMembers::Image(widget) => self.render_image_widget(widget, centered), + ContentWidgetOrderedMembers::H1(widget) => self.render_h1_widget(widget), + ContentWidgetOrderedMembers::H2(widget) => self.render_h2_widget(widget), + ContentWidgetOrderedMembers::H3(widget) => self.render_h3_widget(widget), + ContentWidgetOrderedMembers::H4(widget) => self.render_h4_widget(widget), + ContentWidgetOrderedMembers::H5(widget) => self.render_h5_widget(widget), + ContentWidgetOrderedMembers::H6(widget) => self.render_h6_widget(widget), + ContentWidgetOrderedMembers::HorizontalBreak(widget) => self.render_horizontal_break_widget(widget), + ContentWidgetOrderedMembers::CodeBlock(widget) => self.render_code_block_widget(widget), + } + }) + .collect(); + + let content: Element<_> = column(content) + .into(); + + if centered { + container(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } else { + content + } + } + + fn render_detail_widget<'a>(&self, widget: &DetailWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { + let metadata_element = widget.content.metadata + .as_ref() + .map(|widget| { + let content = self.render_metadata_widget(widget); + + container(content) + .width(if is_in_list { Length::Fill } else { Length::FillPortion(2) }) + .height(if is_in_list { Length::FillPortion(3) } else { Length::Fill }) + .themed(ContainerStyle::DetailMetadata) + }); + + let content_element = widget.content.content + .as_ref() + .map(|widget| { + let content_element: Element<_> = container(self.render_content_widget(widget, false)) + .width(Length::Fill) + .themed(ContainerStyle::DetailContentInner); + + let content_element: Element<_> = scrollable(content_element) + .width(Length::Fill) + .into(); + + let content_element: Element<_> = container(content_element) + .width(if is_in_list { Length::Fill } else { Length::FillPortion(3) }) + .height(if is_in_list { Length::FillPortion(5) } else { Length::Fill }) + .themed(ContainerStyle::DetailContent); + + content_element + }); + + let separator = if is_in_list { + horizontal_rule(1) + .into() + } else { + vertical_rule(1) + .into() + }; + + let list_fn = |vec| { + if is_in_list { + column(vec) + .into() + } else { + row(vec) + .into() + } + }; + + let content: Element<_> = match (content_element, metadata_element) { + (Some(content_element), Some(metadata_element)) => { + list_fn(vec![content_element, separator, metadata_element]) + } + (Some(content_element), None) => { + list_fn(vec![content_element]) + } + (None, Some(metadata_element)) => { + list_fn(vec![metadata_element]) + } + (None, None) => { + list_fn(vec![]) + } + }; + + content + } + + fn render_text_field_widget<'a>(&self, widget: &TextFieldWidget) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let TextFieldState { state_value } = self.text_field_state(widget.__id__); + + text_input("", state_value) + .on_input(move |value| ComponentWidgetEvent::OnChangeTextField { widget_id, value }) + .themed(TextInputStyle::FormInput) + } + + fn render_password_field_widget<'a>(&self, widget: &PasswordFieldWidget) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let TextFieldState { state_value } = self.text_field_state(widget_id); + + text_input("", state_value) + .secure(true) + .on_input(move |value| ComponentWidgetEvent::OnChangePasswordField { widget_id, value }) + .themed(TextInputStyle::FormInput) + } + + fn render_checkbox_widget<'a>(&self, widget: &CheckboxWidget) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let CheckboxState { state_value } = self.checkbox_state(widget_id); + + checkbox(widget.title.as_deref().unwrap_or_default(), state_value.to_owned()) + .on_toggle(move |value| ComponentWidgetEvent::ToggleCheckbox { widget_id, value }) + .into() + } + + fn render_date_picker_widget<'a>(&self, widget: &DatePickerWidget) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let DatePickerState { state_value, show_picker } = self.date_picker_state(widget.__id__); + + let button_text = text(state_value.to_string()) + .shaping(Shaping::Advanced); + + let button = button(button_text) + .on_press(ComponentWidgetEvent::ToggleDatePicker { widget_id: widget.__id__ }); + + // TODO unable to customize buttons here, split to separate button styles + // DatePickerUnderlay, + // DatePickerOverlay, + + date_picker( + show_picker.to_owned(), + state_value.to_owned(), + button, + ComponentWidgetEvent::CancelDatePicker { widget_id }, + move |date| { + ComponentWidgetEvent::SubmitDatePicker { + widget_id, + value: date.to_string(), + } + }, + ).themed(DatePickerStyle::Default) + } + + fn render_select_widget<'a>(&self, widget: &SelectWidget) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let SelectState { state_value } = self.select_state(widget_id); + + let items: Vec<_> = widget.content.ordered_members + .iter() + .map(|members| { + match members { + SelectWidgetOrderedMembers::SelectItem(widget) => { + SelectItem { + value: widget.value.to_owned(), + label: widget.content.text.join(""), + } + } + } + }) + .collect(); + + let state_value = state_value.clone() + .map(|value| items.iter().find(|item| item.value == value)) + .flatten() + .map(|value| value.clone()); + + pick_list( + items, + state_value, + move |item| ComponentWidgetEvent::SelectPickList { widget_id, value: item.value }, + ).themed(PickListStyle::Default) + } + + fn render_separator_widget<'a>(&self, _widget: &SeparatorWidget) -> Element<'a, ComponentWidgetEvent> { + horizontal_rule(1) + .into() + } + + fn render_form_widget<'a>( + &self, + widget: &FormWidget, + plugin_view_state: &PluginViewState, + entrypoint_name: &str, + action_shortcuts: &HashMap, + ) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let RootState { show_action_panel } = self.root_state(widget_id); + + let items: Vec> = widget.content.ordered_members + .iter() + .map(|members| { + fn render_field<'c, 'd>(field: Element<'c, ComponentWidgetEvent>, label: &'d Option) -> Element<'c, ComponentWidgetEvent> { + let before_or_label: Element<_> = match label { + None => { + Space::with_width(Length::FillPortion(2)) + .into() + } + Some(label) => { + let label: Element<_> = text(label) + .shaping(Shaping::Advanced) + .horizontal_alignment(Horizontal::Right) + .width(Length::Fill) + .into(); + + container(label) + .width(Length::FillPortion(2)) + .themed(ContainerStyle::FormInputLabel) + } + }; + + let form_input = container(field) + .width(Length::FillPortion(3)) + .into(); + + let after = Space::with_width(Length::FillPortion(2)) + .into(); + + let content = vec![ + before_or_label, + form_input, + after, + ]; + + let row: Element<_> = row(content) + .align_items(Alignment::Center) + .themed(RowStyle::FormInput); + + row + } + + match members { + FormWidgetOrderedMembers::Separator(widget) => self.render_separator_widget(widget), + FormWidgetOrderedMembers::TextField(widget) => render_field(self.render_text_field_widget(widget), &widget.label), + FormWidgetOrderedMembers::PasswordField(widget) => render_field(self.render_password_field_widget(widget), &widget.label), + FormWidgetOrderedMembers::Checkbox(widget) => render_field(self.render_checkbox_widget(widget), &widget.label), + FormWidgetOrderedMembers::DatePicker(widget) => render_field(self.render_date_picker_widget(widget), &widget.label), + FormWidgetOrderedMembers::Select(widget) => render_field(self.render_select_widget(widget), &widget.label) + } + }) + .collect(); + + let content: Element<_> = column(items) + .into(); + + let content: Element<_> = container(content) + .width(Length::Fill) + .themed(ContainerStyle::FormInner); + + let content: Element<_> = scrollable(content) + .width(Length::Fill) + .into(); + + let content: Element<_> = container(content) + .width(Length::Fill) + .themed(ContainerStyle::Form); + + self.render_plugin_root( + show_action_panel, + widget_id, + &None, + &widget.content.actions, + content, + widget.is_loading.unwrap_or(false), + plugin_view_state, + entrypoint_name, + action_shortcuts + ) + } + + fn render_inline_separator_widget<'a>(&self, widget: &InlineSeparatorWidget) -> Element<'a, ComponentWidgetEvent> { + match &widget.icon { + None => vertical_rule(1).into(), + Some(icon) => { + let top_rule: Element<_> = vertical_rule(1) + .into(); + + let top_rule = container(top_rule) + .center_x() + .into(); + + let icon = text(icon_to_bootstrap(icon)) + .font(icons::BOOTSTRAP_FONT) + .size(45) + .themed(TextStyle::InlineSeparator); + + let bot_rule: Element<_> = vertical_rule(1) + .into(); + + let bot_rule = container(bot_rule) + .center_x() + .into(); + + column([top_rule, icon, bot_rule]) + .align_items(Alignment::Center) + .into() + } + } + } + + fn render_inline_widget<'a>(&self, widget: &InlineWidget, plugin_name: &str, entrypoint_name: &str) -> Element<'a, ComponentWidgetEvent> { + let name: Element<_> = text(format!("{} - {}", plugin_name, entrypoint_name)) + .shaping(Shaping::Advanced) + .themed(TextStyle::InlineName); + + let name: Element<_> = container(name) + .themed(ContainerStyle::InlineName); + + let content: Vec> = widget.content.ordered_members + .iter() + .map(|members| { + match members { + InlineWidgetOrderedMembers::Content(widget) => self.render_content_widget(widget, true), + InlineWidgetOrderedMembers::InlineSeparator(widget) => { + let element = self.render_inline_separator_widget(widget); + + container(element) + .width(Length::Fill) + .into() + } + } + }) + .collect(); + + let content: Element<_> = row(content) + .into(); + + let content: Element<_> = container(content) + .themed(ContainerStyle::InlineInner); + + let content: Element<_> = column(vec![name, content]) + .width(Length::Fill) + .into(); + + let content: Element<_> = container(content) + .width(Length::Fill) + .themed(ContainerStyle::Inline); + + content + } + + fn render_empty_view_widget<'a>(&self, widget: &EmptyViewWidget) -> Element<'a, ComponentWidgetEvent> { + let image: Option> = widget.image + .as_ref() + .map(|image| render_image(image, Some(TextStyle::EmptyViewSubtitle))); + + let title: Element<_> = text(&widget.title) + .shaping(Shaping::Advanced) + .into(); + + let subtitle: Element<_> = match &widget.description { + None => horizontal_space().into(), + Some(subtitle) => { + text(subtitle) + .shaping(Shaping::Advanced) + .themed(TextStyle::EmptyViewSubtitle) + } + }; + + let mut content = vec![title, subtitle]; + if let Some(image) = image { + let image: Element<_> = container(image) + .themed(ContainerStyle::EmptyViewImage); + + content.insert(0, image) + } + + let content: Element<_> = column(content) + .align_items(Alignment::Center) + .into(); + + container(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } + + + fn render_search_bar_widget<'a>(&self, widget: &SearchBarWidget) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let TextFieldState { state_value } = self.text_field_state(widget_id); + + text_input(widget.placeholder.as_deref().unwrap_or_default(), state_value) + .on_input(move |value| ComponentWidgetEvent::OnChangeSearchBar { widget_id, value }) + .themed(TextInputStyle::PluginSearchBar) + } + + fn render_icon_accessory<'a>(&self, widget: &IconAccessoryWidget) -> Element<'a, ComponentWidgetEvent> { + let icon = render_image(&widget.icon, Some(TextStyle::IconAccessory)); + + let content = container(icon) + .center_x() + .center_y() + .themed(ContainerStyle::IconAccessory); + + match widget.tooltip.as_ref() { + None => content, + Some(tooltip_text) => { + let tooltip_text: Element<_> = text(tooltip_text) + .shaping(Shaping::Advanced) + .into(); + + tooltip(content, tooltip_text, Position::Top) + .themed(TooltipStyle::Tooltip) + } + } + } + + fn render_text_accessory<'a>(&self, widget: &TextAccessoryWidget) -> Element<'a, ComponentWidgetEvent> { + let icon: Option> = widget.icon + .as_ref() + .map(|icon| render_image(icon, Some(TextStyle::TextAccessory))); + + let text_content: Element<_> = text(&widget.text) + .shaping(Shaping::Advanced) + .themed(TextStyle::TextAccessory); + + let mut content: Vec> = vec![]; + + if let Some(icon) = icon { + let icon: Element<_> = container(icon) + .themed(ContainerStyle::TextAccessoryIcon); + + content.push(icon) + } + + content.push(text_content); + + let content: Element<_> = row(content) + .align_items(Alignment::Center) + .into(); + + let content = container(content) + .center_x() + .center_y() + .themed(ContainerStyle::TextAccessory); + + match widget.tooltip.as_ref() { + None => content, + Some(tooltip_text) => { + let tooltip_text: Element<_> = text(tooltip_text) + .shaping(Shaping::Advanced) + .into(); + + tooltip(content, tooltip_text, Position::Top) + .themed(TooltipStyle::Tooltip) + } + } + } + + fn render_list_widget<'a>( + &self, + list_widget: &ListWidget, + plugin_view_state: &PluginViewState, + entrypoint_name: &str, + action_shortcuts: &HashMap, + ) -> Element<'a, ComponentWidgetEvent> { + let widget_id = list_widget.__id__; + let RootState { show_action_panel } = self.root_state(widget_id); + + let mut pending: Vec<&ListItemWidget> = vec![]; + let mut items: Vec> = vec![]; + + for members in &list_widget.content.ordered_members { + match &members { + ListWidgetOrderedMembers::ListItem(widget) => { + pending.push(widget) + }, + ListWidgetOrderedMembers::ListSection(widget) => { + if !pending.is_empty() { + let content: Vec<_> = pending + .iter() + .map(|widget| self.render_list_item_widget(widget)) + .collect(); + + let content: Element<_> = column(content) + .into(); + + items.push(content); + + pending = vec![]; + } + + items.push(self.render_list_section_widget(widget)) + }, + } + } + + if !pending.is_empty() { + let content: Vec<_> = pending + .iter() + .map(|widget| self.render_list_item_widget(widget)) + .collect(); + + let content: Element<_> = column(content) + .into(); + + items.push(content); + } + + let content = if items.is_empty() { + match &list_widget.content.empty_view { + Some(widget) => self.render_empty_view_widget(widget), + None => horizontal_space().into() + } + } else { + let content: Element<_> = column(items) + .width(Length::Fill) + .into(); + + let content: Element<_> = container(content) + .width(Length::Fill) + .themed(ContainerStyle::ListInner); + + let content: Element<_> = scrollable(content) + .width(Length::Fill) + .into(); + + let content: Element<_> = container(content) + .width(Length::FillPortion(3)) + .themed(ContainerStyle::List); + + content + }; + + let mut elements = vec![content]; + + if let Some(detail) = &list_widget.content.detail { + let detail = self.render_detail_widget(detail, true); + + let detail: Element<_> = container(detail) + .width(Length::FillPortion(5)) + .into(); + + let separator: Element<_> = vertical_rule(1) + .into(); + + elements.push(separator); + + elements.push(detail); + } + + let content: Element<_> = row(elements) + .height(Length::Fill) + .into(); + + self.render_plugin_root( + show_action_panel, + widget_id, + &list_widget.content.search_bar, + &list_widget.content.actions, + content, + list_widget.is_loading.unwrap_or(false), + plugin_view_state, + entrypoint_name, + action_shortcuts + ) + } + + fn render_list_section_widget<'a>(&self, widget: &ListSectionWidget) -> Element<'a, ComponentWidgetEvent> { + let content: Vec<_> = widget.content.ordered_members + .iter() + .map(|members| { + match members { + ListSectionWidgetOrderedMembers::ListItem(widget) => self.render_list_item_widget(widget) + } + }) + .collect(); + + let content = column(content) + .into(); + + render_section(content, Some(&widget.title), &widget.subtitle, RowStyle::ListSectionTitle, TextStyle::ListSectionTitle, TextStyle::ListSectionSubtitle) + } + + fn render_list_item_widget<'a>(&self, widget: &ListItemWidget) -> Element<'a, ComponentWidgetEvent> { + let icon: Option> = widget.icon + .as_ref() + .map(|icon| render_image(icon, None)); + + let title: Element<_> = text(&widget.title) + .shaping(Shaping::Advanced) + .into(); + let title: Element<_> = container(title) + .themed(ContainerStyle::ListItemTitle); + + let mut content = vec![title]; + + if let Some(icon) = icon { + let icon: Element<_> = container(icon) + .themed(ContainerStyle::ListItemIcon); + + content.insert(0, icon) + } + + if let Some(subtitle) = &widget.subtitle { + let subtitle: Element<_> = text(subtitle) + .shaping(Shaping::Advanced) + .themed(TextStyle::ListItemSubtitle); + let subtitle: Element<_> = container(subtitle) + .themed(ContainerStyle::ListItemSubtitle); + + content.push(subtitle) + } + + if widget.content.accessories.len() > 0 { + let accessories: Vec> = widget.content.accessories + .iter() + .map(|accessory| { + match accessory { + ListItemAccessories::_0(widget) => self.render_text_accessory(widget), + ListItemAccessories::_1(widget) => self.render_icon_accessory(widget) + } + }) + .collect(); + + let accessories: Element<_> = row(accessories) + .into(); + + let space = horizontal_space() + .into(); + + content.push(space); + content.push(accessories); + } + + let content: Element<_> = row(content) + .align_items(Alignment::Center) + .into(); + + let style = if false { + ButtonStyle::ListItemFocused + } else { + ButtonStyle::ListItem + }; + + button(content) + .on_press(ComponentWidgetEvent::ListItemClick { widget_id: widget.__id__ }) + .width(Length::Fill) + .themed(style) + } + + fn render_grid_widget<'a>( + &self, + grid_widget: &GridWidget, + plugin_view_state: &PluginViewState, + entrypoint_name: &str, + action_shortcuts: &HashMap, + ) -> Element<'a, ComponentWidgetEvent> { + let RootState { show_action_panel } = self.root_state(grid_widget.__id__); + + let mut pending: Vec<&GridItemWidget> = vec![]; + let mut items: Vec> = vec![]; + + for members in &grid_widget.content.ordered_members { + match &members { + GridWidgetOrderedMembers::GridItem(widget) => { + pending.push(widget) + } + GridWidgetOrderedMembers::GridSection(widget) => { + if !pending.is_empty() { + let content = self.render_grid(&pending, &grid_widget.columns); + + items.push(content); + + pending = vec![]; + } + + items.push(self.render_grid_section_widget(widget)) + } + } + } + + if !pending.is_empty() { + let content = self.render_grid(&pending, &grid_widget.columns); + + items.push(content); + } + + let content: Element<_> = column(items) + .into(); + + let content: Element<_> = container(content) + .width(Length::Fill) + .themed(ContainerStyle::GridInner); + + let content: Element<_> = scrollable(content) + .width(Length::Fill) + .into(); + + let content: Element<_> = container(content) + .width(Length::Fill) + .themed(ContainerStyle::Grid); + + self.render_plugin_root( + show_action_panel, + grid_widget.__id__, + &grid_widget.content.search_bar, + &grid_widget.content.actions, + content, + grid_widget.is_loading.unwrap_or(false), + plugin_view_state, + entrypoint_name, + action_shortcuts + ) + } + + fn render_grid_section_widget<'a>(&self, widget: &GridSectionWidget) -> Element<'a, ComponentWidgetEvent> { + let items: Vec<_> = widget.content.ordered_members + .iter() + .map(|members| { + match members { + GridSectionWidgetOrderedMembers::GridItem(widget) => widget + } + }) + .collect(); + + let content = self.render_grid(&items, &widget.columns); + + render_section(content, Some(&widget.title), &widget.subtitle, RowStyle::GridSectionTitle, TextStyle::GridSectionTitle, TextStyle::GridSectionSubtitle) + } + + fn render_grid_item_widget<'a>(&self, widget: &GridItemWidget) -> Element<'a, ComponentWidgetEvent> { + // TODO not needed column element? + let content: Element<_> = column(vec![self.render_content_widget(&widget.content.content, true)]) + .height(130) // TODO dynamic height + .into(); + + let style = if false { + ButtonStyle::GridItemFocused + } else { + ButtonStyle::GridItem + }; + + let content: Element<_> = button(content) + .on_press(ComponentWidgetEvent::GridItemClick { widget_id: widget.__id__ }) + .width(Length::Fill) + .themed(style); + + let mut sub_content_left = vec![]; + + if let Some(title) = &widget.title { + // TODO text truncation when iced supports it + let title = text(title) + .shaping(Shaping::Advanced) + .themed(TextStyle::GridItemTitle); + + sub_content_left.push(title); + } + + if let Some(subtitle) = &widget.subtitle { + let subtitle = text(subtitle) + .shaping(Shaping::Advanced) + .themed(TextStyle::GridItemSubTitle); + + sub_content_left.push(subtitle); + } + + let mut sub_content_right = vec![]; + if let Some(widget) = &widget.content.accessory { + sub_content_right.push(self.render_icon_accessory(widget)); + } + + let sub_content_left: Element<_> = column(sub_content_left) + .width(Length::Fill) + .into(); + + let sub_content_right: Element<_> = column(sub_content_right) + .width(Length::Shrink) + .into(); + + let sub_content: Element<_> = row(vec![sub_content_left, sub_content_right]) + .themed(RowStyle::GridItemTitle); + + let content: Element<_> = column(vec![content, sub_content]) + .width(Length::Fill) + .into(); + + content + } + + fn render_grid<'a>(&self, items: &[&GridItemWidget], /*aspect_ratio: Option<&str>,*/ columns: &Option) -> Element<'a, ComponentWidgetEvent> { + // let (width, height) = match aspect_ratio { + // None => (1, 1), + // Some("1") => (1, 1), + // Some("3/2") => (3, 2), + // Some("2/3") => (2, 3), + // Some("4/3") => (4, 3), + // Some("3/4") => (3, 4), + // Some("16/9") => (16, 9), + // Some("9/16") => (9, 16), + // Some(value) => panic!("unsupported aspect_ratio {:?}", value) + // }; + + let grid_width = grid_width(columns); + + let rows: Vec> = items + .iter() + .map(|widget| self.render_grid_item_widget(widget)) + .chunks(grid_width) + .into_iter() + .map(|row_items| { + let mut row_items: Vec<_> = row_items.collect(); + row_items.resize_with(grid_width, || horizontal_space().into()); + + grid_row(row_items).into() + }) + .collect(); + + let grid: Element<_> = grid(rows) + .width(Length::Fill) + .vertical_alignment(Vertical::Top) + .themed(GridStyle::Default); + + grid + } + + fn render_top_panel<'a>(&self, search_bar: &Option) -> Element<'a, ComponentWidgetEvent> { + let icon = text(icons::Bootstrap::ArrowLeft) + .font(icons::BOOTSTRAP_FONT); + + let back_button: Element<_> = button(icon) + .on_press(ComponentWidgetEvent::PreviousView) + .themed(ButtonStyle::RootTopPanelBackButton); + + let search_bar_element = search_bar + .as_ref() + .map(|widget| self.render_search_bar_widget(widget)) + .unwrap_or_else(|| Space::with_width(Length::FillPortion(3)).into()); + + let top_panel: Element<_> = row(vec![back_button, search_bar_element]) + .align_items(Alignment::Center) + .themed(RowStyle::RootTopPanel); + + let top_panel: Element<_> = container(top_panel) + .width(Length::Fill) + .themed(ContainerStyle::RootTopPanel); + + top_panel + } + + fn render_plugin_root<'a>( + &self, + show_action_panel: &bool, + widget_id: UiWidgetId, + search_bar: &Option, + action_panel: &Option, + content: Element<'a, ComponentWidgetEvent>, + is_loading: bool, + plugin_view_state: &PluginViewState, + entrypoint_name: &str, + action_shortcuts: &HashMap, + ) -> Element<'a, ComponentWidgetEvent> { + + let top_panel = self.render_top_panel(search_bar); + + let top_separator = if is_loading { + LoadingBar::new() + .into() + } else { + horizontal_rule(1) + .into() + }; + + let mut action_panel = convert_action_panel(action_panel, &action_shortcuts); + + let primary_action = action_panel.as_mut() + .map(|panel| panel.find_first()) + .flatten() + .map(|(label, widget_id)| { + let shortcut = PhysicalShortcut { + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false + }; + + (label.to_string(), widget_id, shortcut) + }); + + match plugin_view_state { + PluginViewState::None => { + render_root( + show_action_panel, + top_panel, + top_separator, + content, + primary_action, + action_panel, + None::<&ScrollHandle>, + entrypoint_name, + || ComponentWidgetEvent::ToggleActionPanel { widget_id }, + |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id }, + |widget_id| ComponentWidgetEvent::ActionClick { widget_id } + ) + } + PluginViewState::ActionPanel { focused_action_item } => { + render_root( + show_action_panel, + top_panel, + top_separator, + content, + primary_action, + action_panel, + Some(&focused_action_item), + entrypoint_name, + || ComponentWidgetEvent::ToggleActionPanel { widget_id }, + |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id }, + |widget_id| ComponentWidgetEvent::ActionClick { widget_id } + ) + } + } } } -fn create_top_panel<'a>(children: &[ComponentWidgetWrapper]) -> Element<'a, ComponentWidgetEvent> { - let icon = text(icons::Bootstrap::ArrowLeft) - .font(icons::BOOTSTRAP_FONT); +fn render_image<'a>(image: &Image, icon_style: Option) -> Element<'a, ComponentWidgetEvent> { + match image { + Image::ImageSource(source) => { + // TODO image support + // image(Handle::from_memory(bytes.clone())) + // .into() - let back_button: Element<_> = button(icon) - .on_press(ComponentWidgetEvent::PreviousView) - .themed(ButtonStyle::RootTopPanelBackButton); - - let search_bar_element = render_child_by_type(children, |widget| matches!(widget, ComponentWidget::SearchBar { .. }), ComponentRenderContext::None) - .ok() - .unwrap_or_else(|| Space::with_width(Length::FillPortion(3)).into()); - - let top_panel: Element<_> = row(vec![back_button, search_bar_element]) - .align_items(Alignment::Center) - .themed(RowStyle::RootTopPanel); - - let top_panel: Element<_> = container(top_panel) - .width(Length::Fill) - .themed(ContainerStyle::RootTopPanel); - - top_panel + horizontal_space() + .into() + } + Image::Icons(icon) => { + match icon_style { + None => { + text(icon_to_bootstrap(icon)) + .font(icons::BOOTSTRAP_FONT) + .into() + } + Some(icon_style) => { + text(icon_to_bootstrap(icon)) + .font(icons::BOOTSTRAP_FONT) + .themed(icon_style) + } + } + } + } } + #[derive(Clone, Debug, Eq, PartialEq)] struct SelectItem { value: String, @@ -1345,40 +1700,6 @@ fn grid_width(columns: &Option) -> usize { columns.map(|value| value.trunc() as usize).unwrap_or(5) } -fn render_grid<'a>(children: &[ComponentWidgetWrapper], /*aspect_ratio: Option<&str>,*/ columns: &Option, context: ComponentRenderContext) -> Element<'a, ComponentWidgetEvent> { - // let (width, height) = match aspect_ratio { - // None => (1, 1), - // Some("1") => (1, 1), - // Some("3/2") => (3, 2), - // Some("2/3") => (2, 3), - // Some("4/3") => (4, 3), - // Some("3/4") => (3, 4), - // Some("16/9") => (16, 9), - // Some("9/16") => (9, 16), - // Some(value) => panic!("unsupported aspect_ratio {:?}", value) - // }; - - let grid_width = grid_width(columns); - - let rows: Vec> = render_children(children, context) - .into_iter() - .chunks(grid_width) - .into_iter() - .map(|row_items| { - let mut row_items: Vec<_> = row_items.collect(); - row_items.resize_with(grid_width, || horizontal_space().into()); - - grid_row(row_items).into() - }) - .collect(); - - let grid: Element<_> = grid(rows) - .width(Length::Fill) - .vertical_alignment(Vertical::Top) - .themed(GridStyle::Default); - - grid -} fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option<&str>, subtitle: &Option, theme_kind_title: RowStyle, theme_kind_title_text: TextStyle, theme_kind_subtitle_text: TextStyle) -> Element<'a, ComponentWidgetEvent> { let mut title_content = vec![]; @@ -1416,78 +1737,6 @@ fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option< .into() } -fn render_plugin_root<'a>( - show_action_panel: bool, - widget_id: UiWidgetId, - children: &[ComponentWidgetWrapper], - content: Element<'a, ComponentWidgetEvent>, - context: ComponentRenderContext, - is_loading: bool -) -> Element<'a, ComponentWidgetEvent> { - let ComponentRenderContext::Root { entrypoint_name, action_shortcuts, plugin_view_state } = context else { - panic!("not supposed to be passed to root item: {:?}", context) - }; - - let top_panel = create_top_panel(children); - - let top_separator = if is_loading { - LoadingBar::new() - .into() - } else { - horizontal_rule(1) - .into() - }; - - let mut action_panel = convert_action_panel(children, &action_shortcuts); - - let primary_action = action_panel.as_mut() - .map(|panel| panel.find_first()) - .flatten() - .map(|(label, widget_id)| { - let shortcut = PhysicalShortcut { - physical_key: PhysicalKey::Enter, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: false - }; - - (label.to_string(), widget_id, shortcut) - }); - - match plugin_view_state { - PluginViewState::None => { - render_root( - show_action_panel, - top_panel, - top_separator, - content, - primary_action, - action_panel, - None::<&ScrollHandle>, - entrypoint_name, - || ComponentWidgetEvent::ToggleActionPanel { widget_id }, - |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id }, - |widget_id| ComponentWidgetEvent::ActionClick { widget_id } - ) - } - PluginViewState::ActionPanel { focused_action_item } => { - render_root( - show_action_panel, - top_panel, - top_separator, - content, - primary_action, - action_panel, - Some(&focused_action_item), - entrypoint_name, - || ComponentWidgetEvent::ToggleActionPanel { widget_id }, - |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id }, - |widget_id| ComponentWidgetEvent::ActionClick { widget_id } - ) - } - } -} #[derive(Debug)] pub struct ActionPanel { @@ -1546,63 +1795,50 @@ impl ActionPanelItem { } } -fn convert_action_panel(children: &[ComponentWidgetWrapper], action_shortcuts: &HashMap) -> Option { - let action_panel: Vec<_> = children - .into_iter() - .filter(|child| { - let (widget, _) = &*child.get(); - matches!(widget, ComponentWidget::ActionPanel { .. }) - }) - .collect(); - - let action_panel = match action_panel[..] { - [] => None?, - [single] => single, - [_, _, ..] => None?, - }; - - let (action_panel, _) = &*action_panel.get(); - +fn convert_action_panel(action_panel: &Option, action_shortcuts: &HashMap) -> Option { match action_panel { - ComponentWidget::ActionPanel { children, title } => { - fn convert_to_items(children: &[ComponentWidgetWrapper], action_shortcuts: &HashMap) -> Vec { - let mut items = vec![]; + Some(ActionPanelWidget { content, title, .. }) => { + fn action_widget_to_action(ActionWidget { __id__, id, label }: &ActionWidget, action_shortcuts: &HashMap) -> ActionPanelItem { + let physical_shortcut: Option = id.as_ref() + .map(|id| action_shortcuts.get(id)) + .flatten() + .cloned(); - for child in children { - let widget_id = child.id; - let (widget, _) = &*child.get(); - - match widget { - ComponentWidget::Action { id, label } => { - let physical_shortcut: Option = id.as_ref() - .map(|id| action_shortcuts.get(id)) - .flatten() - .cloned(); - - items.push(ActionPanelItem::Action { - label: label.clone(), - widget_id, - physical_shortcut, - }); - } - ComponentWidget::ActionPanelSection { children, title } => { - items.push(ActionPanelItem::ActionSection { - title: title.clone(), - items: convert_to_items(children, action_shortcuts), - }); - } - _ => { - panic!("unexpected widget kind {:?}", widget) - } - }; + ActionPanelItem::Action { + label: label.clone(), + widget_id: *__id__, + physical_shortcut, } - - items } + let items = content.ordered_members.iter() + .map(|members| { + match members { + ActionPanelWidgetOrderedMembers::Action(widget) => { + action_widget_to_action(widget, action_shortcuts) + } + ActionPanelWidgetOrderedMembers::ActionPanelSection(ActionPanelSectionWidget { content, title, .. }) => { + let section_items = content.ordered_members + .iter() + .map(|members| { + match members { + ActionPanelSectionWidgetOrderedMembers::Action(widget) => action_widget_to_action(widget, action_shortcuts) + } + }) + .collect(); + + ActionPanelItem::ActionSection { + title: title.clone(), + items: section_items, + } + } + } + }) + .collect(); + Some(ActionPanel { title: title.clone(), - items: convert_to_items(children, action_shortcuts), + items, }) } _ => None @@ -1745,14 +1981,14 @@ fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T, ACTION>( } pub fn render_root<'a, T: 'a + Clone, ACTION>( - show_action_panel: bool, + show_action_panel: &bool, top_panel: Element<'a, T>, top_separator: Element<'a, T>, content: Element<'a, T>, primary_action: Option<(String, UiWidgetId, PhysicalShortcut)>, action_panel: Option, action_panel_scroll_handle: Option<&ScrollHandle>, - entrypoint_name: String, + entrypoint_name: &str, on_panel_toggle_click: impl Fn() -> T, on_panel_primary_click: impl Fn(UiWidgetId) -> T, on_action_click: impl Fn(UiWidgetId) -> T, @@ -1892,37 +2128,6 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( .into() } -fn render_text_part<'a>(value: &str, context: ComponentRenderContext) -> Element<'a, ComponentWidgetEvent> { - let header = match context { - ComponentRenderContext::None => None, - ComponentRenderContext::H1 => Some(34), - ComponentRenderContext::H2 => Some(30), - ComponentRenderContext::H3 => Some(24), - ComponentRenderContext::H4 => Some(20), - ComponentRenderContext::H5 => Some(18), - ComponentRenderContext::H6 => Some(16), - ComponentRenderContext::List { .. } => panic!("not supposed to be passed to text part"), - ComponentRenderContext::Grid { .. } => panic!("not supposed to be passed to text part"), - ComponentRenderContext::Root { .. } => panic!("not supposed to be passed to text part"), - ComponentRenderContext::Inline => panic!("not supposed to be passed to text part"), - ComponentRenderContext::GridItem => panic!("not supposed to be passed to text part"), - ComponentRenderContext::InlineRoot { .. } => panic!("not supposed to be passed to text part") - }; - - let mut text = text(value) - .shaping(Shaping::Advanced); - - if let Some(size) = header { - text = text - .size(size) - .font(Font { - weight: Weight::Bold, - ..Font::DEFAULT - }) - } - - text.into() -} fn render_shortcut<'a, T: 'a>(shortcut: &PhysicalShortcut) -> Element<'a, T> { let mut result = vec![]; @@ -1964,72 +2169,6 @@ fn render_shortcut<'a, T: 'a>(shortcut: &PhysicalShortcut) -> Element<'a, T> { .themed(RowStyle::ActionShortcut) } -fn render_children_string<'a>( - content: &[ComponentWidgetWrapper], - context: ComponentRenderContext -) -> Element<'a, ComponentWidgetEvent> { - let text_part = content - .into_iter() - .map(|child| { - let (widget, _) = &*child.get(); - - let ComponentWidget::TextPart { value } = widget else { - panic!("unexpected widget kind {:?}", widget) - }; - - value.clone() - }) - .join(""); - - render_text_part(&text_part, context) -} - - -fn render_children<'a>( - content: &[ComponentWidgetWrapper], - context: ComponentRenderContext -) -> Vec> { - content - .into_iter() - .map(|child| child.render_widget(context.clone())) - .collect() -} - -fn render_child_by_type<'a>( - content: &[ComponentWidgetWrapper], - predicate: impl Fn(&ComponentWidget) -> bool, - context: ComponentRenderContext -) -> anyhow::Result> { - let vec: Vec<_> = content - .into_iter() - .filter(|child| { - let (widget, _) = &*child.get(); - predicate(widget) - }) - .collect(); - - match vec[..] { - [] => Err(anyhow::anyhow!("no child matching predicate found")), - [single] => Ok(single.render_widget(context)), - [_, _, ..] => Err(anyhow::anyhow!("more than 1 child matching predicate found")), - } -} - -fn render_children_by_type<'a>( - content: &[ComponentWidgetWrapper], predicate: impl Fn(&ComponentWidget) -> bool, - context: ComponentRenderContext -) -> Vec> { - content - .into_iter() - .filter(|child| { - let (widget, _) = &*child.get(); - predicate(widget) - }) - .map(|child| child.render_widget(context.clone())) - .collect() -} - - #[derive(Clone, Debug)] pub enum ComponentWidgetEvent { LinkClick { @@ -2090,8 +2229,10 @@ pub enum ComponentWidgetEvent { }, } +include!(concat!(env!("OUT_DIR"), "/components.rs")); + impl ComponentWidgetEvent { - pub fn handle(self, _plugin_id: PluginId, widget: ComponentWidgetWrapper) -> Option { + pub fn handle(self, _plugin_id: PluginId, state: &mut ComponentWidgetState) -> Option { match self { ComponentWidgetEvent::LinkClick { widget_id: _, href } => { Some(UiViewEvent::Open { @@ -2104,19 +2245,17 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::RunAction { widget_id } | ComponentWidgetEvent::ActionClick { widget_id } => { Some(create_action_on_action_event(widget_id)) } - ComponentWidgetEvent::ToggleDatePicker { .. } => { - let (widget, ref mut state) = &mut *widget.get_mut(); - let ComponentWidgetState::DatePicker { state_value: _, show_picker } = state else { - panic!("unexpected state kind, widget: {:?} state: {:?}", widget, state) + ComponentWidgetEvent::ToggleDatePicker { widget_id } => { + let ComponentWidgetState::DatePicker(DatePickerState { state_value: _, show_picker }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; *show_picker = !*show_picker; None } - ComponentWidgetEvent::CancelDatePicker { .. } => { - let (widget, ref mut state) = &mut *widget.get_mut(); - let ComponentWidgetState::DatePicker { state_value: _, show_picker } = state else { - panic!("unexpected state kind, widget: {:?} state: {:?}", widget, state) + ComponentWidgetEvent::CancelDatePicker { widget_id } => { + let ComponentWidgetState::DatePicker(DatePickerState { state_value: _, show_picker }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; *show_picker = false; @@ -2124,9 +2263,8 @@ impl ComponentWidgetEvent { } ComponentWidgetEvent::SubmitDatePicker { widget_id, value } => { { - let (widget, ref mut state) = &mut *widget.get_mut(); - let ComponentWidgetState::DatePicker { state_value: _, show_picker, } = state else { - panic!("unexpected state kind, widget: {:?} state: {:?}", widget, state) + let ComponentWidgetState::DatePicker(DatePickerState { state_value: _, show_picker }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; *show_picker = false; @@ -2136,9 +2274,8 @@ impl ComponentWidgetEvent { } ComponentWidgetEvent::ToggleCheckbox { widget_id, value } => { { - let (widget, ref mut state) = &mut *widget.get_mut(); - let ComponentWidgetState::Checkbox { state_value } = state else { - panic!("unexpected state kind, widget: {:?} state: {:?}", widget, state) + let ComponentWidgetState::Checkbox(CheckboxState { state_value }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; *state_value = !*state_value; @@ -2148,9 +2285,8 @@ impl ComponentWidgetEvent { } ComponentWidgetEvent::SelectPickList { widget_id, value } => { { - let (widget, ref mut state) = &mut *widget.get_mut(); - let ComponentWidgetState::Select { state_value } = state else { - panic!("unexpected state kind, widget: {:?} state: {:?}", widget, state) + let ComponentWidgetState::Select(SelectState { state_value }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; *state_value = Some(value.clone()); @@ -2160,9 +2296,8 @@ impl ComponentWidgetEvent { } ComponentWidgetEvent::OnChangeTextField { widget_id, value } => { { - let (widget, ref mut state) = &mut *widget.get_mut(); - let ComponentWidgetState::TextField { state_value } = state else { - panic!("unexpected state kind, widget: {:?} state: {:?}", widget, state) + let ComponentWidgetState::TextField(TextFieldState { state_value }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; *state_value = value.clone(); @@ -2172,9 +2307,8 @@ impl ComponentWidgetEvent { } ComponentWidgetEvent::OnChangePasswordField { widget_id, value } => { { - let (widget, ref mut state) = &mut *widget.get_mut(); - let ComponentWidgetState::PasswordField { state_value } = state else { - panic!("unexpected state kind, widget: {:?} state: {:?}", widget, state) + let ComponentWidgetState::TextField(TextFieldState { state_value }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; *state_value = value.clone(); @@ -2184,9 +2318,8 @@ impl ComponentWidgetEvent { } ComponentWidgetEvent::OnChangeSearchBar { widget_id, value } => { { - let (widget, ref mut state) = &mut *widget.get_mut(); - let ComponentWidgetState::SearchBar { state_value } = state else { - panic!("unexpected state kind, widget: {:?} state: {:?}", widget, state) + let ComponentWidgetState::TextField(TextFieldState { state_value }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; *state_value = value.clone(); @@ -2239,38 +2372,6 @@ impl ComponentWidgetEvent { } } -fn parse_optional_string(properties: &HashMap, name: &str) -> anyhow::Result> { - match properties.get(name) { - None => Ok(None), - Some(value) => Ok(Some(value.as_string().ok_or(anyhow::anyhow!("{} has to be a string", name))?.to_owned())), - } -} - -fn parse_string(properties: &HashMap, name: &str) -> anyhow::Result { - parse_optional_string(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name)) -} - -fn parse_optional_number(properties: &HashMap, name: &str) -> anyhow::Result> { - match properties.get(name) { - None => Ok(None), - Some(value) => Ok(Some(value.as_number().ok_or(anyhow::anyhow!("{} has to be a number", name))?.to_owned())), - } -} - -fn parse_number(properties: &HashMap, name: &str) -> anyhow::Result { - parse_optional_number(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name)) -} - -fn parse_optional_boolean(properties: &HashMap, name: &str) -> anyhow::Result> { - match properties.get(name) { - None => Ok(None), - Some(value) => Ok(Some(value.as_bool().ok_or(anyhow::anyhow!("{} has to be a boolean", name))?.to_owned())), - } -} -fn parse_boolean(properties: &HashMap, name: &str) -> anyhow::Result { - parse_optional_boolean(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name)) -} - pub fn parse_date(value: &str) -> Option<(i32, u32, u32)> { let ymd: Vec<_> = value.split("-") .collect(); @@ -2290,74 +2391,6 @@ pub fn parse_date(value: &str) -> Option<(i32, u32, u32)> { } } -fn parse_bytes_optional(properties: &HashMap, name: &str) -> anyhow::Result>> { - match properties.get(name) { - None => Ok(None), - Some(value) => Ok(Some(value.as_bytes().ok_or(anyhow::anyhow!("{} has to be a byte array", name))?.to_owned())), - } -} - -fn parse_bytes(properties: &HashMap, name: &str) -> anyhow::Result> { - parse_bytes_optional(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name)) -} - -fn parse_enum_optional>(properties: &HashMap, name: &str) -> anyhow::Result> { - match properties.get(name) { - None => Ok(None), - Some(value) => { - let string = value.as_string().ok_or(anyhow::anyhow!("{} has to be a string", name))?.to_owned(); - let enum_value = T::from_str(&string)?; - Ok(Some(enum_value)) - }, - } -} - -fn parse_enum>(properties: &HashMap, name: &str) -> anyhow::Result { - parse_enum_optional(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name)) -} - - -fn parse_object_optional(properties: &HashMap, name: &str) -> anyhow::Result> { - match properties.get(name) { - None => Ok(None), - Some(value) => { - let value = value.as_object().ok_or(anyhow::anyhow!("{} has to be an object", name))?; - - Ok(Some(value)) - }, - } -} - -fn parse_object(properties: &HashMap, name: &str) -> anyhow::Result { - parse_object_optional(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name)) -} - -fn parse_array_optional(properties: &HashMap, name: &str) -> anyhow::Result>> { - match properties.get(name) { - None => Ok(None), - Some(value) => { - let value = value.as_array().ok_or(anyhow::anyhow!("{} has to be an array", name))?; - - Ok(Some(value)) - }, - } -} - -fn parse_array(properties: &HashMap, name: &str) -> anyhow::Result> { - parse_array_optional(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name)) -} - -fn parse_union_optional(properties: &HashMap, name: &str) -> anyhow::Result> { - match properties.get(name) { - None => Ok(None), - Some(value) => Ok(Some(value.as_union()?)), - } -} - -fn parse_union(properties: &HashMap, name: &str) -> anyhow::Result { - parse_union_optional(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name)) -} - fn icon_to_bootstrap(icon: &Icons) -> icons::Bootstrap { match icon { Icons::Airplane => icons::Bootstrap::Airplane, @@ -2582,73 +2615,3 @@ fn icon_to_bootstrap(icon: &Icons) -> icons::Bootstrap { Icons::Unindent => icons::Bootstrap::Unindent, } } - -impl UiPropertyValueToEnum for ListItemIcon { - fn convert(value: &UiPropertyValue) -> anyhow::Result { - match value { - UiPropertyValue::String(value) => Ok(ListItemIcon::_1(Icons::from_str(value)?)), - UiPropertyValue::Bytes(value) => Ok(ListItemIcon::_0(value.clone())), - UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number")), - UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool")), - UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object")), - UiPropertyValue::Array(_) => Err(anyhow!("unexpected type undefined")), - UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined")) - } - } -} - -impl UiPropertyValueToEnum for EmptyViewImage { - fn convert(value: &UiPropertyValue) -> anyhow::Result { - match value { - UiPropertyValue::String(value) => Ok(EmptyViewImage::_1(Icons::from_str(value)?)), - UiPropertyValue::Bytes(value) => Ok(EmptyViewImage::_0(value.clone())), - UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number")), - UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool")), - UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object")), - UiPropertyValue::Array(_) => Err(anyhow!("unexpected type undefined")), - UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined")) - } - } -} - -impl UiPropertyValueToEnum for ImageSource { - fn convert(value: &UiPropertyValue) -> anyhow::Result { - match value { - UiPropertyValue::String(value) => Ok(ImageSource::_1(Icons::from_str(value)?)), - UiPropertyValue::Bytes(value) => Ok(ImageSource::_0(value.clone())), - UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number")), - UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool")), - UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object")), - UiPropertyValue::Array(_) => Err(anyhow!("unexpected type undefined")), - UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined")) - } - } -} - -impl UiPropertyValueToEnum for IconAccessoryIcon { - fn convert(value: &UiPropertyValue) -> anyhow::Result { - match value { - UiPropertyValue::String(value) => Ok(IconAccessoryIcon::_1(Icons::from_str(value)?)), - UiPropertyValue::Bytes(value) => Ok(IconAccessoryIcon::_0(value.clone())), - UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number")), - UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool")), - UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object")), - UiPropertyValue::Array(_) => Err(anyhow!("unexpected type undefined")), - UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined")), - } - } -} - -impl UiPropertyValueToEnum for TextAccessoryIcon { - fn convert(value: &UiPropertyValue) -> anyhow::Result { - match value { - UiPropertyValue::String(value) => Ok(TextAccessoryIcon::_1(Icons::from_str(value)?)), - UiPropertyValue::Bytes(value) => Ok(TextAccessoryIcon::_0(value.clone())), - UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number")), - UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool")), - UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object")), - UiPropertyValue::Array(_) => Err(anyhow!("unexpected type undefined")), - UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined")) - } - } -} diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 8e26131..c06b1e9 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -1,12 +1,17 @@ -use std::collections::HashMap; -use common::model::{EntrypointId, PhysicalShortcut, PluginId, UiWidget, UiWidgetId}; +use std::collections::hash_map::Entry; use crate::model::UiViewEvent; -use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::state::PluginViewState; use crate::ui::theme::Element; -use crate::ui::widget::{ActionPanel, ComponentRenderContext, ComponentWidgetEvent, ComponentWidgetWrapper}; +use crate::ui::widget::{create_state, ActionPanel, ComponentWidgetEvent, ComponentWidgetState, ComponentWidgets}; +use common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiWidgetId}; +use std::collections::HashMap; +use std::mem; +use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; pub struct PluginWidgetContainer { - root_widget: ComponentWidgetWrapper, + root_widget: Arc>>, + state: Arc>>, plugin_id: Option, plugin_name: Option, entrypoint_id: Option, @@ -16,7 +21,8 @@ pub struct PluginWidgetContainer { impl PluginWidgetContainer { pub fn new() -> Self { Self { - root_widget: ComponentWidgetWrapper::root(0), + root_widget: Arc::new(Mutex::new(None)), + state: Arc::new(Mutex::new(HashMap::new())), plugin_id: None, plugin_name: None, entrypoint_id: None, @@ -28,37 +34,11 @@ impl PluginWidgetContainer { self.plugin_id.clone().expect("plugin id should always exist after render") } - pub fn get_plugin_name(&self) -> String { - self.plugin_name.clone().expect("plugin name should always exist after render") - } - pub fn get_entrypoint_id(&self) -> EntrypointId { self.entrypoint_id.clone().expect("entrypoint id should always exist after render") } - pub fn get_entrypoint_name(&self) -> String { - self.entrypoint_name.clone().expect("entrypoint id should always exist after render") - } - - pub fn get_action_panel(&self, action_shortcuts: &HashMap) -> Option { - self.root_widget.get_action_panel(action_shortcuts) - } - - pub fn render_widget<'a>(&self, context: ComponentRenderContext) -> Element<'a, ComponentWidgetEvent> { - self.root_widget.render_widget(context) - } - - fn create_component_widget(&mut self, ui_widget: UiWidget) -> ComponentWidgetWrapper { - let children = ui_widget.widget_children - .into_iter() - .map(|ui_widget| self.create_component_widget(ui_widget)) - .collect(); - - ComponentWidgetWrapper::widget(ui_widget.widget_id, &ui_widget.widget_type, ui_widget.widget_properties, children) - .expect("unable to create widget") - } - - pub fn replace_view(&mut self, container: UiWidget, plugin_id: &PluginId, plugin_name: &str, entrypoint_id: &EntrypointId, entrypoint_name: &str) { + pub fn replace_view(&mut self, container: RootWidget, plugin_id: &PluginId, plugin_name: &str, entrypoint_id: &EntrypointId, entrypoint_name: &str) { tracing::trace!("replace_view is called. container: {:?}", container); self.plugin_id = Some(plugin_id.clone()); @@ -66,40 +46,92 @@ impl PluginWidgetContainer { self.entrypoint_id = Some(entrypoint_id.clone()); self.entrypoint_name = Some(entrypoint_name.to_string()); - let children = container.widget_children.into_iter() - .map(|child| self.create_component_widget(child)) - .collect::>(); + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); - self.root_widget.find_child_with_id(container.widget_id) - .unwrap_or_else(|| panic!("widget with id {:?} doesn't exist", container.widget_id)) - .set_children(children) - .expect("unable to set children"); + // use new state with values from old state but only widget ids which exists in new state + // so we this way we use already existing values but remove state for removed widgets + let old_state = mem::replace(state.deref_mut(), create_state(&container)); + + for (key, value) in old_state.into_iter() { + match state.entry(key) { + Entry::Occupied(mut entry) => { + entry.insert(value); + } + Entry::Vacant(_) => {} + } + } + + *root_widget = Some(container); } pub fn handle_event(&self, plugin_id: PluginId, event: ComponentWidgetEvent) -> Option { - let widget = self.root_widget - .find_child_with_id(event.widget_id()) - .expect("created event for non existing widget?"); + let mut state = self.state.lock().expect("lock is poisoned"); - event.handle(plugin_id, widget) + let state = state.get_mut(&event.widget_id()).expect(&format!("requested state should always be present for event with widget id: {}", &event.widget_id())); + + event.handle(plugin_id, state) + } + + pub fn render_root_widget<'a>( + &self, + plugin_view_state: &PluginViewState, + action_shortcuts: &HashMap, + ) -> Element<'a, ComponentWidgetEvent> { + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state) + .render_root_widget(plugin_view_state, self.entrypoint_name.as_ref(), action_shortcuts) + } + + pub fn render_inline_root_widget<'a>(&self) -> Element<'a, ComponentWidgetEvent> { + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state) + .render_root_inline_widget(self.plugin_name.as_ref(), self.entrypoint_name.as_ref()) } pub fn toggle_action_panel(&self) { - self.root_widget.toggle_action_panel() + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state).toggle_action_panel() } pub fn get_action_ids(&self) -> Vec { - self.root_widget.get_action_ids() + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state).get_action_ids() } + + pub fn get_action_panel(&self, action_shortcuts: &HashMap) -> Option { + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state).get_action_panel(action_shortcuts) + } + pub fn keyboard_navigation_width(&self) -> Option { - self.root_widget.keyboard_navigation_width() + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state).keyboard_navigation_width() } pub fn keyboard_navigation_total(&self) -> usize { - self.root_widget.keyboard_navigation_total() + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state).keyboard_navigation_total() } pub fn has_search_bar(&self) -> bool { - self.root_widget.has_search_bar() + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state).has_search_bar() } } diff --git a/rust/common/Cargo.toml b/rust/common/Cargo.toml index 4ed17f2..87d2238 100644 --- a/rust/common/Cargo.toml +++ b/rust/common/Cargo.toml @@ -9,6 +9,7 @@ tonic = "0.11.0" prost = "0.12.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +serde-value = "0.7.0" tokio = "1.37.0" base64 = "0.22" utils = { path = "../utils" } @@ -18,6 +19,10 @@ directories = "5.0" [build-dependencies] tonic-build = "0.11.0" +component_model = { path = "../component_model" } +convert_case = "0.6.0" +itertools = "0.10.5" +indexmap = "2.1.0" [features] release = [] diff --git a/rust/common/build.rs b/rust/common/build.rs index 2d09089..af2fff9 100644 --- a/rust/common/build.rs +++ b/rust/common/build.rs @@ -1,3 +1,15 @@ +use std::collections::HashMap; +use std::env; +use std::fs::File; +use std::io::Write; +use std::ops::Deref; +use std::path::Path; +use component_model::{create_component_model, Arity, Children, Component, ComponentName, ComponentRef, Property, PropertyKind, PropertyType, SharedType}; +use itertools::Itertools; + +use convert_case::{Case, Casing}; +use indexmap::IndexMap; + fn main() -> Result<(), Box> { tonic_build::configure() .compile( @@ -5,5 +17,477 @@ fn main() -> Result<(), Box> { &["./../../schema/"], )?; + component_model_generator()?; + Ok(()) } + +fn component_model_generator() -> Result<(), Box> { + let out_dir = env::var("OUT_DIR")?; + let dest_path = Path::new(&out_dir).join("components.rs"); + + let mut output = String::new(); + + let components = create_component_model(); + + for component in &components { + match component { + Component::Standard { name, props, children, .. } => { + let props_has_content = props.iter().any(|prop| matches!(prop.property_type.kind(), PropertyKind::Component)); + + let children_has_content = match children { + Children::Members { ordered_members, per_type_members, .. } | Children::StringOrMembers { ordered_members, per_type_members, .. } => { + !ordered_members.is_empty() || !per_type_members.is_empty() + } + _ => false + }; + + let has_text = matches!(children, Children::StringOrMembers { .. } | Children::String { .. }); + + let has_content = children_has_content || props_has_content || has_text; + + let default = IndexMap::new(); + + let (ordered_members, per_type_members) = match children { + Children::Members { ordered_members, per_type_members, .. } | Children::StringOrMembers { ordered_members, per_type_members, .. } => { + (ordered_members, per_type_members) + } + _ => (&default, &default) + }; + + if !ordered_members.is_empty() { + output.push_str("#[derive(Debug)]\n"); + output.push_str(&format!("pub enum {}WidgetOrderedMembers {{\n", name)); + + let unique_component_refs = ordered_members + .iter() + .map(|(_member_name, component_ref)| component_ref) + .unique_by(|component_ref| component_ref.component_name.clone()) + .collect::>(); + + for component_ref in &unique_component_refs { + output.push_str(&format!(" {}({}Widget),\n", component_ref.component_name, component_ref.component_name)); + } + + output.push_str("}\n"); + } + + if has_content { + { + output.push_str("#[derive(Debug)]\n"); + output.push_str(&format!("pub struct {}WidgetContent {{\n", name)); + + for prop in props { + if matches!(prop.property_type.kind(), PropertyKind::Component) { + let is_union = match &prop.property_type { + PropertyType::Union { .. } => true, + PropertyType::Array { item } => matches!(item.as_ref(), PropertyType::Union { .. }), + _ => false + }; + + if is_union { + output.push_str(&format!(" pub {}: {},\n", prop.name.to_case(Case::Snake), generate_required_type(&prop.property_type, Some(format!("{}{}", name, &prop.name.to_case(Case::Pascal)))))); + } else { + output.push_str(&format!(" pub {}: {},\n", prop.name.to_case(Case::Snake), generate_type(&prop, name))); + } + } + } + + for (member_name, component_ref) in per_type_members { + match component_ref.arity { + Arity::ZeroOrOne => { + output.push_str(&format!(" pub {}: Option<{}Widget>,\n", member_name.to_case(Case::Snake), component_ref.component_name)); + } + Arity::One => { + output.push_str(&format!(" pub {}: {}Widget,\n", member_name.to_case(Case::Snake), component_ref.component_name)); + } + Arity::ZeroOrMore => { + todo!() + } + } + } + + if !ordered_members.is_empty() { + output.push_str(&format!(" pub ordered_members: Vec<{}WidgetOrderedMembers>,\n", name)); + } + + if has_text { + output.push_str(" pub text: Vec,\n"); + } + + output.push_str("}\n"); + } + + { + output.push_str(&format!("impl<'de> Deserialize<'de> for {}WidgetContent {{\n", name)); + output.push_str(&format!(" fn deserialize(deserializer: D) -> Result\n")); + output.push_str(&format!(" where\n")); + output.push_str(&format!(" D: Deserializer<'de>,\n")); + output.push_str(&format!(" {{\n")); + + let unique_ordered_component_refs = ordered_members + .iter() + .map(|(_member_name, component_ref)| component_ref) + .unique_by(|component_ref| component_ref.component_name.clone()) + .collect::>(); + + let mut per_type_component_refs = per_type_members + .iter() + .map(|(_member_name, component_ref)| component_ref) + .collect::>(); + + let mut prop_union_component_refs = IndexMap::new(); + let mut prop_other_component_refs = IndexMap::new(); + + for prop in props { + let is_union = match &prop.property_type { + PropertyType::Union { .. } => true, + PropertyType::Array { item } => matches!(item.as_ref(), PropertyType::Union { .. }), + _ => false + }; + + let prop_name = prop.name.to_case(Case::Snake); + + if is_union { + fn all_component_refs(property_type: &PropertyType) -> Vec<&ComponentRef> { + match property_type { + PropertyType::String => vec![], + PropertyType::Number => vec![], + PropertyType::Boolean => vec![], + PropertyType::Component { reference } => vec![reference], + PropertyType::Function { .. } => vec![], + PropertyType::SharedTypeRef { .. } => vec![], + PropertyType::Union { items } => { + items.iter().flat_map(|prop| all_component_refs(prop)).collect() + } + PropertyType::Array { item } => all_component_refs(item) + } + } + + prop_union_component_refs.insert(prop_name, all_component_refs(&prop.property_type)); + } else { + match &prop.property_type { + PropertyType::Component { reference } => { + prop_other_component_refs.insert(prop_name, reference); + }, + PropertyType::Array { item, .. } => { + match item.as_ref() { + PropertyType::Component { reference } => { + prop_other_component_refs.insert(prop_name, reference); + + }, + _ => {} + } + }, + _ => {} + } + } + } + + let mut component_refs = vec![]; + component_refs.extend(unique_ordered_component_refs.clone()); + component_refs.extend(per_type_component_refs.clone()); + component_refs.extend(prop_other_component_refs.values()); + + { + output.push_str(" #[derive(Debug, Deserialize)]\n"); + output.push_str(" #[serde(tag = \"__type__\")]\n"); + output.push_str(&format!(" enum {}WidgetMembers {{\n", name)); + + for (_, prop_union_component_refs) in &prop_union_component_refs { + for prop_union_component_ref in prop_union_component_refs { + output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", prop_union_component_ref.component_internal_name)); + output.push_str(&format!(" {}({}Widget),\n", prop_union_component_ref.component_name, prop_union_component_ref.component_name)); + } + } + + for component_ref in &component_refs { + output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", component_ref.component_internal_name)); + output.push_str(&format!(" {}({}Widget),\n", component_ref.component_name, component_ref.component_name)); + } + + if has_text { + output.push_str(&format!(" #[serde(rename = \"gauntlet:text_part\")]\n")); + output.push_str(&format!(" Text {{\n")); + output.push_str(&format!(" value: String\n")); + output.push_str(&format!(" }},\n")); + } + + output.push_str(" }\n"); + } + + + output.push_str(&format!(" let mut members = Vec::<{}WidgetMembers>::deserialize(deserializer)?;\n", name)); + output.push_str("\n"); + + for (prop_name, _) in &prop_other_component_refs { + output.push_str(&format!(" let mut {}: Option<_> = None;\n", prop_name)); + } + + for (prop_name, _) in &prop_union_component_refs { + output.push_str(&format!(" let mut {}: Vec<_> = vec![];\n", prop_name)); + } + + for per_type_component_ref in &per_type_component_refs { + output.push_str(&format!(" let mut {}: Option<_> = None;\n", per_type_component_ref.component_internal_name)); + } + + if !ordered_members.is_empty() { + output.push_str(" let mut ordered_members = vec![];\n"); + } + + if has_text { + output.push_str(" let mut text = vec![];\n"); + } + + output.push_str("\n"); + + if has_content { + output.push_str(" while let Some(member) = members.pop() {\n"); + output.push_str(" match member {\n"); + + for (prop_name, prop_union_component_refs) in &prop_union_component_refs { + for (index, prop_union_component_ref) in prop_union_component_refs.iter().enumerate() { + output.push_str(&format!(" {}WidgetMembers::{}(widget) => {{\n", name, prop_union_component_ref.component_name)); + output.push_str(&format!(" {}.push({}{}::_{}(widget));\n", prop_name, name, prop_name.to_case(Case::Pascal), index)); + output.push_str(&format!(" }}\n")); + } + } + + for (prop_name, prop_other_component_refs) in &prop_other_component_refs { + output.push_str(&format!(" {}WidgetMembers::{}(widget) => {{\n", name, prop_other_component_refs.component_name)); + output.push_str(&format!(" if let Some(_) = {} {{\n", prop_name)); + output.push_str(&format!(" return Err(Error::custom(\"Only one {} is allowed\"))\n", prop_other_component_refs.component_name)); + output.push_str(&format!(" }}\n")); + output.push_str(&format!(" {} = Some(widget);\n", prop_name)); + output.push_str(&format!(" }}\n")); + } + + for per_type_component_ref in &per_type_component_refs { + output.push_str(&format!(" {}WidgetMembers::{}(widget) => {{\n", name, per_type_component_ref.component_name)); + output.push_str(&format!(" if let Some(_) = {} {{\n", per_type_component_ref.component_internal_name)); + output.push_str(&format!(" return Err(Error::custom(\"Only one {} is allowed\"))\n", per_type_component_ref.component_name)); + output.push_str(&format!(" }}\n")); + output.push_str(&format!(" {} = Some(widget);\n", per_type_component_ref.component_internal_name)); + output.push_str(&format!(" }}\n")); + } + + for ordered_component_ref in unique_ordered_component_refs { + output.push_str(&format!(" {}WidgetMembers::{}(widget) => {{\n", name, ordered_component_ref.component_name)); + output.push_str(&format!(" ordered_members.insert(0, {}WidgetOrderedMembers::{}(widget));\n", name, ordered_component_ref.component_name)); + output.push_str(&format!(" }}\n")); + } + + if has_text { + output.push_str(&format!(" {}WidgetMembers::Text {{ value }} => {{\n", name)); + output.push_str(&format!(" text.insert(0, value);\n")); + output.push_str(&format!(" }}\n")); + } + + output.push_str(" }\n"); + output.push_str(" }\n"); + } + + output.push_str("\n"); + output.push_str(&format!(" Ok({}WidgetContent {{\n", name)); + + for (prop_name, _) in &prop_union_component_refs { + output.push_str(&format!(" {},\n", prop_name)); + } + + for per_type_component_ref in per_type_component_refs { + match per_type_component_ref.arity { + Arity::ZeroOrOne => { + output.push_str(&format!(" {},\n", per_type_component_ref.component_internal_name)); + } + Arity::One => { + output.push_str(&format!(" {}: {}.ok_or(Error::custom(\"{} is required\"))?,\n", per_type_component_ref.component_internal_name, per_type_component_ref.component_internal_name, per_type_component_ref.component_name)); + } + Arity::ZeroOrMore => { + todo!() + } + } + } + + for (prop_name, prop_other_component_ref) in &prop_other_component_refs { + match prop_other_component_ref.arity { + Arity::ZeroOrOne => { + output.push_str(&format!(" {},\n", prop_name)); + } + Arity::One => { + output.push_str(&format!(" {}: {}.ok_or(Error::custom(\"{} is required\"))?,\n", prop_name, prop_other_component_ref.component_internal_name, prop_other_component_ref.component_name)); + } + Arity::ZeroOrMore => { + todo!() + } + } + } + + if !ordered_members.is_empty() { + output.push_str(" ordered_members\n"); + } + + if has_text { + output.push_str(" text\n"); + } + + output.push_str(&format!(" }})\n")); + output.push_str(&format!(" }}\n")); + output.push_str(&format!("}}\n")); + } + } + + output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str(&format!("pub struct {}Widget {{\n", name)); + output.push_str(" #[serde(rename = \"__id__\")]\n"); + output.push_str(" pub __id__: UiWidgetId,\n"); + + for prop in props { + if matches!(prop.property_type.kind(), PropertyKind::Property) { + output.push_str(&format!(" #[serde(rename = \"{}\")]\n", prop.name)); + output.push_str(&format!(" pub {}: {},\n", prop.name.to_case(Case::Snake), generate_type(&prop, name))); + } + } + + if has_content { + output.push_str(&format!(" pub content: {}WidgetContent,\n", name)); + } + + output.push_str("}\n"); + + let generate_union = |output: &mut String, items: &Vec, prop_name: &String| { + output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str(&format!("pub enum {}{} {{\n", name, prop_name.to_case(Case::Pascal))); + + for (index, property_type) in items.iter().enumerate() { + output.push_str(&format!(" _{}({}),\n", index, generate_required_type(&property_type, None))); + } + + output.push_str("}\n"); + output.push_str("\n"); + }; + + for prop in props { + match &prop.property_type { + PropertyType::Union { items } => { + generate_union(&mut output, items, &prop.name) + } + PropertyType::Array { item} => { + match item.deref() { + PropertyType::Union { items } => { + generate_union(&mut output, items, &prop.name) + } + _ => {} + } + } + _ => {} + } + } + } + Component::Root { children, shared_types, .. } => { + for (type_name, shared_type) in shared_types { + match shared_type { + SharedType::Enum { items } => { + output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str(&format!("pub enum {} {{\n", type_name)); + + for item in items { + output.push_str(&format!(" #[serde(rename = \"{}\")]\n", &item)); + output.push_str(&format!(" {},\n", &item)); + } + + output.push_str("}\n"); + output.push_str("\n"); + } + SharedType::Object { items } => { + output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str(&format!("pub struct {} {{\n", type_name)); + + for (property_name, property_type) in items { + output.push_str(&format!(" pub {}: {},\n", &property_name, generate_required_type(&property_type, Some(format!("{}{}", type_name, property_name))))); + } + + output.push_str("}\n"); + output.push_str("\n"); + } + SharedType::Union { items } => { + output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str("#[serde(untagged)]\n"); + output.push_str(&format!("pub enum {} {{\n", type_name)); + + for property_type in items { + match property_type { + PropertyType::SharedTypeRef { name } => { + output.push_str(&format!(" {}({}),\n", &name, name)); + } + _ => { + todo!() + } + } + } + + output.push_str("}\n"); + output.push_str("\n"); + } + } + } + + output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str("#[serde(tag = \"__type__\")]\n"); + output.push_str("pub enum RootWidgetMembers {\n"); + + for component_ref in children { + output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", component_ref.component_internal_name)); + output.push_str(&format!(" {}({}Widget),\n", component_ref.component_name, component_ref.component_name)); + } + + output.push_str("}\n"); + + output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str("pub struct RootWidget {\n"); + output.push_str(" #[serde(default, deserialize_with = \"array_to_option\")]\n"); + output.push_str(" pub content: Option\n"); + output.push_str("}\n"); + } + Component::TextPart { .. } => {} + } + } + + generate_file(&dest_path, &output)?; + + Ok(()) +} + +fn generate_file>(path: P, text: &str) -> std::io::Result<()> { + let mut f = File::create(path)?; + f.write_all(text.as_bytes()) +} + +fn generate_type(property: &Property, name: &ComponentName) -> String { + match property.optional { + true => generate_optional_type(&property.property_type, format!("{}{}", name, &property.name.to_case(Case::Pascal))), + false => generate_required_type(&property.property_type, Some(format!("{}{}", name, &property.name.to_case(Case::Pascal)))) + } +} + +fn generate_optional_type(property_type: &PropertyType, union_name: String) -> String { + format!("Option<{}>", generate_required_type(property_type, Some(union_name))) +} + +fn generate_required_type(property_type: &PropertyType, union_name: Option) -> String { + match property_type { + PropertyType::String => "String".to_owned(), + PropertyType::Number => "f64".to_owned(), + PropertyType::Boolean => "bool".to_owned(), + PropertyType::Function { .. } => panic!("client doesn't know about functions in properties"), + PropertyType::Component { reference } => format!("{}Widget", reference.component_name.to_string()), + PropertyType::SharedTypeRef { name } => name.to_owned(), + PropertyType::Union { .. } => { + match union_name { + None => panic!("should not be used"), + Some(union_name) => union_name + } + }, + PropertyType::Array { item } => format!("Vec<{}>", generate_required_type(item, union_name)) + } +} diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 0a73d1d..ef004d6 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -5,6 +5,8 @@ use std::sync::Arc; use anyhow::anyhow; use gix_url::Scheme; use gix_url::Url; +use serde::{Deserialize, Deserializer}; +use serde::de::Error; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PluginId(Arc); @@ -134,7 +136,7 @@ pub enum UiRequestData { entrypoint_name: String, render_location: UiRenderLocation, top_level_view: bool, - container: UiWidget, + container: RootWidget, }, ShowPreferenceRequiredView { plugin_id: PluginId, @@ -226,6 +228,23 @@ pub enum KeyboardEventOrigin { PluginView, } +fn array_to_option<'de, D, V>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, V: Deserialize<'de> { + let res = Option::>::deserialize(deserializer)?; + + match res { + None => Ok(None), + Some(mut res) => { + match res.len() { + 0 => Ok(None), + 1 => Ok(Some(res.remove(0))), + _ => Err(Error::custom("only zero or one allowed")) + } + } + } +} + +include!(concat!(env!("OUT_DIR"), "/components.rs")); + #[derive(Debug, Clone)] pub struct UiWidget { pub widget_id: UiWidgetId, @@ -245,60 +264,6 @@ pub enum UiPropertyValue { Undefined, } -impl UiPropertyValue { - pub fn as_string(&self) -> Option<&str> { - if let UiPropertyValue::String(val) = self { - Some(val) - } else { - None - } - } - pub fn as_number(&self) -> Option<&f64> { - if let UiPropertyValue::Number(val) = self { - Some(val) - } else { - None - } - } - pub fn as_bool(&self) -> Option<&bool> { - if let UiPropertyValue::Bool(val) = self { - Some(val) - } else { - None - } - } - pub fn as_bytes(&self) -> Option<&[u8]> { - if let UiPropertyValue::Bytes(val) = self { - Some(val) - } else { - None - } - } - pub fn as_array(&self) -> Option> { - // currently array is only used for component refs which are not properties - // implement if needed - unimplemented!() - } - pub fn as_object(&self) -> Option { - if let UiPropertyValue::Object(val) = self { - Some(UiPropertyValueToStruct::convert(val).expect("invalid object")) - } else { - None - } - } - pub fn as_union(&self) -> anyhow::Result { - UiPropertyValueToEnum::convert(self) - } -} - -pub trait UiPropertyValueToStruct { - fn convert(value: &HashMap) -> anyhow::Result where Self: Sized; -} - -pub trait UiPropertyValueToEnum { - fn convert(value: &UiPropertyValue) -> anyhow::Result where Self: Sized; -} - pub type UiWidgetId = usize; #[derive(Debug, Clone)] diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index 7cd97c1..b69b01e 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -2,7 +2,7 @@ use anyhow::anyhow; use thiserror::Error; use utils::channel::{RequestError, RequestSender}; -use crate::model::{EntrypointId, PhysicalShortcut, PluginId, UiRenderLocation, UiRequestData, UiResponseData, UiWidget}; +use crate::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiRequestData, UiResponseData}; #[derive(Error, Debug)] pub enum FrontendApiError { @@ -46,7 +46,7 @@ impl FrontendApi { entrypoint_name: String, render_location: UiRenderLocation, top_level_view: bool, - container: UiWidget, + container: RootWidget, ) -> Result<(), FrontendApiError> { let request = UiRequestData::ReplaceView { plugin_id, diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index 20b5faf..44c5f3c 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -82,10 +82,8 @@ pub enum PropertyType { Function { arguments: Vec }, - #[serde(rename = "image_source")] - ImageSource, - #[serde(rename = "enum")] - Enum { + #[serde(rename = "shared_type_ref")] + SharedTypeRef { name: String }, #[serde(rename = "union")] @@ -96,70 +94,39 @@ pub enum PropertyType { Array { item: Box }, - #[serde(rename = "object")] - Object { - name: String, - }, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum PropertyKind { + Event, + Component, + Property } impl PropertyType { - pub fn is_in_children(&self) -> bool { + // whether the component property, after being sent to rust, is in react properties, children, or a function + pub fn kind(&self) -> PropertyKind { match self { - PropertyType::String => false, - PropertyType::Number => false, - PropertyType::Boolean => false, - PropertyType::Component { .. } => true, - PropertyType::Function { .. } => false, - PropertyType::ImageSource => false, - PropertyType::Enum { .. } => false, + PropertyType::String => PropertyKind::Property, + PropertyType::Number => PropertyKind::Property, + PropertyType::Boolean => PropertyKind::Property, + PropertyType::Component { .. } => PropertyKind::Component, + PropertyType::Function { .. } => PropertyKind::Event, + PropertyType::SharedTypeRef { .. } => PropertyKind::Property, PropertyType::Union { items } => { - let all_in = items.iter().all(|prop_type| prop_type.is_in_children()); - let any_in = items.iter().any(|prop_type| prop_type.is_in_children()); - - if all_in && any_in { - true - } else { - let all_not_in = items.iter().all(|prop_type| !prop_type.is_in_children()); - let any_not_in = items.iter().any(|prop_type| !prop_type.is_in_children()); - if all_not_in && any_not_in { - false - } else { - panic!("all items in union should be of the same kind") - } + if items.is_empty() { + panic!("Union property cannot be empty") } - } - PropertyType::Array { item } => item.is_in_children(), - PropertyType::Object { .. } => false, - } - } - pub fn is_in_property(&self) -> bool { - match self { - PropertyType::String => true, - PropertyType::Number => true, - PropertyType::Boolean => true, - PropertyType::Component { .. } => false, - PropertyType::Function { .. } => false, - PropertyType::ImageSource => true, - PropertyType::Enum { .. } => true, - PropertyType::Union { items } => { - let all_in = items.iter().all(|prop_type| prop_type.is_in_property()); - let any_in = items.iter().any(|prop_type| prop_type.is_in_property()); + let first_variant = &items[0]; - if all_in && any_in { - true - } else { - let all_not_in = items.iter().all(|prop_type| !prop_type.is_in_property()); - let any_not_in = items.iter().any(|prop_type| !prop_type.is_in_property()); - if all_not_in && any_not_in { - false - } else { - panic!("all items in union should be of the same kind") - } - } + if !items.iter().all(|variant| variant.kind() == first_variant.kind()) { + panic!("all items in union should be of the same kind: {:?}", items) + }; + + first_variant.kind() } - PropertyType::Array { item } => item.is_in_property(), - PropertyType::Object { .. } => true, + PropertyType::Array { item } => item.kind(), } } } @@ -174,21 +141,38 @@ pub enum SharedType { #[serde(rename = "object")] Object { items: IndexMap + }, + #[serde(rename = "union")] + Union { + items: Vec } } +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "type")] +pub enum Arity { + #[serde(rename = "zero_or_one")] + ZeroOrOne, + #[serde(rename = "one")] + One, + #[serde(rename = "zero_or_more")] + ZeroOrMore, +} + #[derive(Debug, Clone, Serialize)] #[serde(tag = "type")] pub enum Children { #[serde(rename = "string_or_members")] StringOrMembers { - members: IndexMap, + ordered_members: IndexMap, + per_type_members: IndexMap, #[serde(rename = "textPartInternalName")] text_part_internal_name: String, }, #[serde(rename = "members")] Members { - members: IndexMap, + ordered_members: IndexMap, + per_type_members: IndexMap, }, #[serde(rename = "string")] String { @@ -206,22 +190,27 @@ pub struct ComponentRef { pub component_internal_name: String, #[serde(rename = "componentName")] pub component_name: ComponentName, + pub arity: Arity, } -fn children_string_or_members(members: I) -> Children - where I: IntoIterator +fn children_string_or_members(ordered_members: I1, per_type_members: I2) -> Children +where I1: IntoIterator, + I2: IntoIterator { Children::StringOrMembers { text_part_internal_name: "text_part".to_owned(), - members: members.into_iter().collect(), + ordered_members: ordered_members.into_iter().collect(), + per_type_members: per_type_members.into_iter().collect(), } } -fn children_members(members: I) -> Children - where I: IntoIterator +fn children_members(ordered_members: I1, per_type_members: I2) -> Children +where I1: IntoIterator, + I2: IntoIterator { Children::Members { - members: members.into_iter().collect(), + ordered_members: ordered_members.into_iter().collect(), + per_type_members: per_type_members.into_iter().collect(), } } @@ -236,7 +225,7 @@ fn children_none() -> Children { Children::None } -fn member(member_name: impl ToString, component: &Component) -> (String, ComponentRef) { +fn member(member_name: impl ToString, component: &Component, arity: Arity) -> (String, ComponentRef) { match component { Component::Standard { internal_name, name, .. } => { ( @@ -244,6 +233,7 @@ fn member(member_name: impl ToString, component: &Component) -> (String, Compone ComponentRef { component_internal_name: internal_name.to_owned(), component_name: name.to_owned(), + arity } ) } @@ -265,13 +255,14 @@ fn component(internal_name: impl ToString, description: String, name: impl To } } -fn component_ref(component: &Component) -> PropertyType { +fn component_ref(component: &Component, arity: Arity) -> PropertyType { match component { Component::Standard { internal_name, name, .. } => { PropertyType::Component { reference: ComponentRef { component_internal_name: internal_name.to_owned(), component_name: name.to_owned(), + arity } } } @@ -305,7 +296,7 @@ fn root(children: &[&Component]) -> Component { .map(|child| { match child { Component::Standard { internal_name, name, .. } => { - ComponentRef { component_name: name.to_owned(), component_internal_name: internal_name.to_owned() } + ComponentRef { component_name: name.to_owned(), component_internal_name: internal_name.to_owned(), arity: Arity::ZeroOrOne } } Component::Root { .. } => panic!("invalid root child"), Component::TextPart { .. } => panic!("invalid root child"), @@ -543,6 +534,40 @@ fn root(children: &[&Component]) -> Component { ].into_iter().map(|s| s.to_string()).collect() }), + ("ImageSourceUrl".to_owned(), SharedType::Object { + items: { + let mut map = IndexMap::new(); + map.insert("url".to_string(), PropertyType::String); + map + }, + }), + ("ImageSourceAsset".to_owned(), SharedType::Object { + items: { + let mut map = IndexMap::new(); + map.insert("asset".to_string(), PropertyType::String); + map + }, + }), + ("ImageSource".to_owned(), SharedType::Union { + items: vec![ + PropertyType::SharedTypeRef { + name: "ImageSourceUrl".to_owned() + }, + PropertyType::SharedTypeRef { + name: "ImageSourceAsset".to_owned() + }, + ] + }), + ("Image".to_owned(), SharedType::Union { + items: vec![ + PropertyType::SharedTypeRef { + name: "ImageSource".to_owned() + }, + PropertyType::SharedTypeRef { + name: "Icons".to_owned() + } + ], + }), ]), } } @@ -590,9 +615,12 @@ pub fn create_component_model() -> Vec { [ property("title", mark_doc!("/action_panel_section/props/title.md"), true, PropertyType::String), ], - children_members([ - member("Action", &action_component), - ]), + children_members( + [ + member("Action", &action_component, Arity::ZeroOrMore), + ], + [] + ), ); @@ -603,10 +631,13 @@ pub fn create_component_model() -> Vec { [ property("title", mark_doc!("/action_panel/props/title.md"), true, PropertyType::String), ], - children_members([ - member("Action", &action_component), - member("Section", &action_panel_section_component), - ]), + children_members( + [ + member("Action", &action_component, Arity::ZeroOrMore), + member("Section", &action_panel_section_component, Arity::ZeroOrMore), + ], + [] + ), ); @@ -639,9 +670,12 @@ pub fn create_component_model() -> Vec { [ property("label", mark_doc!("/metadata_tag_list/props/label.md"), false, PropertyType::String) ], - children_members([ - member("Item", &metadata_tag_item_component), - ]), + children_members( + [ + member("Item", &metadata_tag_item_component, Arity::ZeroOrMore), + ], + [], + ), ); let metadata_separator_component = component( @@ -657,7 +691,7 @@ pub fn create_component_model() -> Vec { mark_doc!("/metadata_icon/description.md"), "MetadataIcon", [ - property("icon", mark_doc!("/metadata_icon/props/icon.md"), false, PropertyType::Enum { name: "Icons".to_owned() }), + property("icon", mark_doc!("/metadata_icon/props/icon.md"), false, PropertyType::SharedTypeRef { name: "Icons".to_owned() }), property("label", mark_doc!("/metadata_icon/props/label.md"), false, PropertyType::String), ], children_none(), @@ -678,13 +712,16 @@ pub fn create_component_model() -> Vec { mark_doc!("/metadata/description.md"), "Metadata", [], - children_members([ - member("TagList", &metadata_tag_list_component), - member("Link", &metadata_link_component), - member("Value", &metadata_value_component), - member("Icon", &metadata_icon_component), - member("Separator", &metadata_separator_component), - ]), + children_members( + [ + member("TagList", &metadata_tag_list_component, Arity::ZeroOrMore), + member("Link", &metadata_link_component, Arity::ZeroOrMore), + member("Value", &metadata_value_component, Arity::ZeroOrMore), + member("Icon", &metadata_icon_component, Arity::ZeroOrMore), + member("Separator", &metadata_separator_component, Arity::ZeroOrMore), + ], + [], + ), ); // let link_component = component( @@ -701,7 +738,7 @@ pub fn create_component_model() -> Vec { mark_doc!("/image/description.md"), "Image", [ - property("source", mark_doc!("/image/props/source.md"), false, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] }) + property("source", mark_doc!("/image/props/source.md"), false, PropertyType::SharedTypeRef { name: "Image".to_owned() }) ], children_none(), ); @@ -795,20 +832,23 @@ pub fn create_component_model() -> Vec { mark_doc!("/content/description.md"), "Content", [], - children_members([ - member("Paragraph", ¶graph_component), - // member("Link", &link_component), - member("Image", &image_component), // TODO color - member("H1", &h1_component), - member("H2", &h2_component), - member("H3", &h3_component), - member("H4", &h4_component), - member("H5", &h5_component), - member("H6", &h6_component), - member("HorizontalBreak", &horizontal_break_component), - member("CodeBlock", &code_block_component), - // member("Code", &code_component), - ]), + children_members( + [ + member("Paragraph", ¶graph_component, Arity::ZeroOrMore), + // member("Link", &link_component), + member("Image", &image_component, Arity::ZeroOrMore), // TODO color + member("H1", &h1_component, Arity::ZeroOrMore), + member("H2", &h2_component, Arity::ZeroOrMore), + member("H3", &h3_component, Arity::ZeroOrMore), + member("H4", &h4_component, Arity::ZeroOrMore), + member("H5", &h5_component, Arity::ZeroOrMore), + member("H6", &h6_component, Arity::ZeroOrMore), + member("HorizontalBreak", &horizontal_break_component, Arity::ZeroOrMore), + member("CodeBlock", &code_block_component, Arity::ZeroOrMore), + // member("Code", &code_component), + ], + [] + ), ); let detail_component = component( @@ -817,12 +857,15 @@ pub fn create_component_model() -> Vec { "Detail", [ property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), - property("actions", mark_doc!("/detail/props/actions.md"), true, component_ref(&action_panel_component)) + property("actions", mark_doc!("/detail/props/actions.md"), true, component_ref(&action_panel_component, Arity::ZeroOrOne)) ], - children_members([ - member("Metadata", &metadata_component), - member("Content", &content_component), - ]), + children_members( + [], + [ + member("Metadata", &metadata_component, Arity::ZeroOrOne), + member("Content", &content_component, Arity::ZeroOrOne), + ], + ), ); @@ -911,9 +954,12 @@ pub fn create_component_model() -> Vec { property("value", "".to_string(), true, PropertyType::String) ]) ], - children_members([ - member("Item", &select_item_component) - ]), + children_members( + [ + member("Item", &select_item_component, Arity::ZeroOrMore ) + ], + [] + ), ); // let multi_select_component = component( @@ -937,18 +983,21 @@ pub fn create_component_model() -> Vec { "Form", [ property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), - property("actions", mark_doc!("/form/props/actions.md"), true, component_ref(&action_panel_component)), + property("actions", mark_doc!("/form/props/actions.md"), true, component_ref(&action_panel_component, Arity::ZeroOrOne)), ], - children_members([ - member("TextField", &text_field_component), - member("PasswordField", &password_field_component), - // member("TextArea", &text_area_component), - member("Checkbox", &checkbox_component), - member("DatePicker", &date_picker_component), - member("Select", &select_component), - // member("MultiSelect", &multi_select_component), - member("Separator", &separator_component), - ]), + children_members( + [ + member("TextField", &text_field_component, Arity::ZeroOrMore), + member("PasswordField", &password_field_component, Arity::ZeroOrMore), + // member("TextArea", &text_area_component), + member("Checkbox", &checkbox_component, Arity::ZeroOrMore), + member("DatePicker", &date_picker_component, Arity::ZeroOrMore), + member("Select", &select_component, Arity::ZeroOrMore), + // member("MultiSelect", &multi_select_component), + member("Separator", &separator_component, Arity::ZeroOrMore), + ], + [] + ), ); let inline_separator_component = component( @@ -956,7 +1005,7 @@ pub fn create_component_model() -> Vec { mark_doc!("/inline_separator/description.md"), "InlineSeparator", [ - property("icon", mark_doc!("/inline_separator/props/icon.md"), true, PropertyType::Enum { name: "Icons".to_owned() }), + property("icon", mark_doc!("/inline_separator/props/icon.md"), true, PropertyType::SharedTypeRef { name: "Icons".to_owned() }), ], children_none(), ); @@ -966,14 +1015,17 @@ pub fn create_component_model() -> Vec { mark_doc!("/inline/description.md"), "Inline", [ - property("actions", mark_doc!("/inline/props/actions.md"), true, component_ref(&action_panel_component)), + property("actions", mark_doc!("/inline/props/actions.md"), true, component_ref(&action_panel_component, Arity::ZeroOrOne)), ], - children_members([ - member("Left", &content_component), - member("Separator", &inline_separator_component), - member("Right", &content_component), - member("Center", &content_component), - ]), + children_members( + [ + member("Left", &content_component, Arity::ZeroOrOne), + member("Separator", &inline_separator_component, Arity::ZeroOrMore), + member("Right", &content_component, Arity::ZeroOrOne), + member("Center", &content_component, Arity::ZeroOrOne), + ], + [] + ), ); let empty_view_component = component( @@ -983,7 +1035,7 @@ pub fn create_component_model() -> Vec { [ property("title", mark_doc!("/empty_view/props/title.md"),false, PropertyType::String), property("description", mark_doc!("/empty_view/props/description.md"),true, PropertyType::String), - property("image", mark_doc!("/empty_view/props/image.md"),true, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] }), + property("image", mark_doc!("/empty_view/props/image.md"),true, PropertyType::SharedTypeRef { name: "Image".to_owned() }), ], children_none(), ); @@ -994,7 +1046,7 @@ pub fn create_component_model() -> Vec { "TextAccessory", [ property("text", mark_doc!("/accessory_text/props/text.md"),false, PropertyType::String), - property("icon", mark_doc!("/accessory_text/props/icon.md"),true, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] }), + property("icon", mark_doc!("/accessory_text/props/icon.md"),true, PropertyType::SharedTypeRef { name: "Image".to_owned() }), property("tooltip", mark_doc!("/accessory_text/props/tooltip.md"),true, PropertyType::String), ], children_none(), @@ -1005,7 +1057,7 @@ pub fn create_component_model() -> Vec { mark_doc!("/accessory_icon/description.md"), "IconAccessory", [ - property("icon", mark_doc!("/accessory_icon/props/icon.md"),false, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] }), + property("icon", mark_doc!("/accessory_icon/props/icon.md"),false, PropertyType::SharedTypeRef { name: "Image".to_owned() }), property("tooltip", mark_doc!("/accessory_icon/props/tooltip.md"),true, PropertyType::String), ], children_none(), @@ -1032,8 +1084,8 @@ pub fn create_component_model() -> Vec { [ property("title", mark_doc!("/list_item/props/title.md"),false, PropertyType::String), property("subtitle", mark_doc!("/list_item/props/subtitle.md"),true, PropertyType::String), - property("icon", mark_doc!("/list_item/props/icon.md"),true, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] }), - property("accessories", mark_doc!("/list_item/props/accessories.md"),true, PropertyType::Array { item: Box::new(PropertyType::Union { items: vec![component_ref(&accessory_text_component), component_ref(&accessory_icon_component)]}) }), + property("icon", mark_doc!("/list_item/props/icon.md"),true, PropertyType::SharedTypeRef { name: "Image".to_owned() }), + property("accessories", mark_doc!("/list_item/props/accessories.md"),true, PropertyType::Array { item: Box::new(PropertyType::Union { items: vec![component_ref(&accessory_text_component, Arity::ZeroOrMore), component_ref(&accessory_icon_component, Arity::ZeroOrMore)]}) }), event("onClick", mark_doc!("/list_item/props/onClick.md"), true, []) ], children_none(), @@ -1047,9 +1099,12 @@ pub fn create_component_model() -> Vec { property("title", mark_doc!("/list_section/props/title.md"),false, PropertyType::String), property("subtitle", mark_doc!("/list_section/props/subtitle.md"),true, PropertyType::String), ], - children_members([ - member("Item", &list_item_component), - ]), + children_members( + [ + member("Item", &list_item_component, Arity::ZeroOrMore), + ], + [] + ), ); let list_component = component( @@ -1057,16 +1112,20 @@ pub fn create_component_model() -> Vec { mark_doc!("/list/description.md"), "List", [ - property("actions", mark_doc!("/list/props/actions.md"), true, component_ref(&action_panel_component)), + property("actions", mark_doc!("/list/props/actions.md"), true, component_ref(&action_panel_component, Arity::ZeroOrOne)), property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), ], - children_members([ - member("SearchBar", &search_bar_component), - member("EmptyView", &empty_view_component), - member("Detail", &detail_component), - member("Item", &list_item_component), - member("Section", &list_section_component), - ]), + children_members( + [ + member("Item", &list_item_component, Arity::ZeroOrMore), + member("Section", &list_section_component, Arity::ZeroOrMore), + ], + [ + member("SearchBar", &search_bar_component, Arity::ZeroOrOne), + member("EmptyView", &empty_view_component, Arity::ZeroOrOne), + member("Detail", &detail_component, Arity::ZeroOrOne), + ] + ), ); let grid_item_component = component( @@ -1076,12 +1135,15 @@ pub fn create_component_model() -> Vec { [ property("title", mark_doc!("/grid_item/props/title.md"), true, PropertyType::String), property("subtitle", mark_doc!("/grid_item/props/subtitle.md"), true, PropertyType::String), - property("accessory", mark_doc!("/grid_item/props/accessory.md"),true, component_ref(&accessory_icon_component)), + property("accessory", mark_doc!("/grid_item/props/accessory.md"),true, component_ref(&accessory_icon_component, Arity::ZeroOrOne)), event("onClick", mark_doc!("/grid_item/props/onClick.md"), true, []) ], - children_members([ - member("Content", &content_component), - ]), + children_members( + [], + [ + member("Content", &content_component, Arity::One), + ] + ), ); let grid_section_component = component( @@ -1096,9 +1158,12 @@ pub fn create_component_model() -> Vec { // fit // inset ], - children_members([ - member("Item", &grid_item_component), - ]), + children_members( + [ + member("Item", &grid_item_component, Arity::ZeroOrMore), + ], + [] + ), ); let grid_component = component( @@ -1107,18 +1172,22 @@ pub fn create_component_model() -> Vec { "Grid", [ property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), - property("actions", mark_doc!("/grid/props/actions.md"),true, component_ref(&action_panel_component)), + property("actions", mark_doc!("/grid/props/actions.md"),true, component_ref(&action_panel_component, Arity::ZeroOrOne)), // property("aspectRatio", true, PropertyType::String), property("columns", mark_doc!("/grid/props/columns.md"),true, PropertyType::Number), // TODO default // fit // inset ], - children_members([ - member("SearchBar", &search_bar_component), - member("EmptyView", &empty_view_component), - member("Item", &grid_item_component), - member("Section", &grid_section_component), - ]), + children_members( + [ + member("Item", &grid_item_component, Arity::ZeroOrMore), + member("Section", &grid_section_component, Arity::ZeroOrMore), + ], + [ + member("SearchBar", &search_bar_component, Arity::ZeroOrOne), + member("EmptyView", &empty_view_component, Arity::ZeroOrOne), + ] + ), ); let text_part = text_part(); diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index b884e37..0a28312 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -1,10 +1,6 @@ -use std::collections::HashMap; - -use deno_core::serde_v8; +use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, RootWidget, UiPropertyValue, UiWidgetId}; use serde::{Deserialize, Serialize}; -use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, UiPropertyValue, UiWidget, UiWidgetId}; - #[derive(Debug)] pub enum JsUiResponseData { Nothing @@ -17,7 +13,7 @@ pub enum JsUiRequestData { entrypoint_name: String, render_location: JsUiRenderLocation, top_level_view: bool, - container: UiWidget, + container: RootWidget, }, ClearInlineView, ShowPluginErrorView { @@ -110,18 +106,6 @@ pub enum JsUiPropertyValue { Undefined, } -#[derive(Deserialize, Serialize)] -pub struct JsUiWidget<'a> { - #[serde(rename = "widgetId")] - pub widget_id: UiWidgetId, - #[serde(rename = "widgetType")] - pub widget_type: String, - #[serde(rename = "widgetProperties")] - pub widget_properties: HashMap>, - #[serde(rename = "widgetChildren")] - pub widget_children: Vec>, -} - #[derive(Debug)] pub enum IntermediateUiEvent { OpenView { diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 327c16a..054c685 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -32,7 +32,7 @@ use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, Se use common::rpc::frontend_api::FrontendApi; use component_model::{create_component_model, Children, Component, Property, PropertyType, SharedType}; -use crate::model::{IntermediateUiEvent, JsKeyboardEventOrigin, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData, JsUiWidget, PreferenceUserData}; +use crate::model::{IntermediateUiEvent, JsKeyboardEventOrigin, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData, PreferenceUserData}; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; diff --git a/rust/server/src/plugins/js/ui.rs b/rust/server/src/plugins/js/ui.rs index 7e0a199..3a80ed8 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/server/src/plugins/js/ui.rs @@ -7,14 +7,15 @@ use deno_core::{op, OpState, serde_v8, v8}; use deno_core::futures::executor::block_on; use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; use indexmap::IndexMap; -use serde::Deserialize; -use common::model::{EntrypointId, PhysicalKey, UiPropertyValue, UiWidget}; +use serde::{de, Deserialize, Deserializer}; +use serde::de::Error; +use common::model::{EntrypointId, PhysicalKey, RootWidget, UiPropertyValue, UiWidget}; use component_model::{Component, Property, PropertyType, SharedType}; -use crate::model::{JsUiRenderLocation, JsUiRequestData, JsUiResponseData, JsUiWidget}; +use component_model::Component::Root; +use crate::model::{JsUiRenderLocation, JsUiRequestData, JsUiResponseData}; use crate::plugins::data_db_repository::DataDbRepository; use crate::plugins::js::{ComponentModel, make_request, PluginData}; - #[op] fn show_plugin_error_view(state: Rc>, entrypoint_id: String, render_location: JsUiRenderLocation) -> anyhow::Result<()> { let data = JsUiRequestData::ShowPluginErrorView { @@ -70,18 +71,17 @@ fn op_inline_view_endpoint_id(state: Rc>) -> Option { } #[op(v8)] -fn op_react_replace_view( +fn op_react_replace_view<'a>( scope: &mut v8::HandleScope, state: Rc>, render_location: JsUiRenderLocation, top_level_view: bool, entrypoint_id: &str, - container: JsUiWidget, + container: serde_v8::Value<'a>, ) -> anyhow::Result<()> { tracing::trace!(target = "renderer_rs", "Calling op_react_replace_view..."); let comp_state = state.borrow(); - let component_model = comp_state.borrow::(); let entrypoint_names = comp_state.borrow::(); let entrypoint_id = EntrypointId::from_string(entrypoint_id); @@ -91,16 +91,15 @@ fn op_react_replace_view( .expect("entrypoint name for id should always exist") .to_string(); - let Component::Root { shared_types, .. } = component_model.components.get("gauntlet:root").unwrap() else { - unreachable!() - }; + let mut deserializer = serde_v8::Deserializer::new(scope, container.v8_value, None); + let container = RootWidget::deserialize(&mut deserializer)?; let data = JsUiRequestData::ReplaceView { entrypoint_id, entrypoint_name, render_location, top_level_view, - container: from_js_to_intermediate_widget(state.clone(), scope, container, component_model, shared_types)?, + container, }; match make_request(&state, data).context("ReplaceView frontend response")? { @@ -173,245 +172,21 @@ async fn show_hud(state: Rc>, display: String) -> anyhow::Resul } } -fn from_js_to_intermediate_widget(state: Rc>, scope: &mut v8::HandleScope, ui_widget: JsUiWidget, component_model: &ComponentModel, shared_types: &IndexMap) -> anyhow::Result { - let children = ui_widget.widget_children.into_iter() - .map(|child| from_js_to_intermediate_widget(state.clone(), scope, child, component_model, shared_types)) - .collect::>>()?; - - let component = component_model.components - .get(&ui_widget.widget_type) - .expect(&format!("component with type {} doesn't exist", &ui_widget.widget_type)); - - let empty = vec![]; - let text_part = vec![Property { name: "value".to_owned(), optional: false, property_type: PropertyType::String, description: "".to_string() }]; - let props = match component { - Component::Standard { props, .. } => props, - Component::Root { .. } => &empty, - Component::TextPart { .. } => &text_part, - }; - - let props = props.into_iter() - .map(|prop| (&prop.name, &prop.property_type)) - .collect::>(); - - let properties = from_js_to_intermediate_properties(state.clone(), scope, ui_widget.widget_properties, &props, shared_types); - - Ok(UiWidget { - widget_id: ui_widget.widget_id, - widget_type: ui_widget.widget_type, - widget_properties: properties?, - widget_children: children, - }) -} - -fn from_js_to_intermediate_properties( - state: Rc>, - scope: &mut v8::HandleScope, - v8_properties: HashMap, - component_props: &HashMap<&String, &PropertyType>, - shared_types: &IndexMap -) -> anyhow::Result> { - let vec = v8_properties.into_iter() - .filter(|(name, _)| name.as_str() != "children") - .filter(|(_, value)| !value.v8_value.is_function()) - .map(|(name, value)| { - let val = value.v8_value; - - let Some(property_type) = component_props.get(&name) else { - return Err(anyhow!("unknown property encountered {:?}", name)) - }; - - if !property_type.is_in_property() { - return Err(anyhow!("unknown property encountered {:?}", name)) - } - - convert(state.clone(), scope, property_type, name, val, shared_types) - }) - .collect::>>()?; - - Ok(vec.into_iter().collect()) -} - -fn convert( - state: Rc>, - scope: &mut v8::HandleScope, - property_type: &PropertyType, - name: String, - value: v8::Local, - shared_types: &IndexMap -) -> anyhow::Result<(String, UiPropertyValue)> { - match property_type { - PropertyType::String | PropertyType::Enum { .. } => { - if value.is_string() { - convert_string(scope, name, value) - } else { - invalid_type_err(name, value, property_type) - } - } - PropertyType::Number => { - if value.is_number() { - convert_num(scope, name, value) - } else { - invalid_type_err(name, value, property_type) - } - } - PropertyType::Boolean => { - if value.is_boolean() { - convert_boolean(scope, name, value) - } else { - invalid_type_err(name, value, property_type) - } - } - PropertyType::Component { .. } => { - panic!("components should not be present here") - } - PropertyType::Function { .. } => { - panic!("functions are filtered out") - } - PropertyType::ImageSource => { - let source: ImageSource = serde_v8::from_v8(scope, value)?; - convert_image_source(state.clone(), name, source) - } - PropertyType::Object { name: object_name } => { - if value.is_object() { - convert_object(state.clone(), scope, name, value, object_name, shared_types) - } else { - invalid_type_err(name, value, property_type) - } - } - PropertyType::Union { items } => { - if value.is_string() { - match items.iter().find(|prop_type| matches!(prop_type, PropertyType::String | PropertyType::Enum { .. })) { - None => invalid_type_err(name, value, property_type), - Some(_) => convert_string(scope, name, value) - } - } else if value.is_number() { - match items.iter().find(|prop_type| matches!(prop_type, PropertyType::Number)) { - None => invalid_type_err(name, value, property_type), - Some(_) => convert_num(scope, name, value) - } - } else if value.is_boolean() { - match items.iter().find(|prop_type| matches!(prop_type, PropertyType::Boolean)) { - None => invalid_type_err(name, value, property_type), - Some(_) => convert_boolean(scope, name, value) - } - } else { - if !value.is_object() { - invalid_type_err(name, value, property_type) - } else { - let image_source = items.iter().find(|prop_type| matches!(prop_type, PropertyType::ImageSource)); - let object = items.iter().find(|prop_type| matches!(prop_type, PropertyType::Object { .. })); - - match (image_source, object) { - (Some(PropertyType::ImageSource), Some(PropertyType::Object { .. })) => { - panic!("image_source and object is conflicting prop_types") - } - (None, Some(PropertyType::Object { name: object_name })) => { - convert_object(state.clone(), scope, name, value, &object_name, shared_types) - } - (Some(PropertyType::ImageSource), None) => { - let source: ImageSource = serde_v8::from_v8(scope, value)?; - convert_image_source(state.clone(), name, source) - } - (Some(_), Some(_)) | (Some(_), None) | (None, Some(_)) => { - unreachable!() - } - (None, None) => { - invalid_type_err(name, value, property_type) - } - } - } - } - } - PropertyType::Array { .. } => { - unimplemented!() - } +// TODO move to separate file +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum ImageSourceOld { + Asset { + asset: String + }, + Url { + url: String } } -fn convert_num(scope: &mut v8::HandleScope, name: String, value: v8::Local) -> anyhow::Result<(String, UiPropertyValue)> { - Ok((name, UiPropertyValue::Number(value.number_value(scope).expect("expected number")))) -} - -fn convert_string(scope: &mut v8::HandleScope, name: String, value: v8::Local) -> anyhow::Result<(String, UiPropertyValue)> { - Ok((name, UiPropertyValue::String(value.to_rust_string_lossy(scope)))) -} - -fn convert_boolean(scope: &mut v8::HandleScope, name: String, value: v8::Local) -> anyhow::Result<(String, UiPropertyValue)> { - Ok((name, UiPropertyValue::Bool(value.boolean_value(scope)))) -} - -fn convert_object( - state: Rc>, - scope: &mut v8::HandleScope, - name: String, - value: v8::Local, - object_name: &str, - shared_types: &IndexMap -) -> anyhow::Result<(String, UiPropertyValue)> { - let object: v8::Local = value.try_into().context(format!("error while reading property {}", name))?; - - let props = object - .get_own_property_names(scope, GetPropertyNamesArgs { - property_filter: PropertyFilter::ONLY_ENUMERABLE | PropertyFilter::SKIP_SYMBOLS, - key_conversion: KeyConversionMode::NoNumbers, - ..Default::default() - }) - .context("error getting get_own_property_names".to_string())?; - - let mut result_obj: HashMap = HashMap::new(); - - for index in 0..props.length() { - let key = props.get_index(scope, index).unwrap(); - let value = object.get(scope, key).unwrap(); - let key = key.to_string(scope).unwrap().to_rust_string_lossy(scope); - - let property_type = match shared_types.get(object_name).unwrap() { - SharedType::Enum { .. } => unreachable!(), - SharedType::Object { items } => items.get(&key).unwrap() - }; - - let (key, value) = convert(state.clone(), scope, property_type, key, value, shared_types)?; - - result_obj.insert(key, value); - } - - Ok((name, UiPropertyValue::Object(result_obj))) -} - -fn invalid_type_err(name: String, value: v8::Local, property_type: &PropertyType) -> anyhow::Result { - Err(anyhow!("invalid type for property {:?}, found: {:?}, expected: {}", name, value.type_repr(), expected_type(property_type))) -} - -fn expected_type(prop_type: &PropertyType) -> String { - match prop_type { - PropertyType::String => "string".to_owned(), - PropertyType::Number => "number".to_owned(), - PropertyType::Boolean => "boolean".to_owned(), - PropertyType::Component { .. } => { - panic!("components should not be present here") - } - PropertyType::Function { .. } => { - panic!("functions are filtered out") - } - PropertyType::ImageSource => "ImageSource".to_owned(), - PropertyType::Enum { .. } => "enum".to_owned(), - PropertyType::Union { items } => { - items.into_iter() - .map(|prop_type| expected_type(prop_type)) - .collect::>() - .join(", ") - }, - PropertyType::Object { .. } => "object".to_owned(), - PropertyType::Array { .. } => { - unimplemented!() - } - } -} - -fn convert_image_source(state: Rc>, name: String, source: ImageSource) -> anyhow::Result<(String, UiPropertyValue)> { +fn convert_image_source(state: Rc>, name: String, source: ImageSourceOld) -> anyhow::Result<(String, UiPropertyValue)> { match source { - ImageSource::Asset { asset } => { + ImageSourceOld::Asset { asset } => { let bytes = { let state = state.borrow(); @@ -431,7 +206,7 @@ fn convert_image_source(state: Rc>, name: String, source: Image Ok((name, UiPropertyValue::Bytes(bytes::Bytes::from(bytes)))) } - ImageSource::Url { url } => { + ImageSourceOld::Url { url } => { // FIXME implement error handling so it doesn't error whole view // TODO implement caching @@ -469,15 +244,3 @@ fn debug_object_to_json( result } - - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum ImageSource { - Asset { - asset: String - }, - Url { - url: String - } -} \ No newline at end of file From f68fcf1b6e819fcbde1d3e2ef359225daeec5123 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 10 Nov 2024 17:09:06 +0100 Subject: [PATCH 158/540] Fix view render taking a long time of while app gen command generator runs --- bundled_plugins/gauntlet/src/applications.ts | 16 ++++++++-------- .../src/plugins/js/plugins/applications.rs | 13 +++++++------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/bundled_plugins/gauntlet/src/applications.ts b/bundled_plugins/gauntlet/src/applications.ts index 0c09f50..2b198e6 100644 --- a/bundled_plugins/gauntlet/src/applications.ts +++ b/bundled_plugins/gauntlet/src/applications.ts @@ -47,7 +47,7 @@ interface InternalApi { linux_open_application(desktop_id: string): void linux_application_dirs(): string[] - linux_app_from_path(path: string): undefined | DesktopPathAction + linux_app_from_path(path: string): Promise> macos_major_version(): number @@ -58,8 +58,8 @@ interface InternalApi { macos_system_applications(): string[] macos_application_dirs(): string[] - macos_app_from_path(path: string): undefined | DesktopPathAction - macos_app_from_arbitrary_path(path: string): undefined | DesktopPathAction + macos_app_from_path(path: string): Promise> + macos_app_from_arbitrary_path(path: string): Promise> macos_open_application(app_path: String): void } @@ -106,7 +106,7 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro } for (const path of InternalApi.macos_system_applications()) { - const app = InternalApi.macos_app_from_path(path) + const app = await InternalApi.macos_app_from_path(path) if (app) { switch (app.type) { case "add": { @@ -146,7 +146,7 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro async function genericGenerator( directoriesToWatch: string[], - appFromPath: (path: string) => undefined | DesktopPathAction, + appFromPath: (path: string) => Promise>, commandFromApp: (id: string, data: DATA) => GeneratedCommand, add: (id: string, data: GeneratedCommand) => void, remove: (id: string) => void, @@ -165,7 +165,7 @@ async function genericGenerator( for (const path of paths) { for await (const dirEntry of walk(path, walkOpts)) { - const app = appFromPath(dirEntry.path); + const app = await appFromPath(dirEntry.path); if (app) { switch (app.type) { case "add": { @@ -180,13 +180,13 @@ async function genericGenerator( const watcher = Deno.watchFs(paths); const handle = debounce( - (event: Deno.FsEvent) => { + async (event: Deno.FsEvent) => { switch (event.kind) { case "create": case "modify": case "remove": { for (const path of event.paths) { - const app = appFromPath(path); + const app = await appFromPath(path); if (app) { switch (app.type) { case "remove": { diff --git a/rust/server/src/plugins/js/plugins/applications.rs b/rust/server/src/plugins/js/plugins/applications.rs index 3e1cf41..a342f5b 100644 --- a/rust/server/src/plugins/js/plugins/applications.rs +++ b/rust/server/src/plugins/js/plugins/applications.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use image::ImageFormat; use image::imageops::FilterType; use serde::Serialize; +use tokio::task::spawn_blocking; #[cfg(target_os = "linux")] mod linux; @@ -69,8 +70,8 @@ pub fn current_os() -> &'static str { #[cfg(target_os = "linux")] #[op] -pub fn linux_app_from_path(path: String) -> Option { - linux::linux_app_from_path(PathBuf::from(path)) +pub async fn linux_app_from_path(path: String) -> anyhow::Result> { + Ok(spawn_blocking(|| linux::linux_app_from_path(PathBuf::from(path))).await?) } #[cfg(target_os = "linux")] @@ -99,14 +100,14 @@ pub fn macos_major_version() -> u8 { #[cfg(target_os = "macos")] #[op] -pub fn macos_app_from_path(path: String) -> Option { - macos::macos_app_from_path(&PathBuf::from(path)) +pub async fn macos_app_from_path(path: String) -> Result> { + Ok(spawn_blocking(|| macos::macos_app_from_path(&PathBuf::from(path))).await?) } #[cfg(target_os = "macos")] #[op] -pub fn macos_app_from_arbitrary_path(path: String) -> Option { - macos::macos_app_from_arbitrary_path(PathBuf::from(path)) +pub async fn macos_app_from_arbitrary_path(path: String) -> Result> { + Ok(spawn_blocking(|| macos::macos_app_from_arbitrary_path(PathBuf::from(path))).await?) } #[cfg(target_os = "macos")] From 26695673a9362a3145c09cdedd670026672369da Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 10 Nov 2024 17:28:51 +0100 Subject: [PATCH 159/540] Fix scrollable resetting when clicking action panel button in bottom panel --- rust/client/src/ui/mod.rs | 10 +++++++--- rust/client/src/ui/widget.rs | 36 ++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index ff76640..b6b4b5b 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -724,6 +724,7 @@ impl Application for AppModel { self.hide_window() } AppMsg::IcedEvent(_) => Command::none(), + AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::Noop, .. } => Command::none(), AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => self.global_state.back(), AppMsg::WidgetEvent { widget_event, plugin_id, render_location } => { self.handle_plugin_event(widget_event, plugin_id, render_location) @@ -1524,7 +1525,7 @@ impl Application for AppModel { let root = match sub_state { MainViewState::None => { render_root( - &false, + false, input, separator, content, @@ -1535,11 +1536,12 @@ impl Application for AppModel { || AppMsg::ToggleActionPanel { keyboard: false }, |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, + || AppMsg::Noop, ) } MainViewState::SearchResultActionPanel { focused_action_item, .. } => { render_root( - &true, + true, input, separator, content, @@ -1550,11 +1552,12 @@ impl Application for AppModel { || AppMsg::ToggleActionPanel { keyboard: false }, |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, + || AppMsg::Noop, ) } MainViewState::InlineViewActionPanel { focused_action_item, .. } => { render_root( - &true, + true, input, separator, content, @@ -1565,6 +1568,7 @@ impl Application for AppModel { || AppMsg::ToggleActionPanel { keyboard: false }, |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, + || AppMsg::Noop, ) } }; diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 79f8ac1..bcd2560 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -470,7 +470,7 @@ impl<'b> ComponentWidgets<'b> { let content = self.render_detail_widget(widget, false); self.render_plugin_root( - show_action_panel, + *show_action_panel, widget.__id__, &None, &widget.content.actions, @@ -980,7 +980,7 @@ impl<'b> ComponentWidgets<'b> { .themed(ContainerStyle::Form); self.render_plugin_root( - show_action_panel, + *show_action_panel, widget_id, &None, &widget.content.actions, @@ -1271,7 +1271,7 @@ impl<'b> ComponentWidgets<'b> { .into(); self.render_plugin_root( - show_action_panel, + *show_action_panel, widget_id, &list_widget.content.search_bar, &list_widget.content.actions, @@ -1419,7 +1419,7 @@ impl<'b> ComponentWidgets<'b> { .themed(ContainerStyle::Grid); self.render_plugin_root( - show_action_panel, + *show_action_panel, grid_widget.__id__, &grid_widget.content.search_bar, &grid_widget.content.actions, @@ -1567,7 +1567,7 @@ impl<'b> ComponentWidgets<'b> { fn render_plugin_root<'a>( &self, - show_action_panel: &bool, + show_action_panel: bool, widget_id: UiWidgetId, search_bar: &Option, action_panel: &Option, @@ -1618,7 +1618,8 @@ impl<'b> ComponentWidgets<'b> { entrypoint_name, || ComponentWidgetEvent::ToggleActionPanel { widget_id }, |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id }, - |widget_id| ComponentWidgetEvent::ActionClick { widget_id } + |widget_id| ComponentWidgetEvent::ActionClick { widget_id }, + || ComponentWidgetEvent::Noop, ) } PluginViewState::ActionPanel { focused_action_item } => { @@ -1633,7 +1634,8 @@ impl<'b> ComponentWidgets<'b> { entrypoint_name, || ComponentWidgetEvent::ToggleActionPanel { widget_id }, |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id }, - |widget_id| ComponentWidgetEvent::ActionClick { widget_id } + |widget_id| ComponentWidgetEvent::ActionClick { widget_id }, + || ComponentWidgetEvent::Noop, ) } } @@ -1981,7 +1983,7 @@ fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T, ACTION>( } pub fn render_root<'a, T: 'a + Clone, ACTION>( - show_action_panel: &bool, + show_action_panel: bool, top_panel: Element<'a, T>, top_separator: Element<'a, T>, content: Element<'a, T>, @@ -1992,6 +1994,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( on_panel_toggle_click: impl Fn() -> T, on_panel_primary_click: impl Fn(UiWidgetId) -> T, on_action_click: impl Fn(UiWidgetId) -> T, + noop_msg: impl Fn() -> T, ) -> Element<'a, T> { let entrypoint_name: Element<_> = text(entrypoint_name) .shaping(Shaping::Advanced) @@ -2109,13 +2112,9 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( let content: Element<_> = column(vec![top_panel, top_separator, content, bottom_panel]) .into(); - let content = if hide_action_panel { - content - } else { - mouse_area(content) - .on_press(on_panel_toggle_click()) - .into() - }; + let content: Element<_> = mouse_area(content) + .on_press(if hide_action_panel { noop_msg() } else { on_panel_toggle_click() }) + .into(); let action_panel_element = match (action_panel, action_panel_scroll_handle) { (Some(action_panel), Some(action_panel_scroll_handle)) => render_action_panel(action_panel, on_action_click, action_panel_scroll_handle), @@ -2227,6 +2226,7 @@ pub enum ComponentWidgetEvent { RunPrimaryAction { widget_id: UiWidgetId, }, + Noop, } include!(concat!(env!("OUT_DIR"), "/components.rs")); @@ -2338,8 +2338,8 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::GridItemClick { widget_id } => { Some(create_grid_item_on_click_event(widget_id)) } - ComponentWidgetEvent::PreviousView => { - panic!("handle event on PreviousView event is not supposed to be called") + ComponentWidgetEvent::Noop | ComponentWidgetEvent::PreviousView => { + panic!("widget_id on these events is not supposed to be called") } ComponentWidgetEvent::RunPrimaryAction { widget_id } => { Some(UiViewEvent::AppEvent { @@ -2367,7 +2367,7 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::ListItemClick { widget_id, .. } => widget_id, ComponentWidgetEvent::GridItemClick { widget_id, .. } => widget_id, ComponentWidgetEvent::RunPrimaryAction { widget_id } => widget_id, - ComponentWidgetEvent::PreviousView => panic!("widget_id on PreviousView event is not supposed to be called"), + ComponentWidgetEvent::Noop | ComponentWidgetEvent::PreviousView => panic!("widget_id on these events is not supposed to be called"), }.to_owned() } } From d3cc26cfd3ecfa399613668d87e014c542e31811 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 10 Nov 2024 17:36:53 +0100 Subject: [PATCH 160/540] Fix build on macOS --- rust/server/src/plugins/js/plugins/applications.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/server/src/plugins/js/plugins/applications.rs b/rust/server/src/plugins/js/plugins/applications.rs index a342f5b..557c662 100644 --- a/rust/server/src/plugins/js/plugins/applications.rs +++ b/rust/server/src/plugins/js/plugins/applications.rs @@ -100,13 +100,13 @@ pub fn macos_major_version() -> u8 { #[cfg(target_os = "macos")] #[op] -pub async fn macos_app_from_path(path: String) -> Result> { +pub async fn macos_app_from_path(path: String) -> anyhow::Result> { Ok(spawn_blocking(|| macos::macos_app_from_path(&PathBuf::from(path))).await?) } #[cfg(target_os = "macos")] #[op] -pub async fn macos_app_from_arbitrary_path(path: String) -> Result> { +pub async fn macos_app_from_arbitrary_path(path: String) -> anyhow::Result> { Ok(spawn_blocking(|| macos::macos_app_from_arbitrary_path(PathBuf::from(path))).await?) } From db0a02f97e55704a0891cede598494b062d8a4d0 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 10 Nov 2024 17:37:43 +0100 Subject: [PATCH 161/540] Ignore events that do not have respective widget state instead of crashing. --- rust/client/src/ui/widget_container.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index c06b1e9..afb93cf 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -68,9 +68,9 @@ impl PluginWidgetContainer { pub fn handle_event(&self, plugin_id: PluginId, event: ComponentWidgetEvent) -> Option { let mut state = self.state.lock().expect("lock is poisoned"); - let state = state.get_mut(&event.widget_id()).expect(&format!("requested state should always be present for event with widget id: {}", &event.widget_id())); - - event.handle(plugin_id, state) + // TODO for events like typing, synchronous render should be implemented instead of ignoring events + state.get_mut(&event.widget_id()) + .and_then(|state| event.handle(plugin_id, state)) } pub fn render_root_widget<'a>( From 479594b38500e073c6a366df559e0a0c3df6fd6d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 10 Nov 2024 17:48:15 +0100 Subject: [PATCH 162/540] Fix command generator example in dev plugin --- dev_plugin/src/command-generator.ts | 109 ++++++++++++++-------------- 1 file changed, 53 insertions(+), 56 deletions(-) diff --git a/dev_plugin/src/command-generator.ts b/dev_plugin/src/command-generator.ts index 774a3ce..91579d1 100644 --- a/dev_plugin/src/command-generator.ts +++ b/dev_plugin/src/command-generator.ts @@ -1,60 +1,57 @@ -import { GeneratedCommand, showHud } from "@project-gauntlet/api/helpers"; +import { GeneratorProps, showHud } from "@project-gauntlet/api/helpers"; -export default function CommandGenerator(): GeneratedCommand[] { - return [ - { - id: 'generated-test-1', - name: 'Generated Item 1', - fn: () => { - new Promise(() => { - throw new Error("gen") - }) +export default function CommandGenerator({ add, remove: _ }: GeneratorProps): void { + add('generated-test-1', { + name: 'Generated Item 1', + fn: () => { + new Promise(() => { + throw new Error("gen") + }) - console.log('generated-test-1') - } - }, - { - id: 'generated-test-2', - name: 'Generated Item 2', - fn: () => { - console.log('generated-test-2') - - sessionStorage.setItem("test", "test") - console.dir(sessionStorage.getItem("test")) - - localStorage.setItem("test", "test") - console.dir(localStorage.getItem("test")) - }, - actions: [ - { - label: "Test 1", - fn: () => { - console.log('generated-action-1') - } - }, - { - ref: "testGeneratedAction1", - label: "Test 2", - fn: () => { - console.log('generated-action-2') - } - } - ] - }, - { - id: 'generated-test-3', - name: 'Generated Item 3', - fn: () => { - showHud("HUD test display") - console.log('generated-test-3') - } - }, - { - id: 'generated-test-4', - name: 'Generated Item 4', - fn: () => { - console.log('generated-test-4') - } + console.log('generated-test-1') } - ] + }) + + add('generated-test-2', { + name: 'Generated Item 2', + fn: () => { + console.log('generated-test-2') + + sessionStorage.setItem("test", "test") + console.dir(sessionStorage.getItem("test")) + + localStorage.setItem("test", "test") + console.dir(localStorage.getItem("test")) + }, + actions: [ + { + label: "Test 1", + fn: () => { + console.log('generated-action-1') + } + }, + { + ref: "testGeneratedAction1", + label: "Test 2", + fn: () => { + console.log('generated-action-2') + } + } + ] + }) + + add('generated-test-3', { + name: 'Generated Item 3', + fn: () => { + showHud("HUD test display") + console.log('generated-test-3') + } + }) + + add('generated-test-4', { + name: 'Generated Item 4', + fn: () => { + console.log('generated-test-4') + } + }) } From 579e29664d9cec7fbf66a4c500c88bba070cabca Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 10 Nov 2024 19:43:59 +0100 Subject: [PATCH 163/540] Show loading bar in main search view when there is still some command generator running --- js/core/src/command-generator.ts | 2 ++ js/typings/index.d.ts | 1 + rust/client/src/ui/mod.rs | 35 +++++++++++++++++++++++++++-- rust/common/src/model.rs | 5 +++++ rust/common/src/rpc/frontend_api.rs | 19 ++++++++++++++++ rust/server/src/model.rs | 7 +++++- rust/server/src/plugins/js/mod.rs | 11 +++++++-- rust/server/src/plugins/js/ui.rs | 31 ++++++++++++++++++++++++- 8 files changed, 105 insertions(+), 6 deletions(-) diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index 4033a52..eca654e 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -76,7 +76,9 @@ export async function runCommandGenerators(): Promise { // noinspection ES6MissingAwait (async () => { try { + InternalApi.update_loading_bar(generatorEntrypointId, true) let cleanup = await generator({ add, remove }) + InternalApi.update_loading_bar(generatorEntrypointId, false) if (typeof cleanup === "function") { generatorCleanups[generatorEntrypointId] = cleanup } diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index e378b47..7e9cf3e 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -120,6 +120,7 @@ interface InternalApi { reload_search_index(searchItems: AdditionalSearchItem[], refreshSearchList: boolean): Promise; show_hud(display: string): void; + update_loading_bar(entrypoint_id: string, show: boolean): void; op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, entrypoint_id: string, container: any): void; show_plugin_error_view(entrypoint_id: string, render_location: RenderLocation): void; diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index b6b4b5b..c80be7b 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -56,6 +56,7 @@ mod state; mod hud; use crate::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; +use crate::ui::custom_widgets::loading_bar::LoadingBar; use crate::ui::hud::{close_hud_window, show_hud_window}; use crate::ui::scroll_handle::ScrollHandle; use crate::ui::state::{ErrorViewData, Focus, GlobalState, MainViewState, PluginViewData, PluginViewState}; @@ -81,6 +82,7 @@ pub struct AppModel { client_context: Arc>, global_state: GlobalState, search_results: Vec, + loading_bar_state: HashMap<(PluginId, EntrypointId), ()>, hud_display: Option } @@ -181,6 +183,11 @@ pub enum AppMsg { shortcut: PhysicalShortcut, responder: Arc>>> }, + UpdateLoadingBar { + plugin_id: PluginId, + entrypoint_id: EntrypointId, + show: bool + }, } pub struct AppFlags { @@ -418,6 +425,7 @@ impl Application for AppModel { global_state, client_context, search_results: vec![], + loading_bar_state: HashMap::new(), hud_display: None, }, Command::batch(commands), @@ -1147,6 +1155,15 @@ impl Application for AppModel { } } + Command::none() + } + AppMsg::UpdateLoadingBar { plugin_id, entrypoint_id, show } => { + if show { + self.loading_bar_state.insert((plugin_id, entrypoint_id), ()); + } else { + self.loading_bar_state.remove(&(plugin_id, entrypoint_id)); + } + Command::none() } } @@ -1433,8 +1450,13 @@ impl Application for AppModel { .width(Length::Fill) .themed(ContainerStyle::MainSearchBar); - let separator = horizontal_rule(1) - .into(); + let separator = if !self.loading_bar_state.is_empty() { + LoadingBar::new() + .into() + } else { + horizontal_rule(1) + .into() + }; let content: Element<_> = column(vec![ inline_view_container(self.client_context.clone()).into(), @@ -2060,6 +2082,15 @@ async fn request_loop( responder: Arc::new(Mutex::new(Some(responder))) } } + UiRequestData::UpdateLoadingBar { plugin_id, entrypoint_id, show } => { + responder.respond(UiResponseData::Nothing); + + AppMsg::UpdateLoadingBar { + plugin_id, + entrypoint_id, + show + } + } } }; diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index ef004d6..d254495 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -153,6 +153,11 @@ pub enum UiRequestData { ShowHud { display: String }, + UpdateLoadingBar { + plugin_id: PluginId, + entrypoint_id: EntrypointId, + show: bool + }, SetGlobalShortcut { shortcut: PhysicalShortcut }, diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index b69b01e..7a52a7c 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -140,6 +140,25 @@ impl FrontendApi { Ok(()) } + pub async fn update_loading_bar( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + show: bool + ) -> Result<(), FrontendApiError> { + let request = UiRequestData::UpdateLoadingBar { + plugin_id, + entrypoint_id, + show, + }; + + let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await? else { + unreachable!() + }; + + Ok(()) + } + pub async fn set_global_shortcut( &self, shortcut: PhysicalShortcut diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 0a28312..84b2101 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -1,4 +1,4 @@ -use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, RootWidget, UiPropertyValue, UiWidgetId}; +use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, UiPropertyValue, UiWidgetId}; use serde::{Deserialize, Serialize}; #[derive(Debug)] @@ -28,6 +28,11 @@ pub enum JsUiRequestData { ShowHud { display: String }, + UpdateLoadingBar { + plugin_id: PluginId, + entrypoint_id: EntrypointId, + show: bool + }, } #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 054c685..f7ede2f 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -37,14 +37,14 @@ use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepositor use crate::plugins::icon_cache::IconCache; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text, Clipboard}; -use crate::plugins::js::command_generators::get_command_generator_entrypoint_ids; +use crate::plugins::js::command_generators::{get_command_generator_entrypoint_ids}; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; use crate::plugins::js::permissions::{permissions_to_deno, PluginPermissions, PluginPermissionsClipboard}; use crate::plugins::js::plugins::numbat::{run_numbat, NumbatContext}; use crate::plugins::js::plugins::settings::open_settings; use crate::plugins::js::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; use crate::plugins::js::search::reload_search_index; -use crate::plugins::js::ui::{clear_inline_view, fetch_action_id_for_shortcut, op_component_model, op_inline_view_endpoint_id, op_react_replace_view, show_hud, show_plugin_error_view, show_preferences_required_view}; +use crate::plugins::js::ui::{clear_inline_view, fetch_action_id_for_shortcut, op_component_model, op_inline_view_endpoint_id, op_react_replace_view, show_hud, show_plugin_error_view, show_preferences_required_view, update_loading_bar}; use crate::plugins::run_status::RunStatusGuard; use crate::search::{SearchIndex, SearchIndexItem}; @@ -509,6 +509,7 @@ deno_core::extension!( op_component_model, fetch_action_id_for_shortcut, show_hud, + update_loading_bar, // preferences get_plugin_preferences, @@ -672,6 +673,12 @@ async fn make_request_async(plugin_id: PluginId, plugin_name: String, frontend_a frontend_api.show_hud(display).await?; + Ok(JsUiResponseData::Nothing) + } + JsUiRequestData::UpdateLoadingBar { plugin_id, entrypoint_id, show } => { + + frontend_api.update_loading_bar(plugin_id, entrypoint_id, show).await?; + Ok(JsUiResponseData::Nothing) } } diff --git a/rust/server/src/plugins/js/ui.rs b/rust/server/src/plugins/js/ui.rs index 3a80ed8..bbf7809 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/server/src/plugins/js/ui.rs @@ -9,7 +9,7 @@ use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; use indexmap::IndexMap; use serde::{de, Deserialize, Deserializer}; use serde::de::Error; -use common::model::{EntrypointId, PhysicalKey, RootWidget, UiPropertyValue, UiWidget}; +use common::model::{EntrypointId, PhysicalKey, PluginId, RootWidget, UiPropertyValue, UiWidget}; use component_model::{Component, Property, PropertyType, SharedType}; use component_model::Component::Root; use crate::model::{JsUiRenderLocation, JsUiRequestData, JsUiResponseData}; @@ -172,6 +172,35 @@ async fn show_hud(state: Rc>, display: String) -> anyhow::Resul } } +#[op] +async fn update_loading_bar(state: Rc>, entrypoint_id: String, show: bool) -> anyhow::Result<()> { + let plugin_id = { + let state = state.borrow(); + + let plugin_id = state + .borrow::() + .plugin_id() + .clone(); + + plugin_id + }; + + let data = JsUiRequestData::UpdateLoadingBar { + plugin_id: PluginId::from_string(plugin_id), + entrypoint_id: EntrypointId::from_string(entrypoint_id), + show, + }; + + match make_request(&state, data).context("UpdateLoadingBar response")? { + JsUiResponseData::Nothing => { + tracing::trace!("Calling update_loading_bar returned"); + Ok(()) + } + value @ _ => panic!("unsupported response type {:?}", value), + } +} + + // TODO move to separate file #[derive(Debug, Deserialize)] #[serde(untagged)] From e132fe10a5cb12dd9921506e4d4c5c31ea17070a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 10 Nov 2024 22:03:48 +0100 Subject: [PATCH 164/540] Bring back image support after react renderer parsing rework --- rust/client/src/ui/client_context.rs | 15 +- rust/client/src/ui/mod.rs | 4 +- rust/client/src/ui/widget.rs | 71 ++++--- rust/client/src/ui/widget_container.rs | 29 ++- rust/common/src/model.rs | 252 +++++++++++++++++++++++++ rust/common/src/rpc/frontend_api.rs | 5 +- rust/server/src/model.rs | 2 + rust/server/src/plugins/js/mod.rs | 4 +- rust/server/src/plugins/js/ui.rs | 68 ++++--- 9 files changed, 382 insertions(+), 68 deletions(-) diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index aeff421..c780020 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -74,10 +74,19 @@ impl ClientContext { self.view.get_entrypoint_id() } - pub fn replace_view(&mut self, render_location: UiRenderLocation, container: RootWidget, plugin_id: &PluginId, plugin_name: &str, entrypoint_id: &EntrypointId, entrypoint_name: &str) { + pub fn replace_view( + &mut self, + render_location: UiRenderLocation, + container: RootWidget, + images: HashMap, + plugin_id: &PluginId, + plugin_name: &str, + entrypoint_id: &EntrypointId, + entrypoint_name: &str + ) { match render_location { - UiRenderLocation::InlineView => self.get_mut_inline_view_container(plugin_id).replace_view(container, plugin_id, plugin_name, entrypoint_id, entrypoint_name), - UiRenderLocation::View => self.get_mut_view_container().replace_view(container, plugin_id, plugin_name, entrypoint_id, entrypoint_name) + UiRenderLocation::InlineView => self.get_mut_inline_view_container(plugin_id).replace_view(container, images, plugin_id, plugin_name, entrypoint_id, entrypoint_name), + UiRenderLocation::View => self.get_mut_view_container().replace_view(container, images, plugin_id, plugin_name, entrypoint_id, entrypoint_name) } } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index c80be7b..e9cf57a 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -2007,13 +2007,15 @@ async fn request_loop( entrypoint_name, render_location, top_level_view, - container + container, + images } => { let has_children = container.content.is_some(); client_context.replace_view( render_location, container, + images, &plugin_id, &plugin_name, &entrypoint_id, diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index bcd2560..5865395 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -8,8 +8,9 @@ use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; use iced::widget::text::Shaping; use iced::widget::tooltip::Position; -use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, mouse_area, pick_list, row, scrollable, text, text_input, tooltip, vertical_rule, Space}; +use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, mouse_area, pick_list, row, scrollable, text, text_input, tooltip, vertical_rule, Space}; use iced::{Alignment, Font, Length}; +use iced::widget::image::Handle; use iced_aw::core::icons; use iced_aw::date_picker::Date; use iced_aw::floating_element::Offset; @@ -38,14 +39,20 @@ use crate::ui::AppMsg; #[derive(Debug)] pub struct ComponentWidgets<'b> { root_widget: &'b mut Option, - state: &'b mut HashMap + state: &'b mut HashMap, + images: &'b HashMap } impl<'b> ComponentWidgets<'b> { - pub fn new(root_widget: &'b mut Option, state: &'b mut HashMap) -> ComponentWidgets<'b> { + pub fn new( + root_widget: &'b mut Option, + state: &'b mut HashMap, + images: &'b HashMap + ) -> ComponentWidgets<'b> { Self { root_widget, - state + state, + images } } @@ -653,7 +660,7 @@ impl<'b> ComponentWidgets<'b> { fn render_image_widget<'a>(&self, widget: &ImageWidget, centered: bool) -> Element<'a, ComponentWidgetEvent> { // TODO image size, height and width - let content: Element<_> = render_image(&widget.source, None); + let content: Element<_> = self.render_image(widget.__id__, &widget.source, None); let mut content = container(content) .width(Length::Fill); @@ -1066,7 +1073,7 @@ impl<'b> ComponentWidgets<'b> { fn render_empty_view_widget<'a>(&self, widget: &EmptyViewWidget) -> Element<'a, ComponentWidgetEvent> { let image: Option> = widget.image .as_ref() - .map(|image| render_image(image, Some(TextStyle::EmptyViewSubtitle))); + .map(|image| self.render_image(widget.__id__, image, Some(TextStyle::EmptyViewSubtitle))); let title: Element<_> = text(&widget.title) .shaping(Shaping::Advanced) @@ -1112,7 +1119,7 @@ impl<'b> ComponentWidgets<'b> { } fn render_icon_accessory<'a>(&self, widget: &IconAccessoryWidget) -> Element<'a, ComponentWidgetEvent> { - let icon = render_image(&widget.icon, Some(TextStyle::IconAccessory)); + let icon = self.render_image(widget.__id__, &widget.icon, Some(TextStyle::IconAccessory)); let content = container(icon) .center_x() @@ -1135,7 +1142,7 @@ impl<'b> ComponentWidgets<'b> { fn render_text_accessory<'a>(&self, widget: &TextAccessoryWidget) -> Element<'a, ComponentWidgetEvent> { let icon: Option> = widget.icon .as_ref() - .map(|icon| render_image(icon, Some(TextStyle::TextAccessory))); + .map(|icon| self.render_image(widget.__id__, icon, Some(TextStyle::TextAccessory))); let text_content: Element<_> = text(&widget.text) .shaping(Shaping::Advanced) @@ -1302,7 +1309,7 @@ impl<'b> ComponentWidgets<'b> { fn render_list_item_widget<'a>(&self, widget: &ListItemWidget) -> Element<'a, ComponentWidgetEvent> { let icon: Option> = widget.icon .as_ref() - .map(|icon| render_image(icon, None)); + .map(|icon| self.render_image(widget.__id__, icon, None)); let title: Element<_> = text(&widget.title) .shaping(Shaping::Advanced) @@ -1640,29 +1647,33 @@ impl<'b> ComponentWidgets<'b> { } } } -} -fn render_image<'a>(image: &Image, icon_style: Option) -> Element<'a, ComponentWidgetEvent> { - match image { - Image::ImageSource(source) => { - // TODO image support - // image(Handle::from_memory(bytes.clone())) - // .into() - - horizontal_space() - .into() - } - Image::Icons(icon) => { - match icon_style { - None => { - text(icon_to_bootstrap(icon)) - .font(icons::BOOTSTRAP_FONT) - .into() + fn render_image<'a>(&self, widget_id: UiWidgetId, image_data: &Image, icon_style: Option) -> Element<'a, ComponentWidgetEvent> { + match image_data { + Image::ImageSource(_) => { + match self.images.get(&widget_id) { + Some(bytes) => { + image(Handle::from_memory(bytes.clone())) + .into() + } + None => { + horizontal_space() + .into() + } } - Some(icon_style) => { - text(icon_to_bootstrap(icon)) - .font(icons::BOOTSTRAP_FONT) - .themed(icon_style) + } + Image::Icons(icon) => { + match icon_style { + None => { + text(icon_to_bootstrap(icon)) + .font(icons::BOOTSTRAP_FONT) + .into() + } + Some(icon_style) => { + text(icon_to_bootstrap(icon)) + .font(icons::BOOTSTRAP_FONT) + .themed(icon_style) + } } } } diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index afb93cf..f3714d7 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -12,6 +12,7 @@ use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; pub struct PluginWidgetContainer { root_widget: Arc>>, state: Arc>>, + images: HashMap, plugin_id: Option, plugin_name: Option, entrypoint_id: Option, @@ -23,6 +24,7 @@ impl PluginWidgetContainer { Self { root_widget: Arc::new(Mutex::new(None)), state: Arc::new(Mutex::new(HashMap::new())), + images: HashMap::new(), plugin_id: None, plugin_name: None, entrypoint_id: None, @@ -38,13 +40,22 @@ impl PluginWidgetContainer { self.entrypoint_id.clone().expect("entrypoint id should always exist after render") } - pub fn replace_view(&mut self, container: RootWidget, plugin_id: &PluginId, plugin_name: &str, entrypoint_id: &EntrypointId, entrypoint_name: &str) { + pub fn replace_view( + &mut self, + container: RootWidget, + images: HashMap, + plugin_id: &PluginId, + plugin_name: &str, + entrypoint_id: &EntrypointId, + entrypoint_name: &str + ) { tracing::trace!("replace_view is called. container: {:?}", container); self.plugin_id = Some(plugin_id.clone()); self.plugin_name = Some(plugin_name.to_string()); self.entrypoint_id = Some(entrypoint_id.clone()); self.entrypoint_name = Some(entrypoint_name.to_string()); + self.images = images; let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); @@ -81,7 +92,7 @@ impl PluginWidgetContainer { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state) + ComponentWidgets::new(&mut root_widget, &mut state, &self.images) .render_root_widget(plugin_view_state, self.entrypoint_name.as_ref(), action_shortcuts) } @@ -89,7 +100,7 @@ impl PluginWidgetContainer { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state) + ComponentWidgets::new(&mut root_widget, &mut state, &self.images) .render_root_inline_widget(self.plugin_name.as_ref(), self.entrypoint_name.as_ref()) } @@ -97,41 +108,41 @@ impl PluginWidgetContainer { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state).toggle_action_panel() + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).toggle_action_panel() } pub fn get_action_ids(&self) -> Vec { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state).get_action_ids() + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).get_action_ids() } pub fn get_action_panel(&self, action_shortcuts: &HashMap) -> Option { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state).get_action_panel(action_shortcuts) + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).get_action_panel(action_shortcuts) } pub fn keyboard_navigation_width(&self) -> Option { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state).keyboard_navigation_width() + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).keyboard_navigation_width() } pub fn keyboard_navigation_total(&self) -> usize { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state).keyboard_navigation_total() + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).keyboard_navigation_total() } pub fn has_search_bar(&self) -> bool { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state).has_search_bar() + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).has_search_bar() } } diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index d254495..81b791b 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -137,6 +137,7 @@ pub enum UiRequestData { render_location: UiRenderLocation, top_level_view: bool, container: RootWidget, + images: HashMap, }, ShowPreferenceRequiredView { plugin_id: PluginId, @@ -250,6 +251,257 @@ fn array_to_option<'de, D, V>(deserializer: D) -> Result, D::Error> wh include!(concat!(env!("OUT_DIR"), "/components.rs")); + +// TODO generate this +pub trait WidgetVisitor { + fn action_widget(&mut self, _widget: &ActionWidget) { + } + fn action_panel_section_widget(&mut self, widget: &ActionPanelSectionWidget) { + for members in &widget.content.ordered_members { + match members { + ActionPanelSectionWidgetOrderedMembers::Action(widget) => self.action_widget(widget) + } + } + } + fn action_panel_widget(&mut self, widget: &ActionPanelWidget) { + for members in &widget.content.ordered_members { + match members { + ActionPanelWidgetOrderedMembers::Action(widget) => self.action_widget(widget), + ActionPanelWidgetOrderedMembers::ActionPanelSection(widget) => self.action_panel_section_widget(widget) + } + } + } + + fn metadata_link_widget(&mut self, _widget: &MetadataLinkWidget) {} + fn metadata_tag_item_widget(&mut self, _widget: &MetadataTagItemWidget) {} + fn metadata_tag_list_widget(&mut self, widget: &MetadataTagListWidget) { + for members in &widget.content.ordered_members { + match members { + MetadataTagListWidgetOrderedMembers::MetadataTagItem(widget) => self.metadata_tag_item_widget(widget) + } + } + } + fn metadata_separator_widget(&mut self, _widget: &MetadataSeparatorWidget) {} + fn metadata_value_widget(&mut self, _widget: &MetadataValueWidget) {} + fn metadata_icon_widget(&mut self, _widget: &MetadataIconWidget) {} + fn metadata_widget(&mut self, widget: &MetadataWidget) { + for members in &widget.content.ordered_members { + match members { + MetadataWidgetOrderedMembers::MetadataTagList(widget) => self.metadata_tag_list_widget(widget), + MetadataWidgetOrderedMembers::MetadataLink(widget) => self.metadata_link_widget(widget), + MetadataWidgetOrderedMembers::MetadataValue(widget) => self.metadata_value_widget(widget), + MetadataWidgetOrderedMembers::MetadataIcon(widget) => self.metadata_icon_widget(widget), + MetadataWidgetOrderedMembers::MetadataSeparator(widget) => self.metadata_separator_widget(widget), + } + } + } + + fn image(&mut self, _widget_id: UiWidgetId, _widget: &Image) { + + } + + fn image_widget(&mut self, widget: &ImageWidget) { + self.image(widget.__id__, &widget.source) + } + fn h1_widget(&mut self, _widget: &H1Widget) {} + fn h2_widget(&mut self, _widget: &H2Widget) {} + fn h3_widget(&mut self, _widget: &H3Widget) {} + fn h4_widget(&mut self, _widget: &H4Widget) {} + fn h5_widget(&mut self, _widget: &H5Widget) {} + fn h6_widget(&mut self, _widget: &H6Widget) {} + fn horizontal_break_widget(&mut self, _widget: &HorizontalBreakWidget) {} + fn code_block_widget(&mut self, _widget: &CodeBlockWidget) {} + fn paragraph_widget(&mut self, _widget: &ParagraphWidget) { + } + fn content_widget(&mut self, widget: &ContentWidget) { + for members in &widget.content.ordered_members { + match members { + ContentWidgetOrderedMembers::Paragraph(widget) => self.paragraph_widget(widget), + ContentWidgetOrderedMembers::Image(widget) => self.image_widget(widget), + ContentWidgetOrderedMembers::H1(widget) => self.h1_widget(widget), + ContentWidgetOrderedMembers::H2(widget) => self.h2_widget(widget), + ContentWidgetOrderedMembers::H3(widget) => self.h3_widget(widget), + ContentWidgetOrderedMembers::H4(widget) => self.h4_widget(widget), + ContentWidgetOrderedMembers::H5(widget) => self.h5_widget(widget), + ContentWidgetOrderedMembers::H6(widget) => self.h6_widget(widget), + ContentWidgetOrderedMembers::HorizontalBreak(widget) => self.horizontal_break_widget(widget), + ContentWidgetOrderedMembers::CodeBlock(widget) => self.code_block_widget(widget), + } + } + } + + fn detail_widget(&mut self, widget: &DetailWidget) { + if let Some(widget) = &widget.content.actions { + self.action_panel_widget(widget) + } + if let Some(widget) = &widget.content.metadata { + self.metadata_widget(widget) + } + if let Some(widget) = &widget.content.content { + self.content_widget(widget) + } + } + + fn text_field_widget(&mut self, _widget: &TextFieldWidget) {} + fn password_field_widget(&mut self, _widget: &PasswordFieldWidget) {} + fn checkbox_widget(&mut self, _widget: &CheckboxWidget) {} + fn date_picker_widget(&mut self, _widget: &DatePickerWidget) {} + fn select_item_widget(&mut self, _widget: &SelectItemWidget) { + } + fn select_widget(&mut self, widget: &SelectWidget) { + for members in &widget.content.ordered_members { + match members { + SelectWidgetOrderedMembers::SelectItem(widget) => self.select_item_widget(widget) + } + } + } + fn separator_widget(&mut self, _widget: &SeparatorWidget) { + } + fn form_widget(&mut self, widget: &FormWidget) { + if let Some(widget) = &widget.content.actions { + self.action_panel_widget(widget) + } + for members in &widget.content.ordered_members { + match members { + FormWidgetOrderedMembers::TextField(widget) => self.text_field_widget(widget), + FormWidgetOrderedMembers::PasswordField(widget) => self.password_field_widget(widget), + FormWidgetOrderedMembers::Checkbox(widget) => self.checkbox_widget(widget), + FormWidgetOrderedMembers::DatePicker(widget) => self.date_picker_widget(widget), + FormWidgetOrderedMembers::Select(widget) => self.select_widget(widget), + FormWidgetOrderedMembers::Separator(widget) => self.separator_widget(widget), + } + } + } + + fn inline_separator_widget(&mut self, _widget: &InlineSeparatorWidget) { + } + + fn inline_widget(&mut self, widget: &InlineWidget) { + if let Some(widget) = &widget.content.actions { + self.action_panel_widget(widget) + } + for members in &widget.content.ordered_members { + match members { + InlineWidgetOrderedMembers::Content(widget) => self.content_widget(widget), + InlineWidgetOrderedMembers::InlineSeparator(widget) => self.inline_separator_widget(widget) + } + } + } + + fn empty_view_widget(&mut self, widget: &EmptyViewWidget) { + if let Some(image) = &widget.image { + self.image(widget.__id__, image) + } + } + + fn icon_accessory_widget(&mut self, widget: &IconAccessoryWidget) { + self.image(widget.__id__, &widget.icon) + } + fn text_accessory_widget(&mut self, widget: &TextAccessoryWidget) { + if let Some(image) = &widget.icon { + self.image(widget.__id__, image) + } + } + + fn search_bar_widget(&mut self, _widget: &SearchBarWidget) {} + + fn list_item_widget(&mut self, widget: &ListItemWidget) { + if let Some(image) = &widget.icon { + self.image(widget.__id__, image) + } + + for accessories in &widget.content.accessories { + match accessories { + ListItemAccessories::_0(widget) => self.text_accessory_widget(widget), + ListItemAccessories::_1(widget) => self.icon_accessory_widget(widget) + } + } + } + fn list_section_widget(&mut self, widget: &ListSectionWidget) { + for members in &widget.content.ordered_members { + match members { + ListSectionWidgetOrderedMembers::ListItem(widget) => self.list_item_widget(widget) + } + } + } + + fn list_widget(&mut self, widget: &ListWidget) { + if let Some(widget) = &widget.content.actions { + self.action_panel_widget(widget) + } + if let Some(widget) = &widget.content.search_bar { + self.search_bar_widget(widget) + } + if let Some(widget) = &widget.content.empty_view { + self.empty_view_widget(widget) + } + if let Some(widget) = &widget.content.detail { + self.detail_widget(widget) + } + for members in &widget.content.ordered_members { + match members { + ListWidgetOrderedMembers::ListItem(widget) => self.list_item_widget(widget), + ListWidgetOrderedMembers::ListSection(widget) => self.list_section_widget(widget), + } + } + } + fn grid_item_widget(&mut self, widget: &GridItemWidget) { + if let Some(widget) = &widget.content.accessory { + self.icon_accessory_widget(widget) + } + for members in &widget.content.content.content.ordered_members { + match members { + ContentWidgetOrderedMembers::Paragraph(widget) => self.paragraph_widget(widget), + ContentWidgetOrderedMembers::Image(widget) => self.image_widget(widget), + ContentWidgetOrderedMembers::H1(widget) => self.h1_widget(widget), + ContentWidgetOrderedMembers::H2(widget) => self.h2_widget(widget), + ContentWidgetOrderedMembers::H3(widget) => self.h3_widget(widget), + ContentWidgetOrderedMembers::H4(widget) => self.h4_widget(widget), + ContentWidgetOrderedMembers::H5(widget) => self.h5_widget(widget), + ContentWidgetOrderedMembers::H6(widget) => self.h6_widget(widget), + ContentWidgetOrderedMembers::HorizontalBreak(widget) => self.horizontal_break_widget(widget), + ContentWidgetOrderedMembers::CodeBlock(widget) => self.code_block_widget(widget), + } + } + } + fn grid_section_widget(&mut self, widget: &GridSectionWidget) { + for members in &widget.content.ordered_members { + match members { + GridSectionWidgetOrderedMembers::GridItem(widget) => self.grid_item_widget(widget) + } + } + } + fn grid_widget(&mut self, widget: &GridWidget) { + if let Some(widget) = &widget.content.actions { + self.action_panel_widget(widget) + } + if let Some(widget) = &widget.content.search_bar { + self.search_bar_widget(widget) + } + if let Some(widget) = &widget.content.empty_view { + self.empty_view_widget(widget) + } + for members in &widget.content.ordered_members { + match members { + GridWidgetOrderedMembers::GridItem(widget) => self.grid_item_widget(widget), + GridWidgetOrderedMembers::GridSection(widget) => self.grid_section_widget(widget) + } + } + } + + fn root_widget(&mut self, root_widget: &RootWidget) { + if let Some(members) = &root_widget.content { + match members { + RootWidgetMembers::Detail(widget) => self.detail_widget(widget), + RootWidgetMembers::Form(widget) => self.form_widget(widget), + RootWidgetMembers::Inline(widget) => self.inline_widget(widget), + RootWidgetMembers::List(widget) => self.list_widget(widget), + RootWidgetMembers::Grid(widget) => self.grid_widget(widget), + } + } + } +} + #[derive(Debug, Clone)] pub struct UiWidget { pub widget_id: UiWidgetId, diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index 7a52a7c..f80f857 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -1,8 +1,9 @@ +use std::collections::HashMap; use anyhow::anyhow; use thiserror::Error; use utils::channel::{RequestError, RequestSender}; -use crate::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiRequestData, UiResponseData}; +use crate::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; #[derive(Error, Debug)] pub enum FrontendApiError { @@ -47,6 +48,7 @@ impl FrontendApi { render_location: UiRenderLocation, top_level_view: bool, container: RootWidget, + images: HashMap, ) -> Result<(), FrontendApiError> { let request = UiRequestData::ReplaceView { plugin_id, @@ -56,6 +58,7 @@ impl FrontendApi { render_location, top_level_view, container, + images, }; let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await? else { diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 84b2101..51163e9 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, UiPropertyValue, UiWidgetId}; use serde::{Deserialize, Serialize}; @@ -14,6 +15,7 @@ pub enum JsUiRequestData { render_location: JsUiRenderLocation, top_level_view: bool, container: RootWidget, + images: HashMap, }, ClearInlineView, ShowPluginErrorView { diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index f7ede2f..e5be78a 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -637,13 +637,13 @@ fn make_request(state: &Rc>, data: JsUiRequestData) -> anyhow:: async fn make_request_async(plugin_id: PluginId, plugin_name: String, frontend_api: &mut FrontendApi, data: JsUiRequestData) -> anyhow::Result { match data { - JsUiRequestData::ReplaceView { render_location, top_level_view, container, entrypoint_id, entrypoint_name } => { + JsUiRequestData::ReplaceView { render_location, top_level_view, container, entrypoint_id, entrypoint_name, images } => { let render_location = match render_location { // TODO into? JsUiRenderLocation::InlineView => UiRenderLocation::InlineView, JsUiRenderLocation::View => UiRenderLocation::View, }; - frontend_api.replace_view(plugin_id, plugin_name, entrypoint_id, entrypoint_name, render_location, top_level_view, container).await?; + frontend_api.replace_view(plugin_id, plugin_name, entrypoint_id, entrypoint_name, render_location, top_level_view, container, images).await?; Ok(JsUiResponseData::Nothing) } diff --git a/rust/server/src/plugins/js/ui.rs b/rust/server/src/plugins/js/ui.rs index bbf7809..46c4951 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/server/src/plugins/js/ui.rs @@ -9,7 +9,7 @@ use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; use indexmap::IndexMap; use serde::{de, Deserialize, Deserializer}; use serde::de::Error; -use common::model::{EntrypointId, PhysicalKey, PluginId, RootWidget, UiPropertyValue, UiWidget}; +use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, EntrypointId, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Image, ImageSource, ImageSourceAsset, ImageSourceUrl, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectItemWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiPropertyValue, UiWidget, UiWidgetId, WidgetVisitor}; use component_model::{Component, Property, PropertyType, SharedType}; use component_model::Component::Root; use crate::model::{JsUiRenderLocation, JsUiRequestData, JsUiResponseData}; @@ -81,25 +81,33 @@ fn op_react_replace_view<'a>( ) -> anyhow::Result<()> { tracing::trace!(target = "renderer_rs", "Calling op_react_replace_view..."); - let comp_state = state.borrow(); - let entrypoint_names = comp_state.borrow::(); - let entrypoint_id = EntrypointId::from_string(entrypoint_id); - let entrypoint_name = entrypoint_names.entrypoint_names - .get(&entrypoint_id) - .expect("entrypoint name for id should always exist") - .to_string(); + let entrypoint_name = { + let comp_state = state.borrow(); + + let plugin_data = comp_state.borrow::(); + + let entrypoint_name = plugin_data.entrypoint_names + .get(&entrypoint_id) + .expect("entrypoint name for id should always exist") + .to_string(); + + entrypoint_name + }; let mut deserializer = serde_v8::Deserializer::new(scope, container.v8_value, None); let container = RootWidget::deserialize(&mut deserializer)?; + let images = ImageGatherer::run_gatherer(state.clone(), &container)?; + let data = JsUiRequestData::ReplaceView { entrypoint_id, entrypoint_name, render_location, top_level_view, container, + images, }; match make_request(&state, data).context("ReplaceView frontend response")? { @@ -200,22 +208,38 @@ async fn update_loading_bar(state: Rc>, entrypoint_id: String, } } +struct ImageGatherer { + state: Rc>, + image_sources: HashMap> +} -// TODO move to separate file -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum ImageSourceOld { - Asset { - asset: String - }, - Url { - url: String +impl WidgetVisitor for ImageGatherer { + fn image(&mut self, widget_id: UiWidgetId, widget: &Image) { + if let Image::ImageSource(image_source) = &widget { + self.image_sources.insert(widget_id, get_image_date(self.state.clone(), image_source)); + } } } -fn convert_image_source(state: Rc>, name: String, source: ImageSourceOld) -> anyhow::Result<(String, UiPropertyValue)> { +impl ImageGatherer { + fn run_gatherer(state: Rc>, root_widget: &RootWidget) -> anyhow::Result> { + let mut gatherer = Self { + state, + image_sources: HashMap::new() + }; + + gatherer.root_widget(root_widget); + + gatherer.image_sources + .into_iter() + .map(|(widget_id, image)| image.map(|image| (widget_id, image))) + .collect::>() + } +} + +fn get_image_date(state: Rc>, source: &ImageSource) -> anyhow::Result { match source { - ImageSourceOld::Asset { asset } => { + ImageSource::ImageSourceAsset(ImageSourceAsset { asset }) => { let bytes = { let state = state.borrow(); @@ -233,9 +257,9 @@ fn convert_image_source(state: Rc>, name: String, source: Image })? }; - Ok((name, UiPropertyValue::Bytes(bytes::Bytes::from(bytes)))) + Ok(bytes::Bytes::from(bytes)) } - ImageSourceOld::Url { url } => { + ImageSource::ImageSourceUrl(ImageSourceUrl { url }) => { // FIXME implement error handling so it doesn't error whole view // TODO implement caching @@ -246,7 +270,7 @@ fn convert_image_source(state: Rc>, name: String, source: Image .collect::>>()? .into(); - Ok((name, UiPropertyValue::Bytes(bytes))) + Ok(bytes) } } } From 589c3748f291f131012c8b2d7e19648e346e19b6 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 10 Nov 2024 22:51:31 +0100 Subject: [PATCH 165/540] Add loading bar when opening plugin view takes more than 300 milliseconds --- rust/client/src/ui/mod.rs | 39 ++++++++++++++++++++++++++++----- rust/client/src/ui/state/mod.rs | 12 +++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index e9cf57a..be80798 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -59,7 +59,7 @@ use crate::global_shortcut::{convert_physical_shortcut_to_hotkey, register_liste use crate::ui::custom_widgets::loading_bar::LoadingBar; use crate::ui::hud::{close_hud_window, show_hud_window}; use crate::ui::scroll_handle::ScrollHandle; -use crate::ui::state::{ErrorViewData, Focus, GlobalState, MainViewState, PluginViewData, PluginViewState}; +use crate::ui::state::{ErrorViewData, Focus, GlobalState, LoadingBarState, MainViewState, PluginViewData, PluginViewState}; use crate::ui::widget_container::PluginWidgetContainer; pub use theme::GauntletTheme; @@ -188,6 +188,8 @@ pub enum AppMsg { entrypoint_id: EntrypointId, show: bool }, + PendingPluginViewLoadingBar, + ShowPluginViewLoadingBar, } pub struct AppFlags { @@ -454,7 +456,10 @@ impl Application for AppModel { action_shortcuts: HashMap::new(), }); - self.open_plugin_view(plugin_id, entrypoint_id) + Command::batch([ + self.open_plugin_view(plugin_id, entrypoint_id), + Command::perform(async move { AppMsg::PendingPluginViewLoadingBar }, std::convert::identity) + ]) } GlobalState::ErrorView { .. } => { Command::none() @@ -515,7 +520,11 @@ impl Application for AppModel { } AppMsg::ReplaceView { top_level_view, render_location, has_children } => { match &mut self.global_state { - GlobalState::MainView { pending_plugin_view_data, focused_search_result, .. } => { + GlobalState::MainView { pending_plugin_view_data, focused_search_result, pending_plugin_view_loading_bar, .. } => { + + if let LoadingBarState::Pending = pending_plugin_view_loading_bar { + *pending_plugin_view_loading_bar = LoadingBarState::Off; + } if has_children { if let UiRenderLocation::InlineView = render_location { @@ -1164,6 +1173,26 @@ impl Application for AppModel { self.loading_bar_state.remove(&(plugin_id, entrypoint_id)); } + Command::none() + } + AppMsg::PendingPluginViewLoadingBar => { + if let GlobalState::MainView { pending_plugin_view_loading_bar, .. } = &mut self.global_state { + *pending_plugin_view_loading_bar = LoadingBarState::Pending; + } + + Command::perform(async move { + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + + AppMsg::ShowPluginViewLoadingBar + }, std::convert::identity) + } + AppMsg::ShowPluginViewLoadingBar => { + if let GlobalState::MainView { pending_plugin_view_loading_bar, .. } = &mut self.global_state { + if let LoadingBarState::Pending = pending_plugin_view_loading_bar { + *pending_plugin_view_loading_bar = LoadingBarState::On; + } + } + Command::none() } } @@ -1417,7 +1446,7 @@ impl Application for AppModel { } } } - GlobalState::MainView { focused_search_result, sub_state, search_field_id, .. } => { + GlobalState::MainView { focused_search_result, sub_state, search_field_id, pending_plugin_view_loading_bar, .. } => { let input: Element<_> = text_input("Search...", &self.prompt) .on_input(AppMsg::PromptChanged) .on_submit(AppMsg::PromptSubmit) @@ -1450,7 +1479,7 @@ impl Application for AppModel { .width(Length::Fill) .themed(ContainerStyle::MainSearchBar); - let separator = if !self.loading_bar_state.is_empty() { + let separator = if matches!(pending_plugin_view_loading_bar, LoadingBarState::On) || !self.loading_bar_state.is_empty() { LoadingBar::new() .into() } else { diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index fde8f71..8f8d8db 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -26,6 +26,7 @@ pub enum GlobalState { client_context: Arc>, sub_state: MainViewState, pending_plugin_view_data: Option, + pending_plugin_view_loading_bar: LoadingBarState, }, ErrorView { error_view: ErrorViewData, @@ -70,6 +71,14 @@ pub enum ErrorViewData { }, } +#[derive(Debug, Clone)] +pub enum LoadingBarState { + Off, + Pending, + On +} + + impl GlobalState { pub fn new(search_field_id: text_input::Id, client_context: Arc>) -> GlobalState { GlobalState::MainView { @@ -78,6 +87,7 @@ impl GlobalState { sub_state: MainViewState::new(), pending_plugin_view_data: None, client_context, + pending_plugin_view_loading_bar: LoadingBarState::Off, } } @@ -198,7 +208,7 @@ impl Focus for GlobalState { fn secondary(&mut self, focus_list: &[SearchResult]) -> Command { match self { - GlobalState::MainView { focused_search_result, sub_state, client_context, .. } => { + GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { MainViewState::None => { if let Some(search_result) = focused_search_result.get(focus_list) { From 8c6f6b93e3d0ee926bf6895984439f3ee916011f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:01:16 +0100 Subject: [PATCH 166/540] Fix scenarios after react renderer parsing rework --- Cargo.lock | 1 + rust/client/src/ui/mod.rs | 31 ++++---- rust/common/src/model.rs | 10 +-- rust/common/src/rpc/frontend_api.rs | 4 + rust/common/src/scenario_convert.rs | 91 +---------------------- rust/common/src/scenario_model.rs | 51 ++++++------- rust/scenario_runner/src/frontend_mock.rs | 21 ++++-- rust/server/Cargo.toml | 1 + rust/server/src/model.rs | 2 + rust/server/src/plugins/js/mod.rs | 26 ++++++- rust/server/src/plugins/js/ui.rs | 7 +- 11 files changed, 97 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 72a6b03..93fe9d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8091,6 +8091,7 @@ dependencies = [ "resvg", "scenario_runner", "serde", + "serde-value", "sqlx", "tantivy", "tempfile", diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index be80798..ec4885a 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -21,13 +21,14 @@ use std::fs; use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex as StdMutex, Mutex, RwLock as StdRwLock}; +use serde::Deserialize; use tokio::sync::{Mutex as TokioMutex, RwLock as TokioRwLock}; use tonic::transport::Server; use client_context::ClientContext; -use common::model::{BackendRequestData, BackendResponseData, EntrypointId, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; +use common::model::{BackendRequestData, BackendResponseData, EntrypointId, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; use common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFrontendApiError}; -use common::scenario_convert::{ui_render_location_from_scenario, ui_widget_from_scenario}; +use common::scenario_convert::{ui_render_location_from_scenario}; use common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; use common_ui::physical_key_model; use utils::channel::{channel, RequestReceiver, RequestSender, Responder}; @@ -342,25 +343,25 @@ impl Application for AppModel { ); match event { - ScenarioFrontendEvent::ReplaceView { entrypoint_id, render_location, top_level_view, container } => { + ScenarioFrontendEvent::ReplaceView { entrypoint_id, render_location, top_level_view, container, images } => { let plugin_id = PluginId::from_string("__SCREENSHOT_GEN___"); let entrypoint_id = EntrypointId::from_string(entrypoint_id); let mut context = ClientContext::new(); let render_location = ui_render_location_from_scenario(render_location); - let ui_widget = ui_widget_from_scenario(container); - let has_children = ui_widget.widget_children.len() != 0; + let container = RootWidget::deserialize(container).expect("should always be valid"); + let has_children = container.content.is_some(); - // TODO - // context.replace_view( - // render_location, - // ui_widget, - // &plugin_id, - // "Screenshot Plugin", - // &entrypoint_id, - // "Screenshot Entrypoint", - // ); + context.replace_view( + render_location, + container, + images, + &plugin_id, + "Screenshot Plugin", + &entrypoint_id, + "Screenshot Entrypoint", + ); let context = Arc::new(StdRwLock::new(context)); @@ -2037,6 +2038,8 @@ async fn request_loop( render_location, top_level_view, container, + #[cfg(feature = "scenario_runner")] + container_value: _, images } => { let has_children = container.content.is_some(); diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 81b791b..9087d14 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -137,6 +137,8 @@ pub enum UiRequestData { render_location: UiRenderLocation, top_level_view: bool, container: RootWidget, + #[cfg(feature = "scenario_runner")] + container_value: serde_value::Value, images: HashMap, }, ShowPreferenceRequiredView { @@ -502,14 +504,6 @@ pub trait WidgetVisitor { } } -#[derive(Debug, Clone)] -pub struct UiWidget { - pub widget_id: UiWidgetId, - pub widget_type: String, - pub widget_properties: HashMap, - pub widget_children: Vec, -} - #[derive(Debug, Clone)] pub enum UiPropertyValue { String(String), diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index f80f857..6b17a4c 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -48,6 +48,8 @@ impl FrontendApi { render_location: UiRenderLocation, top_level_view: bool, container: RootWidget, + #[cfg(feature = "scenario_runner")] + container_value: serde_value::Value, images: HashMap, ) -> Result<(), FrontendApiError> { let request = UiRequestData::ReplaceView { @@ -58,6 +60,8 @@ impl FrontendApi { render_location, top_level_view, container, + #[cfg(feature = "scenario_runner")] + container_value, images, }; diff --git a/rust/common/src/scenario_convert.rs b/rust/common/src/scenario_convert.rs index 32450f5..bcbf0d1 100644 --- a/rust/common/src/scenario_convert.rs +++ b/rust/common/src/scenario_convert.rs @@ -1,6 +1,5 @@ -use std::collections::HashMap; -use crate::model::{UiPropertyValue, UiRenderLocation, UiWidget}; -use crate::scenario_model::{ScenarioUiPropertyValue, ScenarioUiRenderLocation, ScenarioUiWidget}; +use crate::model::UiRenderLocation; +use crate::scenario_model::ScenarioUiRenderLocation; pub fn ui_render_location_to_scenario(render_location: UiRenderLocation) -> ScenarioUiRenderLocation { match render_location { @@ -9,92 +8,6 @@ pub fn ui_render_location_to_scenario(render_location: UiRenderLocation) -> Scen } } -pub fn ui_widget_to_scenario(value: UiWidget) -> ScenarioUiWidget { - let children = value.widget_children.into_iter() - .map(|child| ui_widget_to_scenario(child)) - .collect::>(); - - ScenarioUiWidget { - widget_id: value.widget_id, - widget_type: value.widget_type, - widget_properties: ui_property_values_to_scenario(value.widget_properties), - widget_children: children - } -} - -fn ui_property_values_to_scenario(value: HashMap) -> HashMap { - value.into_iter() - .map(|(key, value)| (key, ui_property_value_to_scenario(value))) - .collect() -} - -fn ui_property_value_to_scenario(value: UiPropertyValue) -> ScenarioUiPropertyValue { - match value { - UiPropertyValue::String(value) => ScenarioUiPropertyValue::String(value), - UiPropertyValue::Number(value) => ScenarioUiPropertyValue::Number(value), - UiPropertyValue::Bool(value) => ScenarioUiPropertyValue::Bool(value), - UiPropertyValue::Bytes(value) => ScenarioUiPropertyValue::Bytes(value.to_vec()), - UiPropertyValue::Object(value) => { - let value: HashMap = value.into_iter() - .map(|(name, value)| (name, ui_property_value_to_scenario(value))) - .collect(); - - ScenarioUiPropertyValue::Object(value) - } - UiPropertyValue::Array(values) => { - let value: Vec<_> = values.into_iter() - .map(|value| ui_property_value_to_scenario(value)) - .collect(); - - ScenarioUiPropertyValue::Array(value) - } - UiPropertyValue::Undefined => ScenarioUiPropertyValue::Undefined, - } -} - -pub fn ui_widget_from_scenario(value: ScenarioUiWidget) -> UiWidget { - let children = value.widget_children.into_iter() - .map(|child| ui_widget_from_scenario(child)) - .collect::>(); - - UiWidget { - widget_id: value.widget_id, - widget_type: value.widget_type, - widget_properties: ui_property_values_from_scenario(value.widget_properties), - widget_children: children - } -} - -fn ui_property_values_from_scenario(value: HashMap) -> HashMap { - value.into_iter() - .map(|(key, value)| (key, ui_property_value_from_scenario(value))) - .collect() -} - -fn ui_property_value_from_scenario(value: ScenarioUiPropertyValue) -> UiPropertyValue { - match value { - ScenarioUiPropertyValue::String(value) => UiPropertyValue::String(value), - ScenarioUiPropertyValue::Number(value) => UiPropertyValue::Number(value), - ScenarioUiPropertyValue::Bool(value) => UiPropertyValue::Bool(value), - ScenarioUiPropertyValue::Bytes(value) => UiPropertyValue::Bytes(bytes::Bytes::from(value)), - ScenarioUiPropertyValue::Object(value) => { - let value: HashMap = value.into_iter() - .map(|(name, value)| (name, ui_property_value_from_scenario(value))) - .collect(); - - UiPropertyValue::Object(value) - } - ScenarioUiPropertyValue::Array(values) => { - let value: Vec<_> = values.into_iter() - .map(|value| ui_property_value_from_scenario(value)) - .collect(); - - UiPropertyValue::Array(value) - } - ScenarioUiPropertyValue::Undefined => UiPropertyValue::Undefined, - } -} - pub fn ui_render_location_from_scenario(render_location: ScenarioUiRenderLocation) -> UiRenderLocation { match render_location { ScenarioUiRenderLocation::InlineView => UiRenderLocation::InlineView, diff --git a/rust/common/src/scenario_model.rs b/rust/common/src/scenario_model.rs index a5adcf0..899a2ff 100644 --- a/rust/common/src/scenario_model.rs +++ b/rust/common/src/scenario_model.rs @@ -8,26 +8,6 @@ pub enum ScenarioUiRenderLocation { View } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ScenarioUiWidget { - pub widget_id: UiWidgetId, - pub widget_type: String, - pub widget_properties: HashMap, - pub widget_children: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ScenarioUiPropertyValue { - String(String), - Number(f64), - Bool(bool), - #[serde(with="base64")] - Bytes(Vec), - Object(HashMap), - Array(Vec), - Undefined, -} - #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type")] pub enum ScenarioFrontendEvent { @@ -35,7 +15,9 @@ pub enum ScenarioFrontendEvent { entrypoint_id: String, render_location: ScenarioUiRenderLocation, top_level_view: bool, - container: ScenarioUiWidget, + container: serde_value::Value, + #[serde(with="base64")] + images: HashMap, }, ShowPreferenceRequiredView { entrypoint_id: String, @@ -49,18 +31,31 @@ pub enum ScenarioFrontendEvent { } mod base64 { + use std::collections::HashMap; + use std::str::FromStr; use serde::{Serialize, Deserialize}; use serde::{Deserializer, Serializer}; use base64::Engine; + use base64::engine::general_purpose::STANDARD; + use crate::model::UiWidgetId; - pub fn serialize(v: &Vec, s: S) -> Result { - let base64 = base64::engine::general_purpose::STANDARD.encode(v); - String::serialize(&base64, s) + pub fn serialize(v: &HashMap, s: S) -> Result { + let map = v.iter() + .map(|(key, value)| (key.to_string(), STANDARD.encode(value))) + .collect(); + + HashMap::::serialize(&map, s) } - pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { - let base64 = String::deserialize(d)?; - base64::engine::general_purpose::STANDARD.decode(base64.as_bytes()) - .map_err(|e| serde::de::Error::custom(e)) + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + HashMap::::deserialize(d)? + .into_iter() + .map(|(key, value)| { + STANDARD.decode(value.as_bytes()) + .map_err(|e| serde::de::Error::custom(e)) + .map(|vec| (UiWidgetId::from_str(&key).expect("should not fail"), bytes::Bytes::from(vec))) + }) + .collect() + } } \ No newline at end of file diff --git a/rust/scenario_runner/src/frontend_mock.rs b/rust/scenario_runner/src/frontend_mock.rs index 585edcb..8eca567 100644 --- a/rust/scenario_runner/src/frontend_mock.rs +++ b/rust/scenario_runner/src/frontend_mock.rs @@ -4,7 +4,7 @@ use std::path::Path; use common::model::{BackendRequestData, BackendResponseData, EntrypointId, PluginId, UiRequestData, UiResponseData}; use common::rpc::backend_api::{BackendApi, BackendForFrontendApi}; use common::rpc::backend_server::wait_for_backend_server; -use common::scenario_convert::{ui_render_location_to_scenario, ui_widget_to_scenario}; +use common::scenario_convert::{ui_render_location_to_scenario}; use common::scenario_model::ScenarioFrontendEvent; use utils::channel::{RequestReceiver, RequestSender}; @@ -151,18 +151,29 @@ async fn request_loop(mut request_receiver: RequestReceiver { + UiRequestData::UpdateLoadingBar { .. } | UiRequestData::ShowHud { .. } | UiRequestData::ShowWindow | UiRequestData::ClearInlineView { .. } => { unreachable!() } - UiRequestData::RequestSearchResultUpdate => { + UiRequestData::SetGlobalShortcut { .. } | UiRequestData::RequestSearchResultUpdate => { // noop } - UiRequestData::ReplaceView { plugin_id: _, plugin_name: _, entrypoint_id, entrypoint_name: _, render_location, top_level_view, container } => { + UiRequestData::ReplaceView { + plugin_id: _, + plugin_name: _, + entrypoint_id, + entrypoint_name: _, + render_location, + top_level_view, + container: _, + container_value, + images + } => { let event = ScenarioFrontendEvent::ReplaceView { entrypoint_id: entrypoint_id.to_string(), render_location: ui_render_location_to_scenario(render_location), top_level_view, - container: ui_widget_to_scenario(container), + container: container_value, + images, }; scenario_sender.send(event) diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index eee1cad..95d44f3 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -4,6 +4,7 @@ edition = "2021" [dependencies] serde = { version = "1.0", features = ["derive"] } +serde-value = "0.7.0" deno_core = { version = "0.204.0" } deno_runtime = { version = "0.126.0" } tokio = "1.28.1" diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 51163e9..6f24a21 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -15,6 +15,8 @@ pub enum JsUiRequestData { render_location: JsUiRenderLocation, top_level_view: bool, container: RootWidget, + #[cfg(feature = "scenario_runner")] + container_value: serde_value::Value, images: HashMap, }, ClearInlineView, diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index e5be78a..173e168 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -28,7 +28,7 @@ use tokio::net::TcpStream; use tokio_util::sync::CancellationToken; use common::dirs::Dirs; -use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidget, UiWidgetId}; +use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; use common::rpc::frontend_api::FrontendApi; use component_model::{create_component_model, Children, Component, Property, PropertyType, SharedType}; @@ -637,13 +637,33 @@ fn make_request(state: &Rc>, data: JsUiRequestData) -> anyhow:: async fn make_request_async(plugin_id: PluginId, plugin_name: String, frontend_api: &mut FrontendApi, data: JsUiRequestData) -> anyhow::Result { match data { - JsUiRequestData::ReplaceView { render_location, top_level_view, container, entrypoint_id, entrypoint_name, images } => { + JsUiRequestData::ReplaceView { + entrypoint_id, + entrypoint_name, + render_location, + top_level_view, + container, + #[cfg(feature = "scenario_runner")] + container_value, + images + } => { let render_location = match render_location { // TODO into? JsUiRenderLocation::InlineView => UiRenderLocation::InlineView, JsUiRenderLocation::View => UiRenderLocation::View, }; - frontend_api.replace_view(plugin_id, plugin_name, entrypoint_id, entrypoint_name, render_location, top_level_view, container, images).await?; + frontend_api.replace_view( + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + render_location, + top_level_view, + container, + #[cfg(feature = "scenario_runner")] + container_value, + images + ).await?; Ok(JsUiResponseData::Nothing) } diff --git a/rust/server/src/plugins/js/ui.rs b/rust/server/src/plugins/js/ui.rs index 46c4951..0dc54df 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/server/src/plugins/js/ui.rs @@ -9,7 +9,7 @@ use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; use indexmap::IndexMap; use serde::{de, Deserialize, Deserializer}; use serde::de::Error; -use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, EntrypointId, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Image, ImageSource, ImageSourceAsset, ImageSourceUrl, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectItemWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiPropertyValue, UiWidget, UiWidgetId, WidgetVisitor}; +use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, EntrypointId, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Image, ImageSource, ImageSourceAsset, ImageSourceUrl, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectItemWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiPropertyValue, UiWidgetId, WidgetVisitor}; use component_model::{Component, Property, PropertyType, SharedType}; use component_model::Component::Root; use crate::model::{JsUiRenderLocation, JsUiRequestData, JsUiResponseData}; @@ -97,6 +97,9 @@ fn op_react_replace_view<'a>( }; let mut deserializer = serde_v8::Deserializer::new(scope, container.v8_value, None); + + #[cfg(feature = "scenario_runner")] + let container_value = serde_value::Value::deserialize(&mut deserializer)?; let container = RootWidget::deserialize(&mut deserializer)?; let images = ImageGatherer::run_gatherer(state.clone(), &container)?; @@ -107,6 +110,8 @@ fn op_react_replace_view<'a>( render_location, top_level_view, container, + #[cfg(feature = "scenario_runner")] + container_value, images, }; From c9dd9173bba577374b96218d9e381ca6515db3fe Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:34:48 +0100 Subject: [PATCH 167/540] Implement visual part of keyboard navigation for list and grid views --- rust/client/src/ui/inline_view_container.rs | 72 ------------- rust/client/src/ui/mod.rs | 60 ++++++----- rust/client/src/ui/state/mod.rs | 5 +- rust/client/src/ui/theme/button.rs | 4 +- rust/client/src/ui/theme/mod.rs | 6 +- rust/client/src/ui/view_container.rs | 75 -------------- rust/client/src/ui/widget.rs | 106 ++++++++++++++------ rust/client/src/ui/widget_container.rs | 4 +- 8 files changed, 124 insertions(+), 208 deletions(-) delete mode 100644 rust/client/src/ui/inline_view_container.rs delete mode 100644 rust/client/src/ui/view_container.rs diff --git a/rust/client/src/ui/inline_view_container.rs b/rust/client/src/ui/inline_view_container.rs deleted file mode 100644 index 1721851..0000000 --- a/rust/client/src/ui/inline_view_container.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::sync::{Arc, RwLock}; - -use iced::widget::component; -use iced::widget::{horizontal_space, Component}; - -use common::model::UiRenderLocation; - -use crate::ui::client_context::ClientContext; -use crate::ui::theme::{Element, GauntletTheme}; -use crate::ui::widget::{ActionPanel, ComponentWidgetEvent}; -use crate::ui::AppMsg; - -pub struct InlineViewContainer { - client_context: Arc>, -} - -pub fn inline_view_container(client_context: Arc>) -> InlineViewContainer { - InlineViewContainer { - client_context, - } -} - -impl Component for InlineViewContainer { - type State = (); - type Event = ComponentWidgetEvent; - - fn update( - &mut self, - _state: &mut Self::State, - event: Self::Event, - ) -> Option { - let client_context = self.client_context.read().expect("lock is poisoned"); - let containers = client_context.get_all_inline_view_containers(); - - match containers.first() { - Some((plugin_id, _)) => Some(AppMsg::WidgetEvent { - plugin_id: plugin_id.clone(), - render_location: UiRenderLocation::InlineView, - widget_event: event, - }), - None => None, - } - } - - fn view(&self, _state: &Self::State) -> Element { - let client_context = self.client_context.read().expect("lock is poisoned"); - let containers = client_context.get_all_inline_view_containers(); - - match containers.first() { - Some((_, container)) => { - container.render_inline_root_widget() - } - None => { - horizontal_space() - .into() - } - } - } -} - -impl<'a> From for Element<'a, AppMsg> { - fn from(container: InlineViewContainer) -> Self { - component(container) - } -} - -pub fn inline_view_action_panel(client_context: Arc>) -> Option { - let client_context = client_context.read().expect("lock is poisoned"); - - client_context.get_first_inline_view_action_panel() -} - diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index ec4885a..4a16b58 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -34,21 +34,17 @@ use common_ui::physical_key_model; use utils::channel::{channel, RequestReceiver, RequestSender, Responder}; use crate::model::UiViewEvent; -use crate::ui::inline_view_container::{inline_view_action_panel, inline_view_container}; use crate::ui::search_list::search_list; use crate::ui::theme::container::{ContainerStyle, ContainerStyleInner}; use crate::ui::theme::text_input::TextInputStyle; use crate::ui::theme::{Element, ThemableWidget}; -use crate::ui::view_container::view_container; use crate::ui::widget::{render_root, ActionPanel, ActionPanelItem, ComponentWidgetEvent}; -mod view_container; mod search_list; mod widget; mod theme; mod client_context; mod widget_container; -mod inline_view_container; #[cfg(any(target_os = "macos", target_os = "windows"))] mod sys_tray; mod custom_widgets; @@ -1488,8 +1484,28 @@ impl Application for AppModel { .into() }; + let client_context = self.client_context.read().expect("lock is poisoned"); + + let inline_view = match client_context.get_all_inline_view_containers().first() { + Some((plugin_id, container)) => { + let plugin_id = plugin_id.clone(); + container.render_inline_root_widget() + .map(move |widget_event| { + AppMsg::WidgetEvent { + plugin_id: plugin_id.clone(), + render_location: UiRenderLocation::InlineView, + widget_event, + } + }) + } + None => { + horizontal_space() + .into() + } + }; + let content: Element<_> = column(vec![ - inline_view_container(self.client_context.clone()).into(), + inline_view, list, ]).into(); @@ -1553,7 +1569,9 @@ impl Application for AppModel { (Some((label, primary_action_widget_id, default_shortcut)), Some(action_panel)) } } else { - match inline_view_action_panel(self.client_context.clone()) { + let client_context = self.client_context.read().expect("lock is poisoned"); + + match client_context.get_first_inline_view_action_panel() { None => (None, None), Some(action_panel) => { match action_panel.find_first() { @@ -1632,25 +1650,19 @@ impl Application for AppModel { root } - GlobalState::PluginView { plugin_view_data, sub_state, .. } => { - let PluginViewData { - top_level_view: _, - plugin_id, - plugin_name, - entrypoint_id, - entrypoint_name, - action_shortcuts, - } = plugin_view_data; + GlobalState::PluginView { plugin_view_data, sub_state, focused_item, .. } => { + let PluginViewData { plugin_id, action_shortcuts, .. } = plugin_view_data; - let container_element: Element<_> = view_container( - self.client_context.clone(), - sub_state.clone(), - plugin_id.to_owned(), - plugin_name.to_owned(), - entrypoint_id.to_owned(), - entrypoint_name.to_owned(), - action_shortcuts.to_owned(), - ).into(); + let client_context = self.client_context.read().expect("lock is poisoned"); + let view_container = client_context.get_view_container(); + + let container_element = view_container + .render_root_widget(sub_state, action_shortcuts, focused_item) + .map(|widget_event| AppMsg::WidgetEvent { + plugin_id: plugin_id.clone(), + render_location: UiRenderLocation::View, + widget_event, + }); let element: Element<_> = container(container_element) .width(Length::Fill) diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 8f8d8db..61cc87e 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -2,7 +2,6 @@ mod main_view; mod plugin_view; use crate::ui::client_context::ClientContext; -use crate::ui::inline_view_container::inline_view_action_panel; use crate::ui::scroll_handle::ScrollHandle; pub use crate::ui::state::main_view::MainViewState; pub use crate::ui::state::plugin_view::PluginViewState; @@ -373,7 +372,9 @@ impl Focus for GlobalState { } } MainViewState::InlineViewActionPanel { focused_action_item } => { - match inline_view_action_panel(client_context.clone()) { + let client_context = client_context.read().expect("lock is poisoned"); + + match client_context.get_first_inline_view_action_panel() { Some(action_panel) => { if action_panel.action_count() != 0 { focused_action_item.focus_next_row(action_panel.action_count(), 1) diff --git a/rust/client/src/ui/theme/button.rs b/rust/client/src/ui/theme/button.rs index 8ca3168..f35c17a 100644 --- a/rust/client/src/ui/theme/button.rs +++ b/rust/client/src/ui/theme/button.rs @@ -103,7 +103,7 @@ impl ButtonStyle { } ButtonStyle::GridItemFocused => { let theme = &theme.grid_item; - (Some(&theme.background_color_hovered), Some(&theme.background_color_hovered), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color_focused), Some(&theme.background_color_focused), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::Action => { let theme = &theme.action; @@ -119,7 +119,7 @@ impl ButtonStyle { } ButtonStyle::ListItemFocused => { let theme = &theme.list_item; - (Some(&theme.background_color_hovered), Some(&theme.background_color_hovered), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color_focused), Some(&theme.background_color_focused), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::MainListItem => { let theme = &theme.main_list_item; diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 1a7c791..35132ee 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -260,7 +260,7 @@ impl GauntletTheme { action: ThemeButton { padding: padding_all(8.0), background_color: TRANSPARENT, - background_color_focused: background_lighter_color, + background_color_focused: background_lightest_color, background_color_hovered: background_lighter_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, @@ -351,7 +351,7 @@ impl GauntletTheme { grid_item: ThemeButton { padding: padding_all(8.0), background_color: background_lighter_color, - background_color_focused: background_lightest_color, + background_color_focused: background_darker_color, background_color_hovered: background_lightest_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, @@ -422,7 +422,7 @@ impl GauntletTheme { padding: padding_all(5.0), background_color: TRANSPARENT, background_color_focused: background_lighter_color, - background_color_hovered: background_lighter_color, + background_color_hovered: background_darker_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, border_radius: BUTTON_BORDER_RADIUS, diff --git a/rust/client/src/ui/view_container.rs b/rust/client/src/ui/view_container.rs deleted file mode 100644 index b1a3504..0000000 --- a/rust/client/src/ui/view_container.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; - -use iced::widget::component; -use iced::widget::Component; - -use common::model::{EntrypointId, PhysicalShortcut, PluginId, UiRenderLocation}; - -use crate::ui::client_context::ClientContext; -use crate::ui::state::PluginViewState; -use crate::ui::theme::{Element, GauntletTheme}; -use crate::ui::widget::ComponentWidgetEvent; -use crate::ui::AppMsg; - -pub struct ViewContainer { - client_context: Arc>, - plugin_view_state: PluginViewState, - plugin_id: PluginId, - plugin_name: String, - entrypoint_id: EntrypointId, - entrypoint_name: String, - action_shortcuts: HashMap, -} - -pub fn view_container( - client_context: Arc>, - plugin_view_state: PluginViewState, - plugin_id: PluginId, - plugin_name: String, - entrypoint_id: EntrypointId, - entrypoint_name: String, - action_shortcuts: HashMap -) -> ViewContainer { - ViewContainer { - client_context, - plugin_view_state, - plugin_id, - plugin_name, - entrypoint_id, - entrypoint_name, - action_shortcuts - } -} - -impl Component for ViewContainer { - type State = (); - type Event = ComponentWidgetEvent; - - fn update( - &mut self, - _state: &mut Self::State, - event: Self::Event, - ) -> Option { - Some(AppMsg::WidgetEvent { - plugin_id: self.plugin_id.clone(), - render_location: UiRenderLocation::View, - widget_event: event, - }) - } - - fn view(&self, _state: &Self::State) -> Element { - let client_context = self.client_context.read().expect("lock is poisoned"); - let view_container = client_context.get_view_container(); - view_container.render_root_widget( - &self.plugin_view_state, - &self.action_shortcuts, - ) - } -} - -impl<'a> From for Element<'a, AppMsg> { - fn from(container: ViewContainer) -> Self { - component(container) - } -} \ No newline at end of file diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 5865395..199a9ff 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -455,6 +455,7 @@ impl<'b> ComponentWidgets<'b> { plugin_view_state: &PluginViewState, entrypoint_name: Option<&String>, action_shortcuts: &HashMap, + focused_item: &ScrollHandle, ) -> Element<'a, ComponentWidgetEvent> { match &self.root_widget { None => { @@ -489,8 +490,8 @@ impl<'b> ComponentWidgets<'b> { ) }, RootWidgetMembers::Form(widget) => self.render_form_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts), - RootWidgetMembers::List(widget) => self.render_list_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts), - RootWidgetMembers::Grid(widget) => self.render_grid_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts), + RootWidgetMembers::List(widget) => self.render_list_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts, focused_item), + RootWidgetMembers::Grid(widget) => self.render_grid_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts, focused_item), _ => { panic!("used inline widget in non-inline place") } @@ -1187,12 +1188,14 @@ impl<'b> ComponentWidgets<'b> { plugin_view_state: &PluginViewState, entrypoint_name: &str, action_shortcuts: &HashMap, + focused_item: &ScrollHandle, ) -> Element<'a, ComponentWidgetEvent> { let widget_id = list_widget.__id__; let RootState { show_action_panel } = self.root_state(widget_id); let mut pending: Vec<&ListItemWidget> = vec![]; let mut items: Vec> = vec![]; + let index_counter = &Cell::new(0); for members in &list_widget.content.ordered_members { match &members { @@ -1203,7 +1206,7 @@ impl<'b> ComponentWidgets<'b> { if !pending.is_empty() { let content: Vec<_> = pending .iter() - .map(|widget| self.render_list_item_widget(widget)) + .map(|widget| self.render_list_item_widget(widget, focused_item.index, index_counter)) .collect(); let content: Element<_> = column(content) @@ -1214,7 +1217,7 @@ impl<'b> ComponentWidgets<'b> { pending = vec![]; } - items.push(self.render_list_section_widget(widget)) + items.push(self.render_list_section_widget(widget, focused_item.index, index_counter)) }, } } @@ -1222,7 +1225,7 @@ impl<'b> ComponentWidgets<'b> { if !pending.is_empty() { let content: Vec<_> = pending .iter() - .map(|widget| self.render_list_item_widget(widget)) + .map(|widget| self.render_list_item_widget(widget, focused_item.index, index_counter)) .collect(); let content: Element<_> = column(content) @@ -1246,6 +1249,7 @@ impl<'b> ComponentWidgets<'b> { .themed(ContainerStyle::ListInner); let content: Element<_> = scrollable(content) + .id(focused_item.scrollable_id.clone()) .width(Length::Fill) .into(); @@ -1290,12 +1294,17 @@ impl<'b> ComponentWidgets<'b> { ) } - fn render_list_section_widget<'a>(&self, widget: &ListSectionWidget) -> Element<'a, ComponentWidgetEvent> { + fn render_list_section_widget<'a>( + &self, + widget: &ListSectionWidget, + item_focus_index: Option, + index_counter: &Cell + ) -> Element<'a, ComponentWidgetEvent> { let content: Vec<_> = widget.content.ordered_members .iter() .map(|members| { match members { - ListSectionWidgetOrderedMembers::ListItem(widget) => self.render_list_item_widget(widget) + ListSectionWidgetOrderedMembers::ListItem(widget) => self.render_list_item_widget(widget, item_focus_index, index_counter) } }) .collect(); @@ -1306,7 +1315,12 @@ impl<'b> ComponentWidgets<'b> { render_section(content, Some(&widget.title), &widget.subtitle, RowStyle::ListSectionTitle, TextStyle::ListSectionTitle, TextStyle::ListSectionSubtitle) } - fn render_list_item_widget<'a>(&self, widget: &ListItemWidget) -> Element<'a, ComponentWidgetEvent> { + fn render_list_item_widget<'a>( + &self, + widget: &ListItemWidget, + item_focus_index: Option, + index_counter: &Cell + ) -> Element<'a, ComponentWidgetEvent> { let icon: Option> = widget.icon .as_ref() .map(|icon| self.render_image(widget.__id__, icon, None)); @@ -1361,12 +1375,19 @@ impl<'b> ComponentWidgets<'b> { .align_items(Alignment::Center) .into(); - let style = if false { - ButtonStyle::ListItemFocused - } else { - ButtonStyle::ListItem + let style = match item_focus_index { + None => ButtonStyle::ListItem, + Some(focused_index) => { + if focused_index == index_counter.get() { + ButtonStyle::ListItemFocused + } else { + ButtonStyle::ListItem + } + } }; + index_counter.set(index_counter.get() + 1); + button(content) .on_press(ComponentWidgetEvent::ListItemClick { widget_id: widget.__id__ }) .width(Length::Fill) @@ -1379,11 +1400,13 @@ impl<'b> ComponentWidgets<'b> { plugin_view_state: &PluginViewState, entrypoint_name: &str, action_shortcuts: &HashMap, + focused_item: &ScrollHandle, ) -> Element<'a, ComponentWidgetEvent> { let RootState { show_action_panel } = self.root_state(grid_widget.__id__); let mut pending: Vec<&GridItemWidget> = vec![]; let mut items: Vec> = vec![]; + let index_counter = &Cell::new(0); for members in &grid_widget.content.ordered_members { match &members { @@ -1392,20 +1415,20 @@ impl<'b> ComponentWidgets<'b> { } GridWidgetOrderedMembers::GridSection(widget) => { if !pending.is_empty() { - let content = self.render_grid(&pending, &grid_widget.columns); + let content = self.render_grid(&pending, &grid_widget.columns, focused_item.index, index_counter); items.push(content); pending = vec![]; } - items.push(self.render_grid_section_widget(widget)) + items.push(self.render_grid_section_widget(widget, focused_item.index, index_counter)) } } } if !pending.is_empty() { - let content = self.render_grid(&pending, &grid_widget.columns); + let content = self.render_grid(&pending, &grid_widget.columns, focused_item.index, index_counter); items.push(content); } @@ -1418,6 +1441,7 @@ impl<'b> ComponentWidgets<'b> { .themed(ContainerStyle::GridInner); let content: Element<_> = scrollable(content) + .id(focused_item.scrollable_id.clone()) .width(Length::Fill) .into(); @@ -1438,7 +1462,12 @@ impl<'b> ComponentWidgets<'b> { ) } - fn render_grid_section_widget<'a>(&self, widget: &GridSectionWidget) -> Element<'a, ComponentWidgetEvent> { + fn render_grid_section_widget<'a>( + &self, + widget: &GridSectionWidget, + item_focus_index: Option, + index_counter: &Cell + ) -> Element<'a, ComponentWidgetEvent> { let items: Vec<_> = widget.content.ordered_members .iter() .map(|members| { @@ -1448,23 +1477,35 @@ impl<'b> ComponentWidgets<'b> { }) .collect(); - let content = self.render_grid(&items, &widget.columns); + let content = self.render_grid(&items, &widget.columns, item_focus_index, index_counter); render_section(content, Some(&widget.title), &widget.subtitle, RowStyle::GridSectionTitle, TextStyle::GridSectionTitle, TextStyle::GridSectionSubtitle) } - fn render_grid_item_widget<'a>(&self, widget: &GridItemWidget) -> Element<'a, ComponentWidgetEvent> { + fn render_grid_item_widget<'a>( + &self, + widget: &GridItemWidget, + item_focus_index: Option, + index_counter: &Cell + ) -> Element<'a, ComponentWidgetEvent> { // TODO not needed column element? let content: Element<_> = column(vec![self.render_content_widget(&widget.content.content, true)]) .height(130) // TODO dynamic height .into(); - let style = if false { - ButtonStyle::GridItemFocused - } else { - ButtonStyle::GridItem + let style = match item_focus_index { + None => ButtonStyle::GridItem, + Some(focused_index) => { + if focused_index == index_counter.get() { + ButtonStyle::GridItemFocused + } else { + ButtonStyle::GridItem + } + } }; + index_counter.set(index_counter.get() + 1); + let content: Element<_> = button(content) .on_press(ComponentWidgetEvent::GridItemClick { widget_id: widget.__id__ }) .width(Length::Fill) @@ -1512,7 +1553,14 @@ impl<'b> ComponentWidgets<'b> { content } - fn render_grid<'a>(&self, items: &[&GridItemWidget], /*aspect_ratio: Option<&str>,*/ columns: &Option) -> Element<'a, ComponentWidgetEvent> { + fn render_grid<'a>( + &self, + items: &[&GridItemWidget], + /*aspect_ratio: Option<&str>,*/ + columns: &Option, + item_focus_index: Option, + index_counter: &Cell + ) -> Element<'a, ComponentWidgetEvent> { // let (width, height) = match aspect_ratio { // None => (1, 1), // Some("1") => (1, 1), @@ -1529,7 +1577,7 @@ impl<'b> ComponentWidgets<'b> { let rows: Vec> = items .iter() - .map(|widget| self.render_grid_item_widget(widget)) + .map(|widget| self.render_grid_item_widget(widget, item_focus_index, index_counter)) .chunks(grid_width) .into_iter() .map(|row_items| { @@ -1858,10 +1906,10 @@ fn convert_action_panel(action_panel: &Option, action_shortcu } } -fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( +fn render_action_panel_items<'a, T: 'a + Clone>( title: Option, items: Vec, - action_panel_scroll_handle: &ScrollHandle, + action_panel_focus_index: Option, on_action_click: &dyn Fn(UiWidgetId) -> T, index_counter: &Cell ) -> Vec> { @@ -1934,7 +1982,7 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( .into() }; - let style = match action_panel_scroll_handle.index { + let style = match action_panel_focus_index { None => ButtonStyle::Action, Some(focused_index) => { if focused_index == index_counter.get() { @@ -1960,7 +2008,7 @@ fn render_action_panel_items<'a, T: 'a + Clone, ACTION>( columns.push(separator); - let content = render_action_panel_items(title, items, action_panel_scroll_handle, on_action_click, index_counter); + let content = render_action_panel_items(title, items, action_panel_focus_index, on_action_click, index_counter); for content in content { columns.push(content); @@ -1979,7 +2027,7 @@ fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T, ACTION>( on_action_click: F, action_panel_scroll_handle: &ScrollHandle, ) -> Element<'a, T> { - let columns = render_action_panel_items(action_panel.title, action_panel.items, action_panel_scroll_handle, &on_action_click, &Cell::new(0)); + let columns = render_action_panel_items(action_panel.title, action_panel.items, action_panel_scroll_handle.index, &on_action_click, &Cell::new(0)); let actions: Element<_> = column(columns) .into(); diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index f3714d7..ea5d9ed 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -8,6 +8,7 @@ use std::collections::HashMap; use std::mem; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use crate::ui::scroll_handle::ScrollHandle; pub struct PluginWidgetContainer { root_widget: Arc>>, @@ -88,12 +89,13 @@ impl PluginWidgetContainer { &self, plugin_view_state: &PluginViewState, action_shortcuts: &HashMap, + focused_item: &ScrollHandle, ) -> Element<'a, ComponentWidgetEvent> { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); ComponentWidgets::new(&mut root_widget, &mut state, &self.images) - .render_root_widget(plugin_view_state, self.entrypoint_name.as_ref(), action_shortcuts) + .render_root_widget(plugin_view_state, self.entrypoint_name.as_ref(), action_shortcuts, focused_item) } pub fn render_inline_root_widget<'a>(&self) -> Element<'a, ComponentWidgetEvent> { From 504adac487454539ed6be4bb3a5ca6ac085269c0 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 12 Nov 2024 23:43:18 +0100 Subject: [PATCH 168/540] Fix keyboard navigation in grid not working properly in situation when going from one section to another with non-full rows --- rust/client/src/ui/client_context.rs | 23 +- rust/client/src/ui/mod.rs | 4 +- rust/client/src/ui/scroll_handle.rs | 85 +++--- rust/client/src/ui/state/main_view.rs | 6 +- rust/client/src/ui/state/mod.rs | 66 ++--- rust/client/src/ui/state/plugin_view.rs | 4 +- rust/client/src/ui/widget.rs | 345 +++++++++++++++++++++--- rust/client/src/ui/widget_container.rs | 24 +- 8 files changed, 400 insertions(+), 157 deletions(-) diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index c780020..a517963 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -1,9 +1,10 @@ -use std::collections::HashMap; -use common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiWidgetId}; use crate::model::UiViewEvent; - use crate::ui::widget::{ActionPanel, ComponentWidgetEvent}; use crate::ui::widget_container::PluginWidgetContainer; +use crate::ui::AppMsg; +use common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiWidgetId}; +use iced::Command; +use std::collections::HashMap; pub struct ClientContext { inline_views: Vec<(PluginId, PluginWidgetContainer)>, // Vec to have stable ordering @@ -119,15 +120,19 @@ impl ClientContext { self.view.get_action_ids() } - pub fn keyboard_navigation_width(&self) -> Option { - self.view.keyboard_navigation_width() + pub fn focus_up(&self) -> Command { + self.view.focus_up() } - pub fn keyboard_navigation_total(&self) -> usize { - self.view.keyboard_navigation_total() + pub fn focus_down(&self) -> Command { + self.view.focus_down() } - pub fn has_search_bar(&self) -> bool { - self.view.has_search_bar() + pub fn focus_left(&self) -> Command { + self.view.focus_left() + } + + pub fn focus_right(&self) -> Command { + self.view.focus_right() } } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 4a16b58..d63f08f 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -1650,14 +1650,14 @@ impl Application for AppModel { root } - GlobalState::PluginView { plugin_view_data, sub_state, focused_item, .. } => { + GlobalState::PluginView { plugin_view_data, sub_state, .. } => { let PluginViewData { plugin_id, action_shortcuts, .. } = plugin_view_data; let client_context = self.client_context.read().expect("lock is poisoned"); let view_container = client_context.get_view_container(); let container_element = view_container - .render_root_widget(sub_state, action_shortcuts, focused_item) + .render_root_widget(sub_state, action_shortcuts) .map(|widget_event| AppMsg::WidgetEvent { plugin_id: plugin_id.clone(), render_location: UiRenderLocation::View, diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs index 3301186..9e6185e 100644 --- a/rust/client/src/ui/scroll_handle.rs +++ b/rust/client/src/ui/scroll_handle.rs @@ -4,9 +4,9 @@ use iced::widget::scrollable; use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; use crate::ui::AppMsg; -// TODO this size of item for main view list, incorrect for actions, -// but amount of actions is usually small so it is not that noticeable -const ESTIMATED_ITEM_SIZE: f32 = 38.8; +pub const ESTIMATED_MAIN_LIST_ITEM_HEIGHT: f32 = 38.8; +pub const ESTIMATED_ACTION_ITEM_HEIGHT: f32 = 38.8; // TODO +pub const ESTIMATED_GRID_ITEM_HEIGHT: f32 = 38.0; // TODO #[derive(Clone, Debug)] pub struct ScrollHandle { @@ -14,15 +14,17 @@ pub struct ScrollHandle { pub scrollable_id: scrollable::Id, pub index: Option, offset: usize, + item_height: f32, } impl ScrollHandle { - pub fn new(first_focused: bool) -> ScrollHandle { + pub fn new(first_focused: bool, item_height: f32) -> ScrollHandle { ScrollHandle { phantom: PhantomData, scrollable_id: scrollable::Id::unique(), index: if first_focused { Some(0) } else { None }, offset: 0, + item_height, } } @@ -42,7 +44,11 @@ impl ScrollHandle { } } - pub fn focus_next_row(&mut self, total_items: usize, total_columns: usize) -> Command { + pub fn focus_next(&mut self, total_item_amount: usize) -> Option> { + self.focus_next_in(total_item_amount, 1) + } + + pub fn focus_next_in(&mut self, total_item_amount: usize, amount: usize) -> Option> { self.offset = if self.offset < 7 { self.offset + 1 } else { @@ -52,31 +58,35 @@ impl ScrollHandle { match self.index.as_mut() { None => { // focus first - if total_items > 0 { + if total_item_amount > 0 { self.index = Some(0); - self.scroll_to(0) + Some(self.scroll_to(0)) } else { - Command::none() + None } } Some(index) => { - // focus next row only if there is an item on it - let new_index = *index + total_columns; - if new_index < total_items { + // focus next if there is an item + let new_index = *index + amount; + if new_index < total_item_amount { *index = new_index; let index = *index; - self.scroll_to(index) + Some(self.scroll_to(index)) } else { - Command::none() + None } } } } - pub fn focus_previous_row(&mut self, total_columns: usize) -> Command { + pub fn focus_previous(&mut self) -> Option> { + self.focus_previous_in(1) + } + + pub fn focus_previous_in(&mut self, amount: usize) -> Option> { self.offset = if self.offset > 1 { self.offset - 1 } else { @@ -84,59 +94,24 @@ impl ScrollHandle { }; match self.index.as_mut() { - None => Command::none(), + None => None, Some(index) => { - match index.checked_sub(total_columns) { // basically a check if result is >= 0 + match index.checked_sub(amount) { // basically a check if result is >= 0 Some(new_index) => { *index = new_index; let index = *index; - self.scroll_to(index) + Some(self.scroll_to(index)) } - None => Command::none() + None => None } } } } - pub fn focus_next_column(&mut self, total_items: usize, total_columns: usize) -> Command { - match self.index.as_mut() { - None => Command::none(), - Some(index) => { - // put focus on next column only if it doesn't put it on next row - // and if there is an item there - let new_index = *index + 1; - if *index % total_columns != 0 && new_index <= total_items { - *index = new_index; - } - - Command::none() - } - } - } - - pub fn focus_previous_column(&mut self, total_columns: usize) -> Command { - match self.index.as_mut() { - None => Command::none(), - Some(index) => { - // put focus on previous column only if it doesn't put it on previous row - if *index == 0 { - Command::none() - } else { - let new_index = *index - 1; - if new_index % total_columns != 0 { - *index = new_index; - } - - Command::none() - } - } - } - } - - fn scroll_to(&self, index: usize) -> Command { - let pos_y = index as f32 * ESTIMATED_ITEM_SIZE - (self.offset as f32 * ESTIMATED_ITEM_SIZE); + pub fn scroll_to(&self, row_index: usize) -> Command { + let pos_y = row_index as f32 * self.item_height - (self.offset as f32 * self.item_height); scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) } diff --git a/rust/client/src/ui/state/main_view.rs b/rust/client/src/ui/state/main_view.rs index c72e080..ddd85c7 100644 --- a/rust/client/src/ui/state/main_view.rs +++ b/rust/client/src/ui/state/main_view.rs @@ -1,4 +1,4 @@ -use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_ACTION_ITEM_HEIGHT}; use common::model::{SearchResultEntrypointAction, UiWidgetId}; pub enum MainViewState { @@ -24,13 +24,13 @@ impl MainViewState { pub fn search_result_action_panel(prev_state: &mut MainViewState, focus_first: bool) { *prev_state = Self::SearchResultActionPanel { - focused_action_item: ScrollHandle::new(focus_first), + focused_action_item: ScrollHandle::new(focus_first, ESTIMATED_ACTION_ITEM_HEIGHT), } } pub fn inline_result_action_panel(prev_state: &mut MainViewState, focus_first: bool) { *prev_state = Self::InlineViewActionPanel { - focused_action_item: ScrollHandle::new(focus_first), + focused_action_item: ScrollHandle::new(focus_first, ESTIMATED_ACTION_ITEM_HEIGHT), } } } diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 61cc87e..34c7f5b 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -2,11 +2,11 @@ mod main_view; mod plugin_view; use crate::ui::client_context::ClientContext; -use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_MAIN_LIST_ITEM_HEIGHT}; pub use crate::ui::state::main_view::MainViewState; pub use crate::ui::state::plugin_view::PluginViewState; use crate::ui::AppMsg; -use common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult, UiWidgetId}; +use common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult}; use iced::widget::text_input; use iced::widget::text_input::focus; use iced::Command; @@ -34,9 +34,6 @@ pub enum GlobalState { // logic client_context: Arc>, - // ephemeral state - focused_item: ScrollHandle, - // state plugin_view_data: PluginViewData, sub_state: PluginViewState, @@ -82,7 +79,7 @@ impl GlobalState { pub fn new(search_field_id: text_input::Id, client_context: Arc>) -> GlobalState { GlobalState::MainView { search_field_id, - focused_search_result: ScrollHandle::new(true), + focused_search_result: ScrollHandle::new(true, ESTIMATED_MAIN_LIST_ITEM_HEIGHT), sub_state: MainViewState::new(), pending_plugin_view_data: None, client_context, @@ -99,7 +96,6 @@ impl GlobalState { pub fn new_plugin(plugin_view_data: PluginViewData, client_context: Arc>) -> GlobalState { GlobalState::PluginView { client_context, - focused_item: ScrollHandle::new(false), // TODO first focused? plugin_view_data, sub_state: PluginViewState::new(), } @@ -321,29 +317,30 @@ impl Focus for GlobalState { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { MainViewState::None => { - focused_search_result.focus_previous_row(1) + focused_search_result.focus_previous() + .unwrap_or_else(|| Command::none()) } MainViewState::SearchResultActionPanel { focused_action_item } => { - focused_action_item.focus_previous_row(1) + focused_action_item.focus_previous() + .unwrap_or_else(|| Command::none()) } MainViewState::InlineViewActionPanel { focused_action_item } => { - focused_action_item.focus_previous_row(1) + focused_action_item.focus_previous() + .unwrap_or_else(|| Command::none()) } } } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { sub_state, client_context, focused_item, .. } => { + GlobalState::PluginView { sub_state, client_context, .. } => { match sub_state { PluginViewState::None => { let client_context = client_context.read().expect("lock is poisoned"); - match client_context.keyboard_navigation_width() { - None => Command::none(), - Some(width) => focused_item.focus_previous_row(width) - } + client_context.focus_up() }, PluginViewState::ActionPanel { focused_action_item } => { - focused_action_item.focus_previous_row(1) + focused_action_item.focus_previous() + .unwrap_or_else(|| Command::none()) } } }, @@ -355,7 +352,8 @@ impl Focus for GlobalState { match sub_state { MainViewState::None => { if focus_list.len() != 0 { - focused_search_result.focus_next_row(focus_list.len(), 1) + focused_search_result.focus_next(focus_list.len()) + .unwrap_or_else(|| Command::none()) } else { Command::none() } @@ -363,7 +361,8 @@ impl Focus for GlobalState { MainViewState::SearchResultActionPanel { focused_action_item } => { if let Some(search_item) = focused_search_result.get(focus_list) { if search_item.entrypoint_actions.len() != 0 { - focused_action_item.focus_next_row(search_item.entrypoint_actions.len() + 1, 1) + focused_action_item.focus_next(search_item.entrypoint_actions.len() + 1) + .unwrap_or_else(|| Command::none()) } else { Command::none() } @@ -377,7 +376,8 @@ impl Focus for GlobalState { match client_context.get_first_inline_view_action_panel() { Some(action_panel) => { if action_panel.action_count() != 0 { - focused_action_item.focus_next_row(action_panel.action_count(), 1) + focused_action_item.focus_next(action_panel.action_count()) + .unwrap_or_else(|| Command::none()) } else { Command::none() } @@ -388,17 +388,12 @@ impl Focus for GlobalState { } } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { sub_state, client_context, focused_item, .. } => { + GlobalState::PluginView { sub_state, client_context, .. } => { match sub_state { PluginViewState::None => { let client_context = client_context.read().expect("lock is poisoned"); - let total = client_context.keyboard_navigation_total(); - - match client_context.keyboard_navigation_width() { - None => Command::none(), - Some(width) => focused_item.focus_next_row(total, width) - } + client_context.focus_down() }, PluginViewState::ActionPanel { focused_action_item } => { let client_context = client_context.read().expect("lock is poisoned"); @@ -406,7 +401,8 @@ impl Focus for GlobalState { let action_ids = client_context.get_action_ids(); if action_ids.len() != 0 { - focused_action_item.focus_next_row(action_ids.len(), 1) + focused_action_item.focus_next(action_ids.len()) + .unwrap_or_else(|| Command::none()) } else { Command::none() } @@ -417,15 +413,12 @@ impl Focus for GlobalState { } fn left(&mut self, _focus_list: &[SearchResult]) -> Command { match self { - GlobalState::PluginView { client_context, sub_state, focused_item, .. } => { + GlobalState::PluginView { client_context, sub_state, .. } => { match sub_state { PluginViewState::None => { let client_context = client_context.read().expect("lock is poisoned"); - match client_context.keyboard_navigation_width() { - None => Command::none(), - Some(width) => focused_item.focus_previous_column(width) - } + client_context.focus_left() } PluginViewState::ActionPanel { .. } => Command::none() } @@ -436,17 +429,12 @@ impl Focus for GlobalState { } fn right(&mut self, _focus_list: &[SearchResult]) -> Command { match self { - GlobalState::PluginView { client_context, sub_state, focused_item, .. } => { + GlobalState::PluginView { client_context, sub_state, .. } => { match sub_state { PluginViewState::None => { let client_context = client_context.read().expect("lock is poisoned"); - let total = client_context.keyboard_navigation_total(); - - match client_context.keyboard_navigation_width() { - None => Command::none(), - Some(width) => focused_item.focus_next_column(total, width) - } + client_context.focus_right() } PluginViewState::ActionPanel { .. } => Command::none() } diff --git a/rust/client/src/ui/state/plugin_view.rs b/rust/client/src/ui/state/plugin_view.rs index 72e8044..4ffc88e 100644 --- a/rust/client/src/ui/state/plugin_view.rs +++ b/rust/client/src/ui/state/plugin_view.rs @@ -1,4 +1,4 @@ -use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_ACTION_ITEM_HEIGHT}; use common::model::UiWidgetId; #[derive(Debug, Clone)] @@ -21,7 +21,7 @@ impl PluginViewState { pub fn action_panel(prev_state: &mut PluginViewState, focus_first: bool) { *prev_state = Self::ActionPanel { - focused_action_item: ScrollHandle::new(focus_first), + focused_action_item: ScrollHandle::new(focus_first, ESTIMATED_ACTION_ITEM_HEIGHT), } } } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 199a9ff..8a9e21d 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1,4 +1,5 @@ use std::cell::Cell; +use std::cmp::max; use std::collections::HashMap; use std::fmt::{Debug, Display}; @@ -6,11 +7,12 @@ use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMem use common_ui::shortcut_to_text; use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; +use iced::widget::image::Handle; use iced::widget::text::Shaping; use iced::widget::tooltip::Position; use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, mouse_area, pick_list, row, scrollable, text, text_input, tooltip, vertical_rule, Space}; -use iced::{Alignment, Font, Length}; -use iced::widget::image::Handle; +use iced::{Alignment, Command, Font, Length}; +use iced::futures::StreamExt; use iced_aw::core::icons; use iced_aw::date_picker::Date; use iced_aw::floating_element::Offset; @@ -20,7 +22,7 @@ use itertools::Itertools; use crate::model::UiViewEvent; use crate::ui::custom_widgets::loading_bar::LoadingBar; -use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_GRID_ITEM_HEIGHT, ESTIMATED_MAIN_LIST_ITEM_HEIGHT}; use crate::ui::state::PluginViewState; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; @@ -102,7 +104,11 @@ impl<'b> ComponentWidgets<'b> { } fn root_state_mut(&mut self, widget_id: UiWidgetId) -> &mut RootState { - let state = self.state.get_mut(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + Self::root_state_mut_on_field(&mut self.state, widget_id) + } + + fn root_state_mut_on_field(state: &mut HashMap, widget_id: UiWidgetId) -> &mut RootState { + let state = state.get_mut(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); match state { ComponentWidgetState::Root(state) => state, @@ -120,10 +126,10 @@ pub fn create_state(root_widget: &RootWidget) -> HashMap { match members { RootWidgetMembers::Detail(widget) => { - result.insert(widget.__id__, ComponentWidgetState::root()); + result.insert(widget.__id__, ComponentWidgetState::root(0.0)); } RootWidgetMembers::Form(widget) => { - result.insert(widget.__id__, ComponentWidgetState::root()); + result.insert(widget.__id__, ComponentWidgetState::root(0.0)); for members in &widget.content.ordered_members { match members { @@ -147,14 +153,14 @@ pub fn create_state(root_widget: &RootWidget) -> HashMap { - result.insert(widget.__id__, ComponentWidgetState::root()); + result.insert(widget.__id__, ComponentWidgetState::root(ESTIMATED_MAIN_LIST_ITEM_HEIGHT)); if let Some(widget) = &widget.content.search_bar { result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); } } RootWidgetMembers::Grid(widget) => { - result.insert(widget.__id__, ComponentWidgetState::root()); + result.insert(widget.__id__, ComponentWidgetState::root(ESTIMATED_GRID_ITEM_HEIGHT)); if let Some(widget) = &widget.content.search_bar { result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); @@ -201,12 +207,14 @@ struct SelectState { #[derive(Debug, Clone)] struct RootState { show_action_panel: bool, + focused_item: ScrollHandle, } impl ComponentWidgetState { - fn root() -> ComponentWidgetState { + fn root(item_height: f32) -> ComponentWidgetState { ComponentWidgetState::Root(RootState { show_action_panel: false, + focused_item: ScrollHandle::new(false, item_height), // TODO first focused? }) } @@ -320,34 +328,140 @@ impl<'b> ComponentWidgets<'b> { result } - pub fn keyboard_navigation_width(&self) -> Option { + pub fn focus_up(&mut self) -> Command { let Some(root_widget) = &self.root_widget else { - return None; + return Command::none(); }; let Some(content) = &root_widget.content else { - return None; + return Command::none(); }; match content { - RootWidgetMembers::List(_) => Some(1), - RootWidgetMembers::Grid(GridWidget { columns, .. }) => Some(grid_width(columns)), - _ => None + RootWidgetMembers::Detail(_) => Command::none(), + RootWidgetMembers::Form(_) => Command::none(), + RootWidgetMembers::Inline(_) => Command::none(), + RootWidgetMembers::List(widget) => { + let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, widget.__id__); + + focused_item.focus_previous() + .unwrap_or_else(|| Command::none()) + } + RootWidgetMembers::Grid(grid_widget) => { + let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); + + let Some(current_index) = &focused_item.index else { + return Command::none(); + }; + + let mut amount_per_section: Vec<(usize, usize)> = vec![]; + let mut current_width: usize = 0; + + { + let mut cumulative_index = 0; + let mut pending_section_size = 0; + + for members in &grid_widget.content.ordered_members { + match &members { + GridWidgetOrderedMembers::GridItem(_) => { + pending_section_size = pending_section_size + 1; + } + GridWidgetOrderedMembers::GridSection(widget) => { + if pending_section_size > 0 { + cumulative_index = cumulative_index + pending_section_size; + if cumulative_index >= (*current_index + 1) { + current_width = grid_width(&grid_widget.columns); + break + } + + amount_per_section.push((pending_section_size, grid_width(&grid_widget.columns))); + + pending_section_size = 0; + } + + let section_amount = widget + .content + .ordered_members + .iter() + .filter(|members| matches!(members, GridSectionWidgetOrderedMembers::GridItem(_))) + .count(); + + cumulative_index = cumulative_index + section_amount; + if cumulative_index >= (*current_index + 1) { + current_width = grid_width(&widget.columns); + break + } + + amount_per_section.push((section_amount, grid_width(&widget.columns))) + } + } + } + + if pending_section_size > 0 { + cumulative_index = cumulative_index + pending_section_size; + if cumulative_index >= (*current_index + 1) { + current_width = grid_width(&grid_widget.columns); + // break + } else { + amount_per_section.push((pending_section_size, grid_width(&grid_widget.columns))); + } + } + } + + let Some((amount_prev_section, width_prev_section)) = amount_per_section.iter().last() else { + return Command::none() + }; + + let total_amount_prev_section: usize = amount_per_section.iter() + .map(|(section, _)| section) + .sum(); + + let inside_of_current_section = (current_index - total_amount_prev_section + 1) + .checked_sub(current_width) // basically a check if result is >= 0 + .is_some(); + + if inside_of_current_section { + let _ = focused_item.focus_previous_in(current_width); + + // focused_item.scroll_to(row_amount) + Command::none() + } else { + let last_row_amount_prev_section = amount_prev_section % width_prev_section; + + if last_row_amount_prev_section == 0 { + let _ = focused_item.focus_previous_in(current_width); + + // focused_item.scroll_to(row_amount) + Command::none() + } else { + let current_column_current_section = (current_index - amount_prev_section) % current_width; + let _ = focused_item.focus_previous_in(max(current_column_current_section + 1, last_row_amount_prev_section)); + + // focused_item.scroll_to(row_amount) + Command::none() + } + } + } } } - pub fn keyboard_navigation_total(&self) -> usize { + pub fn focus_down(&mut self) -> Command { let Some(root_widget) = &self.root_widget else { - return 0; + return Command::none(); }; let Some(content) = &root_widget.content else { - return 0; + return Command::none(); }; match content { + RootWidgetMembers::Detail(_) => Command::none(), + RootWidgetMembers::Form(_) => Command::none(), + RootWidgetMembers::Inline(_) => Command::none(), RootWidgetMembers::List(widget) => { - widget.content.ordered_members + let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, widget.__id__); + + let total = widget.content.ordered_members .iter() .flat_map(|members| { match members { @@ -364,10 +478,15 @@ impl<'b> ComponentWidgets<'b> { } } }) - .count() + .count(); + + focused_item.focus_next(total) + .unwrap_or_else(|| Command::none()) } - RootWidgetMembers::Grid(widget) => { - widget.content.ordered_members + RootWidgetMembers::Grid(grid_widget) => { + let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); + + let total = grid_widget.content.ordered_members .iter() .flat_map(|members| { match members { @@ -384,25 +503,176 @@ impl<'b> ComponentWidgets<'b> { } } }) - .count() + .count(); + + let Some(current_index) = &focused_item.index else { + let _ = focused_item.focus_next(total); + + return focused_item.scroll_to(0) + }; + + let mut amount_per_section: Vec<(usize, usize)> = vec![]; + let mut amount_next_section: Option<(usize, usize)> = None; + + { + let mut cumulative_index = 0; + let mut consume_one_more = false; + let mut pending_section_size = 0; + + for members in &grid_widget.content.ordered_members { + match &members { + GridWidgetOrderedMembers::GridItem(_) => { + pending_section_size = pending_section_size + 1; + } + GridWidgetOrderedMembers::GridSection(widget) => { + if pending_section_size > 0 { + cumulative_index = cumulative_index + pending_section_size; + let section = (pending_section_size, grid_width(&grid_widget.columns)); + if consume_one_more { + amount_next_section = Some(section); + break + } else { + amount_per_section.push(section); + if cumulative_index >= (*current_index + 1) { + consume_one_more = true; + } + } + + pending_section_size = 0; + } + + let section_amount = widget + .content + .ordered_members + .iter() + .filter(|members| matches!(members, GridSectionWidgetOrderedMembers::GridItem(_))) + .count(); + + cumulative_index = cumulative_index + section_amount; + let section = (section_amount, grid_width(&widget.columns)); + if consume_one_more { + amount_next_section = Some(section); + break + } else { + amount_per_section.push(section); + if cumulative_index >= (*current_index + 1) { + consume_one_more = true; + } + } + } + } + } + + if pending_section_size > 0 { + if !consume_one_more { + amount_per_section.push((pending_section_size, grid_width(&grid_widget.columns))); + + cumulative_index = cumulative_index + pending_section_size; + } + } + } + + let Some((amount_current_section, width_current_section)) = amount_per_section.iter().last() else { + return Command::none() + }; + + let total_amount_current_section: usize = amount_per_section.iter() + .map(|(amount_prev_section, _)| amount_prev_section) + .sum(); + + let inside_of_current_section = current_index + width_current_section <= total_amount_current_section - 1; + + if inside_of_current_section { + let _ = focused_item.focus_next_in(total, *width_current_section); + + // focused_item.scroll_to(row_amount) + Command::none() + } else { + let current_column_current_section = (total_amount_current_section - 1 - current_index) % width_current_section; + + if current_column_current_section == *width_current_section - 1 { + let _ = focused_item.focus_next_in(total, *width_current_section); + + // focused_item.scroll_to(row_amount) + Command::none() + } else { + let _ = focused_item.focus_next_in(total, current_column_current_section + 1); + + // focused_item.scroll_to(row_amount) + Command::none() + } + } } - _ => 0 } } - pub fn has_search_bar(&self) -> bool { + pub fn focus_left(&mut self) -> Command { let Some(root_widget) = &self.root_widget else { - return false; + return Command::none(); }; let Some(content) = &root_widget.content else { - return false; + return Command::none(); }; match content { - RootWidgetMembers::List(widget) => widget.content.search_bar.is_some(), - RootWidgetMembers::Grid(widget) => widget.content.search_bar.is_some(), - _ => false + RootWidgetMembers::Detail(_) => Command::none(), + RootWidgetMembers::Form(_) => Command::none(), + RootWidgetMembers::Inline(_) => Command::none(), + RootWidgetMembers::List(_) => Command::none(), + RootWidgetMembers::Grid(widget) => { + let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, widget.__id__); + + let _ = focused_item.focus_previous(); + + // focused_item.scroll_to(0) + // TODO + Command::none() + } + } + } + + pub fn focus_right(&mut self) -> Command { + let Some(root_widget) = &self.root_widget else { + return Command::none(); + }; + + let Some(content) = &root_widget.content else { + return Command::none(); + }; + + match content { + RootWidgetMembers::Detail(_) => Command::none(), + RootWidgetMembers::Form(_) => Command::none(), + RootWidgetMembers::Inline(_) => Command::none(), + RootWidgetMembers::List(_) => Command::none(), + RootWidgetMembers::Grid(grid_widget) => { + let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); + + let total = grid_widget.content.ordered_members + .iter() + .flat_map(|members| { + match members { + GridWidgetOrderedMembers::GridItem(widget) => vec![widget], + GridWidgetOrderedMembers::GridSection(widget) => { + widget.content.ordered_members + .iter() + .map(|members| { + match members { + GridSectionWidgetOrderedMembers::GridItem(widget) => widget, + } + }) + .collect() + } + } + }) + .count(); + + let _ = focused_item.focus_next(total); + + // focused_item.scroll_to(0) + Command::none() + } } } @@ -455,7 +725,6 @@ impl<'b> ComponentWidgets<'b> { plugin_view_state: &PluginViewState, entrypoint_name: Option<&String>, action_shortcuts: &HashMap, - focused_item: &ScrollHandle, ) -> Element<'a, ComponentWidgetEvent> { match &self.root_widget { None => { @@ -473,7 +742,7 @@ impl<'b> ComponentWidgets<'b> { match content { RootWidgetMembers::Detail(widget) => { - let RootState { show_action_panel } = self.root_state(widget.__id__); + let RootState { show_action_panel, .. } = self.root_state(widget.__id__); let content = self.render_detail_widget(widget, false); @@ -490,8 +759,8 @@ impl<'b> ComponentWidgets<'b> { ) }, RootWidgetMembers::Form(widget) => self.render_form_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts), - RootWidgetMembers::List(widget) => self.render_list_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts, focused_item), - RootWidgetMembers::Grid(widget) => self.render_grid_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts, focused_item), + RootWidgetMembers::List(widget) => self.render_list_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts), + RootWidgetMembers::Grid(widget) => self.render_grid_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts), _ => { panic!("used inline widget in non-inline place") } @@ -917,7 +1186,7 @@ impl<'b> ComponentWidgets<'b> { action_shortcuts: &HashMap, ) -> Element<'a, ComponentWidgetEvent> { let widget_id = widget.__id__; - let RootState { show_action_panel } = self.root_state(widget_id); + let RootState { show_action_panel, .. } = self.root_state(widget_id); let items: Vec> = widget.content.ordered_members .iter() @@ -1188,10 +1457,9 @@ impl<'b> ComponentWidgets<'b> { plugin_view_state: &PluginViewState, entrypoint_name: &str, action_shortcuts: &HashMap, - focused_item: &ScrollHandle, ) -> Element<'a, ComponentWidgetEvent> { let widget_id = list_widget.__id__; - let RootState { show_action_panel } = self.root_state(widget_id); + let RootState { show_action_panel, focused_item } = self.root_state(widget_id); let mut pending: Vec<&ListItemWidget> = vec![]; let mut items: Vec> = vec![]; @@ -1400,9 +1668,8 @@ impl<'b> ComponentWidgets<'b> { plugin_view_state: &PluginViewState, entrypoint_name: &str, action_shortcuts: &HashMap, - focused_item: &ScrollHandle, ) -> Element<'a, ComponentWidgetEvent> { - let RootState { show_action_panel } = self.root_state(grid_widget.__id__); + let RootState { show_action_panel, focused_item } = self.root_state(grid_widget.__id__); let mut pending: Vec<&GridItemWidget> = vec![]; let mut items: Vec> = vec![]; diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index ea5d9ed..8a8be43 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -8,6 +8,8 @@ use std::collections::HashMap; use std::mem; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use iced::Command; +use crate::ui::AppMsg; use crate::ui::scroll_handle::ScrollHandle; pub struct PluginWidgetContainer { @@ -89,13 +91,12 @@ impl PluginWidgetContainer { &self, plugin_view_state: &PluginViewState, action_shortcuts: &HashMap, - focused_item: &ScrollHandle, ) -> Element<'a, ComponentWidgetEvent> { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); ComponentWidgets::new(&mut root_widget, &mut state, &self.images) - .render_root_widget(plugin_view_state, self.entrypoint_name.as_ref(), action_shortcuts, focused_item) + .render_root_widget(plugin_view_state, self.entrypoint_name.as_ref(), action_shortcuts) } pub fn render_inline_root_widget<'a>(&self) -> Element<'a, ComponentWidgetEvent> { @@ -127,24 +128,31 @@ impl PluginWidgetContainer { ComponentWidgets::new(&mut root_widget, &mut state, &self.images).get_action_panel(action_shortcuts) } - pub fn keyboard_navigation_width(&self) -> Option { + pub fn focus_up(&self) -> Command { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).keyboard_navigation_width() + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_up() } - pub fn keyboard_navigation_total(&self) -> usize { + pub fn focus_down(&self) -> Command { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).keyboard_navigation_total() + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_down() } - pub fn has_search_bar(&self) -> bool { + pub fn focus_left(&self) -> Command { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).has_search_bar() + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_left() + } + + pub fn focus_right(&self) -> Command { + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_right() } } From 1bd4491429b9dce37b20e535acea39c95b5077f6 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Wed, 13 Nov 2024 20:04:50 +0100 Subject: [PATCH 169/540] Rework grid keyboard navigation again, this time with tests --- dev_plugin/src/grid-view.tsx | 73 ++++ rust/client/src/ui/grid_navigation.rs | 505 ++++++++++++++++++++++++++ rust/client/src/ui/mod.rs | 1 + rust/client/src/ui/scroll_handle.rs | 13 +- rust/client/src/ui/widget.rs | 287 +++++---------- 5 files changed, 681 insertions(+), 198 deletions(-) create mode 100644 rust/client/src/ui/grid_navigation.rs diff --git a/dev_plugin/src/grid-view.tsx b/dev_plugin/src/grid-view.tsx index 6bb9abd..e671602 100644 --- a/dev_plugin/src/grid-view.tsx +++ b/dev_plugin/src/grid-view.tsx @@ -55,6 +55,20 @@ export default function GridView(): ReactElement { + + + + Test Paragraph Section 1 2 + + + + + + + Test Paragraph Section 1 3 + + + @@ -82,6 +96,65 @@ export default function GridView(): ReactElement { + + + + Test Paragraph Section 2 3 + + + + + + + + + Test Paragraph Section 2 1 + + + + + + + Test Paragraph Section 2 2 + + + + + + + Test Paragraph Section 2 2 + + + + + + + Test Paragraph Section 2 3 + + + + + + + Test Paragraph Section 2 4 + + + + + + { + Array.from({ length: 200 }, (_, k) => k + 1) + .map(value => ( + + + + Test Paragraph {value} + + + + ) + ) + } ) diff --git a/rust/client/src/ui/grid_navigation.rs b/rust/client/src/ui/grid_navigation.rs new file mode 100644 index 0000000..547e2ab --- /dev/null +++ b/rust/client/src/ui/grid_navigation.rs @@ -0,0 +1,505 @@ +use std::ops::{Div, Rem}; + +#[derive(Debug)] +pub struct GridSectionData { + pub start_index: usize, + pub start_row_index: usize, + pub amount_in_section: usize, + pub width: usize +} + +struct GridRowData { + row_index: usize, + amount_in_row: usize, + max_amount_in_row: usize, +} + +struct GridCurrentRowData { + row_index: usize, + column_index: usize, + amount_in_row: usize, + max_amount_in_row: usize, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct GridItemOffset { + pub row_index: usize, + pub offset: usize, +} + +fn grid_row_data(mut amount_per_section_total: Vec, current_index: usize) -> (Option, GridCurrentRowData, Option) { + let mut previous_section_index: Option = None; + let mut current_section_index: usize = 0; + + for (index, GridSectionData { start_index, amount_in_section, .. }) in amount_per_section_total.iter().enumerate() { + if start_index + amount_in_section >= (current_index + 1) { + current_section_index = index; + break + } + previous_section_index = Some(index); + } + + let prev_section = previous_section_index.and_then(|index| amount_per_section_total.get(index)); + let current_section = amount_per_section_total.get(current_section_index).expect("guarantied to exist"); + let next_section = amount_per_section_total.get(current_section_index + 1); + + let item_index_in_section = current_index - current_section.start_index; + let row_index_in_section = usize::div(item_index_in_section, current_section.width); + let row_index = current_section.start_row_index + row_index_in_section; + + let amount_of_rows = usize::div_ceil(current_section.amount_in_section, current_section.width); + let last_row = row_index_in_section + 1 == amount_of_rows; + + let current_row = { + if last_row { + let reminder = usize::rem(current_section.amount_in_section, current_section.width); + GridCurrentRowData { + row_index, + column_index: usize::rem(item_index_in_section, current_section.width), + amount_in_row: if reminder == 0 { current_section.width } else { reminder }, + max_amount_in_row: current_section.width + } + } else { + GridCurrentRowData { + row_index, + column_index: usize::rem(item_index_in_section, current_section.width), + amount_in_row: current_section.width, + max_amount_in_row: current_section.width + } + } + }; + + let prev_row = { + if row_index_in_section == 0 { + match prev_section { + None => None, + Some(prev_section) => { + let reminder = usize::rem(prev_section.amount_in_section, prev_section.width); + Some(GridRowData { + row_index: current_row.row_index - 1, + amount_in_row: if reminder == 0 { prev_section.width } else { reminder }, + max_amount_in_row: prev_section.width + }) + } + } + } else { + Some(GridRowData { + row_index: current_row.row_index - 1, + amount_in_row: current_section.width, + max_amount_in_row: current_section.width + }) + } + }; + + let next_row = { + if last_row { + match next_section { + None => None, + Some(next_section) => { + let reminder = usize::rem(next_section.amount_in_section, next_section.width); + Some(GridRowData { + row_index: current_row.row_index + 1, + amount_in_row: if reminder == 0 { next_section.width } else { reminder }, + max_amount_in_row: next_section.width + }) + } + } + } else { + let amount_of_full_rows = usize::div(current_section.amount_in_section, current_section.width); + if row_index_in_section + 1 == amount_of_full_rows { + let reminder = usize::rem(current_section.amount_in_section, current_section.width); + Some(GridRowData { + row_index: current_row.row_index + 1, + amount_in_row: if reminder == 0 { current_section.width } else { reminder }, + max_amount_in_row: current_section.width + }) + } else { + Some(GridRowData { + row_index: current_row.row_index + 1, + amount_in_row: current_section.width, + max_amount_in_row: current_section.width + }) + } + } + }; + + (prev_row, current_row, next_row) +} + +pub fn grid_up_offset(current_index: usize, amount_per_section_total: Vec) -> Option { + let (prev_row, current_row, _next_row) = grid_row_data(amount_per_section_total, current_index); + + match prev_row { + None => None, + Some(prev_row) => { + if prev_row.amount_in_row < prev_row.max_amount_in_row { + if prev_row.amount_in_row > (current_row.column_index + 1) { + Some(GridItemOffset { + row_index: prev_row.row_index, + offset: prev_row.max_amount_in_row - prev_row.amount_in_row + (current_row.column_index + 1), + }) + } else { + Some(GridItemOffset { + row_index: prev_row.row_index, + offset: current_row.column_index + 1, + }) + } + } else { + Some(GridItemOffset { + row_index: prev_row.row_index, + offset: current_row.max_amount_in_row, + }) + } + } + } +} + +pub fn grid_down_offset(current_index: usize, amount_per_section_total: Vec) -> Option { + let (_prev_row, current_row, next_row) = grid_row_data(amount_per_section_total, current_index); + + match next_row { + None => None, + Some(next_row) => { + if (current_row.column_index + 1) > next_row.amount_in_row { + Some(GridItemOffset { + row_index: next_row.row_index, + offset: (current_row.max_amount_in_row - (current_row.column_index + 1)) + next_row.amount_in_row, + }) + } else { + Some(GridItemOffset { + row_index: next_row.row_index, + offset: current_row.amount_in_row, + }) + } + } + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + fn prepare_sections(data: Vec>>) -> Vec { + let mut cumulative_item_index = 0; + let mut cumulative_row_index = 0; + + data.iter() + .map(|section| { + let cumulative_item_index_at_start = cumulative_item_index; + let cumulative_row_index_at_start = cumulative_row_index; + + let amount = section.iter() + .map(|row| row.iter().sum::()) + .sum(); + + let width = section[0].len(); + + assert!(section.iter().all(|row| row.len() == width)); + for row in section { + assert!(row.iter().all(|item| *item == 0 || *item == 1)); + } + + cumulative_item_index = cumulative_item_index_at_start + amount; + cumulative_row_index = cumulative_row_index_at_start + (usize::div_ceil(amount, width)); + + GridSectionData { + start_index: cumulative_item_index_at_start, + start_row_index: cumulative_row_index_at_start, + amount_in_section: amount, + width, + } + }) + .collect() + } + + #[test] + fn grid_down_last_row() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //fr V + vec![1, 1, 0], + ], + ] + ); + + assert_eq!(grid_down_offset(1, sections_amount_width), None) + } + + #[test] + fn grid_down_inside_1() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //fr V + vec![1, 1, 1], + //to V + vec![1, 1, 0], + ], + ] + ); + + assert_eq!(grid_down_offset(0, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 3 } )) + } + + #[test] + fn grid_down_inside_2() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //fr V + vec![1, 1, 1], + //to V + vec![1, 1, 0], + ], + ] + ); + + assert_eq!(grid_down_offset(1, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 3 } )) + } + + #[test] + fn grid_down_inside_3() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //fr V + vec![1, 1, 1], + //to V + vec![1, 1, 0], + ], + ] + ); + + assert_eq!(grid_down_offset(2, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 2 } )) + } + + #[test] + fn grid_down_inside_4() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //fr V + vec![1, 1, 1], + //to V + vec![1, 0, 0], + ], + ] + ); + + assert_eq!(grid_down_offset(2, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 1 } )) + } + + #[test] + fn grid_down_inside_5() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + vec![1, 1, 1], + //fr V + vec![1, 1, 1], + //to V + vec![1, 1, 0], + ], + ] + ); + + assert_eq!(grid_down_offset(4, sections_amount_width), Some(GridItemOffset { row_index: 2, offset: 3 } )) + } + + #[test] + fn grid_down_outside_1() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //fr V + vec![1, 1, 1], + ], + vec![ + //to V + vec![1, 1, 1], + ], + ] + ); + + assert_eq!(grid_down_offset(1, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 3 })) + } + + #[test] + fn grid_down_outside_2() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //fr V + vec![1, 1, 0], + ], + vec![ + //to V + vec![1, 1, 1], + ], + ] + ); + + assert_eq!(grid_down_offset(1, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 2 })) + } + + #[test] + fn grid_down_outside_3() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //fr V + vec![1, 1, 1], + ], + vec![ + //to V + vec![1, 1, 0], + ], + vec![ + vec![1, 1, 1], + ], + ] + ); + + assert_eq!(grid_down_offset(2, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 2 })) + } + + #[test] + fn grid_up_first_row() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //fr V + vec![1, 1, 0], + ], + ] + ); + + assert_eq!(grid_up_offset(1, sections_amount_width), None) + } + + #[test] + fn grid_up_inside_1() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //to V + vec![1, 1, 1], + //fr V + vec![1, 1, 0], + ], + ] + ); + + assert_eq!(grid_up_offset(4, sections_amount_width), Some(GridItemOffset { row_index: 0, offset: 3 })) + } + + #[test] + fn grid_up_inside_2() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //to V + vec![1, 1, 1], + //fr V + vec![1, 1, 0], + ], + ] + ); + + assert_eq!(grid_up_offset(3, sections_amount_width), Some(GridItemOffset { row_index: 0, offset: 3 })) + } + + #[test] + fn grid_up_inside_3() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //to V + vec![1, 1, 1], + //fr V + vec![1, 1, 1], + ], + ] + ); + + assert_eq!(grid_up_offset(5, sections_amount_width), Some(GridItemOffset { row_index: 0, offset: 3 })) + } + + #[test] + fn grid_up_outside_1() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //to V + vec![1, 1, 1], + ], + vec![ + //fr V + vec![1, 1, 1], + ], + ] + ); + + assert_eq!(grid_up_offset(4, sections_amount_width), Some(GridItemOffset { row_index: 0, offset: 3 })) + } + + #[test] + fn grid_up_outside_2() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //to V + vec![1, 1, 0], + ], + vec![ + //fr V + vec![1, 1, 1], + ], + ] + ); + + assert_eq!(grid_up_offset(4, sections_amount_width), Some(GridItemOffset { row_index: 0, offset: 3 })) + } + + + #[test] + fn grid_up_outside_3() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + //to V + vec![1, 1, 0], + ], + vec![ + //fr V + vec![1, 0, 0], + ], + ] + ); + + assert_eq!(grid_up_offset(2, sections_amount_width), Some(GridItemOffset { row_index: 0, offset: 2 })) + } + + #[test] + fn grid_up_outside_4() { + let sections_amount_width = prepare_sections( + vec![ + vec![ + vec![1, 1, 1], + ], + vec![ + //to V + vec![1, 1, 0], + ], + vec![ + //fr V + vec![1, 1, 1], + ], + ] + ); + + assert_eq!(grid_up_offset(7, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 3 })) + } +} \ No newline at end of file diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index d63f08f..a8a461f 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -51,6 +51,7 @@ mod custom_widgets; mod scroll_handle; mod state; mod hud; +mod grid_navigation; use crate::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; use crate::ui::custom_widgets::loading_bar::LoadingBar; diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs index 9e6185e..efe103e 100644 --- a/rust/client/src/ui/scroll_handle.rs +++ b/rust/client/src/ui/scroll_handle.rs @@ -6,7 +6,7 @@ use crate::ui::AppMsg; pub const ESTIMATED_MAIN_LIST_ITEM_HEIGHT: f32 = 38.8; pub const ESTIMATED_ACTION_ITEM_HEIGHT: f32 = 38.8; // TODO -pub const ESTIMATED_GRID_ITEM_HEIGHT: f32 = 38.0; // TODO +pub const ESTIMATED_GRID_ITEM_HEIGHT: f32 = 190.0; // TODO #[derive(Clone, Debug)] pub struct ScrollHandle { @@ -109,9 +109,16 @@ impl ScrollHandle { } } } - pub fn scroll_to(&self, row_index: usize) -> Command { - let pos_y = row_index as f32 * self.item_height - (self.offset as f32 * self.item_height); + self.scroll_to_offset(row_index, true) + } + + pub fn scroll_to_offset(&self, row_index: usize, no_offset: bool) -> Command { + let mut pos_y = row_index as f32 * self.item_height; + + if !no_offset { + pos_y = pos_y - (self.offset as f32 * self.item_height); + } scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 8a9e21d..ebf78cb 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1,27 +1,26 @@ -use std::cell::Cell; -use std::cmp::max; -use std::collections::HashMap; -use std::fmt::{Debug, Display}; - use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Icons, Image, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiWidgetId}; use common_ui::shortcut_to_text; use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; +use iced::futures::StreamExt; use iced::widget::image::Handle; use iced::widget::text::Shaping; use iced::widget::tooltip::Position; use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, mouse_area, pick_list, row, scrollable, text, text_input, tooltip, vertical_rule, Space}; use iced::{Alignment, Command, Font, Length}; -use iced::futures::StreamExt; use iced_aw::core::icons; use iced_aw::date_picker::Date; use iced_aw::floating_element::Offset; use iced_aw::helpers::{date_picker, grid, grid_row, wrap_horizontal}; use iced_aw::{floating_element, GridRow}; use itertools::Itertools; +use std::cell::Cell; +use std::collections::HashMap; +use std::fmt::{Debug, Display}; use crate::model::UiViewEvent; use crate::ui::custom_widgets::loading_bar::LoadingBar; +use crate::ui::grid_navigation::{grid_down_offset, grid_up_offset, GridSectionData}; use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_GRID_ITEM_HEIGHT, ESTIMATED_MAIN_LIST_ITEM_HEIGHT}; use crate::ui::state::PluginViewState; use crate::ui::theme::button::ButtonStyle; @@ -328,6 +327,76 @@ impl<'b> ComponentWidgets<'b> { result } + fn grid_section_sizes(grid_widget: &GridWidget) -> Vec { + let mut amount_per_section: Vec = vec![]; + let mut pending_section_size = 0; + + let mut cumulative_item_index = 0; + let mut cumulative_row_index = 0; + + let mut cumulative_item_index_at_start = cumulative_item_index; + let mut cumulative_row_index_at_start = cumulative_row_index; + + for members in &grid_widget.content.ordered_members { + match &members { + GridWidgetOrderedMembers::GridItem(_) => { + pending_section_size = pending_section_size + 1; + } + GridWidgetOrderedMembers::GridSection(widget) => { + if pending_section_size > 0 { + let width = grid_width(&grid_widget.columns); + amount_per_section.push(GridSectionData { + start_index: cumulative_item_index_at_start, + start_row_index: cumulative_row_index_at_start, + amount_in_section: pending_section_size, + width, + }); + + cumulative_item_index = cumulative_item_index + pending_section_size; + cumulative_row_index = cumulative_row_index_at_start + (usize::div_ceil(pending_section_size, width)); + + cumulative_item_index_at_start = cumulative_item_index; + cumulative_row_index_at_start = cumulative_row_index; + + pending_section_size = 0; + } + + let section_amount = widget + .content + .ordered_members + .iter() + .filter(|members| matches!(members, GridSectionWidgetOrderedMembers::GridItem(_))) + .count(); + + let width = grid_width(&widget.columns); + amount_per_section.push(GridSectionData { + start_index: cumulative_item_index_at_start, + start_row_index: cumulative_row_index_at_start, + amount_in_section: section_amount, + width, + }); + + cumulative_item_index = cumulative_item_index + section_amount; + cumulative_row_index = cumulative_row_index_at_start + (usize::div_ceil(section_amount, width)); + + cumulative_item_index_at_start = cumulative_item_index; + cumulative_row_index_at_start = cumulative_row_index; + } + } + } + + if pending_section_size > 0 { + amount_per_section.push(GridSectionData { + start_index: cumulative_item_index_at_start, + start_row_index: cumulative_row_index_at_start, + amount_in_section: pending_section_size, + width: grid_width(&grid_widget.columns), + }); + } + + amount_per_section + } + pub fn focus_up(&mut self) -> Command { let Some(root_widget) = &self.root_widget else { return Command::none(); @@ -354,91 +423,14 @@ impl<'b> ComponentWidgets<'b> { return Command::none(); }; - let mut amount_per_section: Vec<(usize, usize)> = vec![]; - let mut current_width: usize = 0; + let amount_per_section_total = Self::grid_section_sizes(grid_widget); - { - let mut cumulative_index = 0; - let mut pending_section_size = 0; + match grid_up_offset(*current_index, amount_per_section_total) { + None => Command::none(), + Some(data) => { + let _ = focused_item.focus_previous_in(data.offset); - for members in &grid_widget.content.ordered_members { - match &members { - GridWidgetOrderedMembers::GridItem(_) => { - pending_section_size = pending_section_size + 1; - } - GridWidgetOrderedMembers::GridSection(widget) => { - if pending_section_size > 0 { - cumulative_index = cumulative_index + pending_section_size; - if cumulative_index >= (*current_index + 1) { - current_width = grid_width(&grid_widget.columns); - break - } - - amount_per_section.push((pending_section_size, grid_width(&grid_widget.columns))); - - pending_section_size = 0; - } - - let section_amount = widget - .content - .ordered_members - .iter() - .filter(|members| matches!(members, GridSectionWidgetOrderedMembers::GridItem(_))) - .count(); - - cumulative_index = cumulative_index + section_amount; - if cumulative_index >= (*current_index + 1) { - current_width = grid_width(&widget.columns); - break - } - - amount_per_section.push((section_amount, grid_width(&widget.columns))) - } - } - } - - if pending_section_size > 0 { - cumulative_index = cumulative_index + pending_section_size; - if cumulative_index >= (*current_index + 1) { - current_width = grid_width(&grid_widget.columns); - // break - } else { - amount_per_section.push((pending_section_size, grid_width(&grid_widget.columns))); - } - } - } - - let Some((amount_prev_section, width_prev_section)) = amount_per_section.iter().last() else { - return Command::none() - }; - - let total_amount_prev_section: usize = amount_per_section.iter() - .map(|(section, _)| section) - .sum(); - - let inside_of_current_section = (current_index - total_amount_prev_section + 1) - .checked_sub(current_width) // basically a check if result is >= 0 - .is_some(); - - if inside_of_current_section { - let _ = focused_item.focus_previous_in(current_width); - - // focused_item.scroll_to(row_amount) - Command::none() - } else { - let last_row_amount_prev_section = amount_prev_section % width_prev_section; - - if last_row_amount_prev_section == 0 { - let _ = focused_item.focus_previous_in(current_width); - - // focused_item.scroll_to(row_amount) - Command::none() - } else { - let current_column_current_section = (current_index - amount_prev_section) % current_width; - let _ = focused_item.focus_previous_in(max(current_column_current_section + 1, last_row_amount_prev_section)); - - // focused_item.scroll_to(row_amount) - Command::none() + focused_item.scroll_to_offset(data.row_index, false) } } } @@ -486,120 +478,25 @@ impl<'b> ComponentWidgets<'b> { RootWidgetMembers::Grid(grid_widget) => { let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); - let total = grid_widget.content.ordered_members + let amount_per_section_total = Self::grid_section_sizes(grid_widget); + + let total = amount_per_section_total .iter() - .flat_map(|members| { - match members { - GridWidgetOrderedMembers::GridItem(widget) => vec![widget], - GridWidgetOrderedMembers::GridSection(widget) => { - widget.content.ordered_members - .iter() - .map(|members| { - match members { - GridSectionWidgetOrderedMembers::GridItem(widget) => widget, - } - }) - .collect() - } - } - }) - .count(); + .map(|data| data.amount_in_section) + .sum(); let Some(current_index) = &focused_item.index else { let _ = focused_item.focus_next(total); - return focused_item.scroll_to(0) + return focused_item.scroll_to_offset(0, false) }; - let mut amount_per_section: Vec<(usize, usize)> = vec![]; - let mut amount_next_section: Option<(usize, usize)> = None; + match grid_down_offset(*current_index, amount_per_section_total) { + None => Command::none(), + Some(data) => { + let _ = focused_item.focus_next_in(total, data.offset); - { - let mut cumulative_index = 0; - let mut consume_one_more = false; - let mut pending_section_size = 0; - - for members in &grid_widget.content.ordered_members { - match &members { - GridWidgetOrderedMembers::GridItem(_) => { - pending_section_size = pending_section_size + 1; - } - GridWidgetOrderedMembers::GridSection(widget) => { - if pending_section_size > 0 { - cumulative_index = cumulative_index + pending_section_size; - let section = (pending_section_size, grid_width(&grid_widget.columns)); - if consume_one_more { - amount_next_section = Some(section); - break - } else { - amount_per_section.push(section); - if cumulative_index >= (*current_index + 1) { - consume_one_more = true; - } - } - - pending_section_size = 0; - } - - let section_amount = widget - .content - .ordered_members - .iter() - .filter(|members| matches!(members, GridSectionWidgetOrderedMembers::GridItem(_))) - .count(); - - cumulative_index = cumulative_index + section_amount; - let section = (section_amount, grid_width(&widget.columns)); - if consume_one_more { - amount_next_section = Some(section); - break - } else { - amount_per_section.push(section); - if cumulative_index >= (*current_index + 1) { - consume_one_more = true; - } - } - } - } - } - - if pending_section_size > 0 { - if !consume_one_more { - amount_per_section.push((pending_section_size, grid_width(&grid_widget.columns))); - - cumulative_index = cumulative_index + pending_section_size; - } - } - } - - let Some((amount_current_section, width_current_section)) = amount_per_section.iter().last() else { - return Command::none() - }; - - let total_amount_current_section: usize = amount_per_section.iter() - .map(|(amount_prev_section, _)| amount_prev_section) - .sum(); - - let inside_of_current_section = current_index + width_current_section <= total_amount_current_section - 1; - - if inside_of_current_section { - let _ = focused_item.focus_next_in(total, *width_current_section); - - // focused_item.scroll_to(row_amount) - Command::none() - } else { - let current_column_current_section = (total_amount_current_section - 1 - current_index) % width_current_section; - - if current_column_current_section == *width_current_section - 1 { - let _ = focused_item.focus_next_in(total, *width_current_section); - - // focused_item.scroll_to(row_amount) - Command::none() - } else { - let _ = focused_item.focus_next_in(total, current_column_current_section + 1); - - // focused_item.scroll_to(row_amount) - Command::none() + focused_item.scroll_to_offset(data.row_index, false) } } } From 57ea67d13e19690f3270129a2d2965fbed8f7ff1 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Wed, 13 Nov 2024 21:55:00 +0100 Subject: [PATCH 170/540] Crappy heuristic for scrolling distance of grid items, dynamic grid item height --- dev_plugin/src/grid-view.tsx | 6 +- rust/client/src/ui/scroll_handle.rs | 44 +++++++------- rust/client/src/ui/state/main_view.rs | 4 +- rust/client/src/ui/state/mod.rs | 2 +- rust/client/src/ui/state/plugin_view.rs | 2 +- rust/client/src/ui/widget.rs | 80 +++++++++++++++++++------ 6 files changed, 89 insertions(+), 49 deletions(-) diff --git a/dev_plugin/src/grid-view.tsx b/dev_plugin/src/grid-view.tsx index e671602..d7d1cef 100644 --- a/dev_plugin/src/grid-view.tsx +++ b/dev_plugin/src/grid-view.tsx @@ -143,12 +143,12 @@ export default function GridView(): ReactElement { { - Array.from({ length: 200 }, (_, k) => k + 1) + Array.from({ length: 50 }, (_, k) => k + 1) .map(value => ( - + - Test Paragraph {value} + Test {value} diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs index efe103e..4c71b0d 100644 --- a/rust/client/src/ui/scroll_handle.rs +++ b/rust/client/src/ui/scroll_handle.rs @@ -6,7 +6,6 @@ use crate::ui::AppMsg; pub const ESTIMATED_MAIN_LIST_ITEM_HEIGHT: f32 = 38.8; pub const ESTIMATED_ACTION_ITEM_HEIGHT: f32 = 38.8; // TODO -pub const ESTIMATED_GRID_ITEM_HEIGHT: f32 = 190.0; // TODO #[derive(Clone, Debug)] pub struct ScrollHandle { @@ -14,16 +13,18 @@ pub struct ScrollHandle { pub scrollable_id: scrollable::Id, pub index: Option, offset: usize, + rows_per_view: usize, item_height: f32, } impl ScrollHandle { - pub fn new(first_focused: bool, item_height: f32) -> ScrollHandle { + pub fn new(first_focused: bool, item_height: f32, rows_per_view: usize) -> ScrollHandle { ScrollHandle { phantom: PhantomData, scrollable_id: scrollable::Id::unique(), index: if first_focused { Some(0) } else { None }, offset: 0, + rows_per_view, item_height, } } @@ -45,14 +46,17 @@ impl ScrollHandle { } pub fn focus_next(&mut self, total_item_amount: usize) -> Option> { - self.focus_next_in(total_item_amount, 1) + match self.focus_next_in(total_item_amount, 1) { + None => None, + Some(index) => Some(self.scroll_to(index)) + } } - pub fn focus_next_in(&mut self, total_item_amount: usize, amount: usize) -> Option> { - self.offset = if self.offset < 7 { + pub fn focus_next_in(&mut self, total_item_amount: usize, amount: usize) -> Option { + self.offset = if self.offset < self.rows_per_view { self.offset + 1 } else { - 7 + self.rows_per_view }; match self.index.as_mut() { @@ -61,7 +65,7 @@ impl ScrollHandle { if total_item_amount > 0 { self.index = Some(0); - Some(self.scroll_to(0)) + Some(0) } else { None } @@ -72,9 +76,7 @@ impl ScrollHandle { if new_index < total_item_amount { *index = new_index; - let index = *index; - - Some(self.scroll_to(index)) + Some(*index) } else { None } @@ -83,10 +85,13 @@ impl ScrollHandle { } pub fn focus_previous(&mut self) -> Option> { - self.focus_previous_in(1) + match self.focus_previous_in(1) { + None => None, + Some(index) => Some(self.scroll_to(index)) + } } - pub fn focus_previous_in(&mut self, amount: usize) -> Option> { + pub fn focus_previous_in(&mut self, amount: usize) -> Option { self.offset = if self.offset > 1 { self.offset - 1 } else { @@ -100,25 +105,18 @@ impl ScrollHandle { Some(new_index) => { *index = new_index; - let index = *index; - - Some(self.scroll_to(index)) + Some(new_index) } None => None } } } } + pub fn scroll_to(&self, row_index: usize) -> Command { - self.scroll_to_offset(row_index, true) - } + let mut pos_y = row_index as f32 * self.item_height - (self.offset as f32 * self.item_height); - pub fn scroll_to_offset(&self, row_index: usize, no_offset: bool) -> Command { - let mut pos_y = row_index as f32 * self.item_height; - - if !no_offset { - pos_y = pos_y - (self.offset as f32 * self.item_height); - } + println!("pos_y: {}, row_index: {}, item_height: {}, offset: {}", pos_y, row_index, self.item_height, self.offset); scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) } diff --git a/rust/client/src/ui/state/main_view.rs b/rust/client/src/ui/state/main_view.rs index ddd85c7..5502cb1 100644 --- a/rust/client/src/ui/state/main_view.rs +++ b/rust/client/src/ui/state/main_view.rs @@ -24,13 +24,13 @@ impl MainViewState { pub fn search_result_action_panel(prev_state: &mut MainViewState, focus_first: bool) { *prev_state = Self::SearchResultActionPanel { - focused_action_item: ScrollHandle::new(focus_first, ESTIMATED_ACTION_ITEM_HEIGHT), + focused_action_item: ScrollHandle::new(focus_first, ESTIMATED_ACTION_ITEM_HEIGHT, 7), } } pub fn inline_result_action_panel(prev_state: &mut MainViewState, focus_first: bool) { *prev_state = Self::InlineViewActionPanel { - focused_action_item: ScrollHandle::new(focus_first, ESTIMATED_ACTION_ITEM_HEIGHT), + focused_action_item: ScrollHandle::new(focus_first, ESTIMATED_ACTION_ITEM_HEIGHT, 7), } } } diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 34c7f5b..ed8ba83 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -79,7 +79,7 @@ impl GlobalState { pub fn new(search_field_id: text_input::Id, client_context: Arc>) -> GlobalState { GlobalState::MainView { search_field_id, - focused_search_result: ScrollHandle::new(true, ESTIMATED_MAIN_LIST_ITEM_HEIGHT), + focused_search_result: ScrollHandle::new(true, ESTIMATED_MAIN_LIST_ITEM_HEIGHT, 7), sub_state: MainViewState::new(), pending_plugin_view_data: None, client_context, diff --git a/rust/client/src/ui/state/plugin_view.rs b/rust/client/src/ui/state/plugin_view.rs index 4ffc88e..e9924e2 100644 --- a/rust/client/src/ui/state/plugin_view.rs +++ b/rust/client/src/ui/state/plugin_view.rs @@ -21,7 +21,7 @@ impl PluginViewState { pub fn action_panel(prev_state: &mut PluginViewState, focus_first: bool) { *prev_state = Self::ActionPanel { - focused_action_item: ScrollHandle::new(focus_first, ESTIMATED_ACTION_ITEM_HEIGHT), + focused_action_item: ScrollHandle::new(focus_first, ESTIMATED_ACTION_ITEM_HEIGHT, 7), } } } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index ebf78cb..35b4699 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -21,7 +21,7 @@ use std::fmt::{Debug, Display}; use crate::model::UiViewEvent; use crate::ui::custom_widgets::loading_bar::LoadingBar; use crate::ui::grid_navigation::{grid_down_offset, grid_up_offset, GridSectionData}; -use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_GRID_ITEM_HEIGHT, ESTIMATED_MAIN_LIST_ITEM_HEIGHT}; +use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_MAIN_LIST_ITEM_HEIGHT}; use crate::ui::state::PluginViewState; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; @@ -125,10 +125,10 @@ pub fn create_state(root_widget: &RootWidget) -> HashMap { match members { RootWidgetMembers::Detail(widget) => { - result.insert(widget.__id__, ComponentWidgetState::root(0.0)); + result.insert(widget.__id__, ComponentWidgetState::root(0.0, 0)); } RootWidgetMembers::Form(widget) => { - result.insert(widget.__id__, ComponentWidgetState::root(0.0)); + result.insert(widget.__id__, ComponentWidgetState::root(0.0, 0)); for members in &widget.content.ordered_members { match members { @@ -152,14 +152,43 @@ pub fn create_state(root_widget: &RootWidget) -> HashMap { - result.insert(widget.__id__, ComponentWidgetState::root(ESTIMATED_MAIN_LIST_ITEM_HEIGHT)); + result.insert(widget.__id__, ComponentWidgetState::root(ESTIMATED_MAIN_LIST_ITEM_HEIGHT, 7)); if let Some(widget) = &widget.content.search_bar { result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); } } RootWidgetMembers::Grid(widget) => { - result.insert(widget.__id__, ComponentWidgetState::root(ESTIMATED_GRID_ITEM_HEIGHT)); + // cursed heuristic + let has_title = widget.content + .ordered_members + .iter() + .flat_map(|members| match members { + GridWidgetOrderedMembers::GridItem(widget) => vec![widget], + GridWidgetOrderedMembers::GridSection(widget) => { + widget.content.ordered_members + .iter() + .map(|members| match members { + GridSectionWidgetOrderedMembers::GridItem(widget) => widget + }) + .collect() + } + }) + .next() + .map(|widget| widget.title.is_some() || widget.subtitle.is_some()) + .unwrap_or_default(); + + let (height, rows_per_view) = match grid_width(&widget.columns) { + ..4 => (150.0, 0), + 4 => (150.0, 0), + 5 => (130.0, 0), + 6 => (110.0, 1), + 7 => (90.0, 3), + 8 => (if has_title { 50.0 } else { 50.0 }, if has_title { 3 } else { 4 }), + 8.. => (50.0, 4), + }; + + result.insert(widget.__id__, ComponentWidgetState::root(height, rows_per_view)); if let Some(widget) = &widget.content.search_bar { result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); @@ -210,10 +239,10 @@ struct RootState { } impl ComponentWidgetState { - fn root(item_height: f32) -> ComponentWidgetState { + fn root(item_height: f32, rows_per_view: usize) -> ComponentWidgetState { ComponentWidgetState::Root(RootState { show_action_panel: false, - focused_item: ScrollHandle::new(false, item_height), // TODO first focused? + focused_item: ScrollHandle::new(false, item_height, rows_per_view), // TODO first focused? }) } @@ -428,9 +457,10 @@ impl<'b> ComponentWidgets<'b> { match grid_up_offset(*current_index, amount_per_section_total) { None => Command::none(), Some(data) => { - let _ = focused_item.focus_previous_in(data.offset); - - focused_item.scroll_to_offset(data.row_index, false) + match focused_item.focus_previous_in(data.offset) { + None => Command::none(), + Some(_) => focused_item.scroll_to(data.row_index) + } } } } @@ -488,15 +518,16 @@ impl<'b> ComponentWidgets<'b> { let Some(current_index) = &focused_item.index else { let _ = focused_item.focus_next(total); - return focused_item.scroll_to_offset(0, false) + return focused_item.scroll_to(0) }; match grid_down_offset(*current_index, amount_per_section_total) { None => Command::none(), Some(data) => { - let _ = focused_item.focus_next_in(total, data.offset); - - focused_item.scroll_to_offset(data.row_index, false) + match focused_item.focus_next_in(total, data.offset) { + None => Command::none(), + Some(_) => focused_item.scroll_to(data.row_index) + } } } } @@ -1650,11 +1681,21 @@ impl<'b> ComponentWidgets<'b> { &self, widget: &GridItemWidget, item_focus_index: Option, - index_counter: &Cell + index_counter: &Cell, + grid_width: usize ) -> Element<'a, ComponentWidgetEvent> { - // TODO not needed column element? - let content: Element<_> = column(vec![self.render_content_widget(&widget.content.content, true)]) - .height(130) // TODO dynamic height + let height = match grid_width { + ..4 => 130, + 4 => 150, + 5 => 130, + 6 => 110, + 7 => 90, + 8 => 70, + 8.. => 50, + }; + + let content: Element<_> = container(self.render_content_widget(&widget.content.content, true)) + .height(height) .into(); let style = match item_focus_index { @@ -1725,6 +1766,7 @@ impl<'b> ComponentWidgets<'b> { item_focus_index: Option, index_counter: &Cell ) -> Element<'a, ComponentWidgetEvent> { + // TODO // let (width, height) = match aspect_ratio { // None => (1, 1), // Some("1") => (1, 1), @@ -1741,7 +1783,7 @@ impl<'b> ComponentWidgets<'b> { let rows: Vec> = items .iter() - .map(|widget| self.render_grid_item_widget(widget, item_focus_index, index_counter)) + .map(|widget| self.render_grid_item_widget(widget, item_focus_index, index_counter, grid_width)) .chunks(grid_width) .into_iter() .map(|row_items| { From 058bcde9d203af9dd129b14c49e10ac504d04063 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:09:32 +0100 Subject: [PATCH 171/540] Distinct styling for pressed state of buttons --- rust/client/src/ui/theme/button.rs | 44 +++++++++++++++++++----------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/rust/client/src/ui/theme/button.rs b/rust/client/src/ui/theme/button.rs index f35c17a..49e582f 100644 --- a/rust/client/src/ui/theme/button.rs +++ b/rust/client/src/ui/theme/button.rs @@ -28,7 +28,8 @@ pub enum ButtonStyle { enum ButtonState { Active, - Hover + Hover, + Pressed, } impl ButtonStyle { @@ -88,61 +89,61 @@ impl ButtonStyle { } fn appearance(&self, theme: &GauntletTheme, state: ButtonState) -> Appearance { - let (background_color, background_color_hover, text_color, text_color_hover, border_radius, border_width, border_color) = match &self { + let (background_color, background_color_hover, background_color_pressed, text_color, text_color_hover, border_radius, border_width, border_color) = match &self { ButtonStyle::RootBottomPanelPrimaryActionButton | ButtonStyle::RootBottomPanelActionToggleButton => { let theme = &theme.root_bottom_panel_action_toggle_button; - (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) }, ButtonStyle::RootTopPanelBackButton => { let theme = &theme.root_top_panel_button; - (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) }, ButtonStyle::GridItem => { let theme = &theme.grid_item; - (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::GridItemFocused => { let theme = &theme.grid_item; - (Some(&theme.background_color_focused), Some(&theme.background_color_focused), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color_focused), Some(&theme.background_color_focused), Some(&theme.background_color), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::Action => { let theme = &theme.action; - (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::ActionFocused => { let theme = &theme.action; - (Some(&theme.background_color_focused), Some(&theme.background_color_focused), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color_focused), Some(&theme.background_color_focused), Some(&theme.background_color), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::ListItem => { let theme = &theme.list_item; - (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::ListItemFocused => { let theme = &theme.list_item; - (Some(&theme.background_color_focused), Some(&theme.background_color_focused), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color_focused), Some(&theme.background_color_focused), Some(&theme.background_color), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::MainListItem => { let theme = &theme.main_list_item; - (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::MainListItemFocused => { let theme = &theme.main_list_item; - (Some(&theme.background_color_focused), Some(&theme.background_color_focused), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color_focused), Some(&theme.background_color_focused), Some(&theme.background_color), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::MetadataLink => { let theme = &theme.metadata_link; - (None, None, &theme.text_color, &theme.text_color_hovered, &0.0, &1.0, &TRANSPARENT) + (None, None, None, &theme.text_color, &theme.text_color_hovered, &0.0, &1.0, &TRANSPARENT) } ButtonStyle::MetadataTagItem => { let theme = &theme.metadata_tag_item_button; - (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::ShouldNotBeUsed => { - (Some(&NOT_INTENDED_TO_BE_USED), Some(&NOT_INTENDED_TO_BE_USED), &NOT_INTENDED_TO_BE_USED, &NOT_INTENDED_TO_BE_USED, &0.0, &1.0, &TRANSPARENT) + (Some(&NOT_INTENDED_TO_BE_USED), Some(&NOT_INTENDED_TO_BE_USED), Some(&NOT_INTENDED_TO_BE_USED), &NOT_INTENDED_TO_BE_USED, &NOT_INTENDED_TO_BE_USED, &0.0, &1.0, &TRANSPARENT) } ButtonStyle::DatePicker => { let theme = &theme.form_input_date_picker_buttons; - (Some(&theme.background_color), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } }; @@ -159,6 +160,13 @@ impl ButtonStyle { match state { ButtonState::Active => active, + ButtonState::Pressed => { + Appearance { + background: background_color_pressed.map(|color| color.to_iced().into()), + text_color: text_color_hover.to_iced(), + ..active + } + } ButtonState::Hover => { Appearance { background: background_color_hover.map(|color| color.to_iced().into()), @@ -180,6 +188,10 @@ impl button::StyleSheet for GauntletTheme { fn hovered(&self, style: &Self::Style) -> Appearance { style.appearance(self, ButtonState::Hover) } + + fn pressed(&self, style: &Self::Style) -> Appearance { + style.appearance(self, ButtonState::Pressed) + } } impl<'a, Message: 'a + Clone> ThemableWidget<'a, Message> for Button<'a, Message, GauntletTheme, Renderer> { From 201fea1446e006cb067fbdf57bfa419f58d41802 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:37:34 +0100 Subject: [PATCH 172/540] Autofocus searchbar in grid and list when opening the plugin view --- rust/client/src/ui/client_context.rs | 14 ++- rust/client/src/ui/mod.rs | 85 ++++++++++---- rust/client/src/ui/widget.rs | 151 +++++++++++++++++++++++-- rust/client/src/ui/widget_container.rs | 35 +++++- 4 files changed, 249 insertions(+), 36 deletions(-) diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index a517963..ecbb9b7 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -84,7 +84,7 @@ impl ClientContext { plugin_name: &str, entrypoint_id: &EntrypointId, entrypoint_name: &str - ) { + ) -> AppMsg { match render_location { UiRenderLocation::InlineView => self.get_mut_inline_view_container(plugin_id).replace_view(container, images, plugin_id, plugin_name, entrypoint_id, entrypoint_name), UiRenderLocation::View => self.get_mut_view_container().replace_view(container, images, plugin_id, plugin_name, entrypoint_id, entrypoint_name) @@ -112,6 +112,18 @@ impl ClientContext { } } + pub fn append_text(&self, text: &str) -> Command { + self.view.append_text(text) + } + + pub fn backspace_text(&self) -> Command { + self.view.backspace_text() + } + + pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Command { + self.view.focus_search_bar(widget_id) + } + pub fn toggle_action_panel(&self) { self.view.toggle_action_panel() } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index a8a461f..31ac186 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -188,6 +188,9 @@ pub enum AppMsg { }, PendingPluginViewLoadingBar, ShowPluginViewLoadingBar, + FocusPluginViewSearchBar { + widget_id: UiWidgetId + }, } pub struct AppFlags { @@ -350,7 +353,8 @@ impl Application for AppModel { let container = RootWidget::deserialize(container).expect("should always be valid"); let has_children = container.content.is_some(); - context.replace_view( + // ignore commands because screenshots are non-interactive + let _ = context.replace_view( render_location, container, images, @@ -596,7 +600,16 @@ impl Application for AppModel { } } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { .. } => Command::none() + GlobalState::PluginView { sub_state, .. } => { + match sub_state { + PluginViewState::None => { + let mut client_context = self.client_context.write().expect("lock is poisoned"); + + client_context.backspace_text() + } + PluginViewState::ActionPanel { .. } => Command::none() + } + } } }, _ => { @@ -695,7 +708,7 @@ impl Application for AppModel { } } GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { .. } => { + GlobalState::PluginView { sub_state, .. } => { match physical_key_model(physical_key, modifiers) { Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) @@ -704,7 +717,19 @@ impl Application for AppModel { if modifier_shift || modifier_control || modifier_alt || modifier_meta { self.handle_plugin_view_keyboard_event(physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) } else { - Command::none() + match sub_state { + PluginViewState::None => { + match text { + None => Command::none(), + Some(text) => { + let mut client_context = self.client_context.write().expect("lock is poisoned"); + + client_context.append_text(text.as_str()) + } + } + } + PluginViewState::ActionPanel { .. } => Command::none() + } } } _ => Command::none() @@ -1193,6 +1218,11 @@ impl Application for AppModel { Command::none() } + AppMsg::FocusPluginViewSearchBar { widget_id } => { + let mut client_context = self.client_context.write().expect("lock is poisoned"); + + client_context.focus_search_bar(widget_id) + } } } @@ -2039,7 +2069,7 @@ async fn request_loop( loop { let (request_data, responder) = frontend_receiver.recv().await; - let app_msg = { + let app_msgs = { let mut client_context = client_context.write().expect("lock is poisoned"); match request_data { @@ -2057,7 +2087,7 @@ async fn request_loop( } => { let has_children = container.content.is_some(); - client_context.replace_view( + let message = client_context.replace_view( render_location, container, images, @@ -2069,23 +2099,26 @@ async fn request_loop( responder.respond(UiResponseData::Nothing); - AppMsg::ReplaceView { - top_level_view, - has_children, - render_location - } + vec![ + AppMsg::ReplaceView { + top_level_view, + has_children, + render_location, + }, + message + ] } UiRequestData::ClearInlineView { plugin_id } => { client_context.clear_inline_view(&plugin_id); responder.respond(UiResponseData::Nothing); - AppMsg::Noop // refresh ui + vec![AppMsg::Noop] // refresh ui } UiRequestData::ShowWindow => { responder.respond(UiResponseData::Nothing); - AppMsg::ShowWindow + vec![AppMsg::ShowWindow] } UiRequestData::ShowPreferenceRequiredView { plugin_id, @@ -2095,52 +2128,54 @@ async fn request_loop( } => { responder.respond(UiResponseData::Nothing); - AppMsg::ShowPreferenceRequiredView { + vec![AppMsg::ShowPreferenceRequiredView { plugin_id, entrypoint_id, plugin_preferences_required, entrypoint_preferences_required - } + }] } UiRequestData::ShowPluginErrorView { plugin_id, entrypoint_id, render_location } => { responder.respond(UiResponseData::Nothing); - AppMsg::ShowPluginErrorView { + vec![AppMsg::ShowPluginErrorView { plugin_id, entrypoint_id, render_location, - } + }] } UiRequestData::RequestSearchResultUpdate => { responder.respond(UiResponseData::Nothing); - AppMsg::UpdateSearchResults + vec![AppMsg::UpdateSearchResults] } UiRequestData::ShowHud { display } => { responder.respond(UiResponseData::Nothing); - AppMsg::ShowHud { + vec![AppMsg::ShowHud { display - } + }] } UiRequestData::SetGlobalShortcut { shortcut } => { - AppMsg::SetGlobalShortcut { + vec![AppMsg::SetGlobalShortcut { shortcut, responder: Arc::new(Mutex::new(Some(responder))) - } + }] } UiRequestData::UpdateLoadingBar { plugin_id, entrypoint_id, show } => { responder.respond(UiResponseData::Nothing); - AppMsg::UpdateLoadingBar { + vec![AppMsg::UpdateLoadingBar { plugin_id, entrypoint_id, show - } + }] } } }; - let _ = sender.send(app_msg).await; + for app_msg in app_msgs { + let _ = sender.send(app_msg).await; + } } } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 35b4699..3177379 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -2,7 +2,6 @@ use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMem use common_ui::shortcut_to_text; use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; -use iced::futures::StreamExt; use iced::widget::image::Handle; use iced::widget::text::Shaping; use iced::widget::tooltip::Position; @@ -17,7 +16,6 @@ use itertools::Itertools; use std::cell::Cell; use std::collections::HashMap; use std::fmt::{Debug, Display}; - use crate::model::UiViewEvent; use crate::ui::custom_widgets::loading_bar::LoadingBar; use crate::ui::grid_navigation::{grid_down_offset, grid_up_offset, GridSectionData}; @@ -66,6 +64,19 @@ impl<'b> ComponentWidgets<'b> { } } + fn text_field_state_mut(&mut self, widget_id: UiWidgetId) -> &mut TextFieldState { + Self::text_field_state_mut_on_state(&mut self.state, widget_id) + } + + fn text_field_state_mut_on_state(state: &mut HashMap, widget_id: UiWidgetId) -> &mut TextFieldState { + let state = state.get_mut(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + + match state { + ComponentWidgetState::TextField(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state) + } + } + fn checkbox_state(&self, widget_id: UiWidgetId) -> &CheckboxState { let state = self.state.get(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); @@ -213,6 +224,7 @@ pub enum ComponentWidgetState { #[derive(Debug, Clone)] struct TextFieldState { + text_input_id: text_input::Id, state_value: String } @@ -242,12 +254,13 @@ impl ComponentWidgetState { fn root(item_height: f32, rows_per_view: usize) -> ComponentWidgetState { ComponentWidgetState::Root(RootState { show_action_panel: false, - focused_item: ScrollHandle::new(false, item_height, rows_per_view), // TODO first focused? + focused_item: ScrollHandle::new(false, item_height, rows_per_view), }) } fn text_field(value: &Option) -> ComponentWidgetState { ComponentWidgetState::TextField(TextFieldState { + text_input_id: text_input::Id::unique(), state_value: value.to_owned().unwrap_or_default() }) } @@ -426,6 +439,124 @@ impl<'b> ComponentWidgets<'b> { amount_per_section } + pub fn append_text(&mut self, text: &str) -> Command { + let Some(root_widget) = &self.root_widget else { + return Command::none(); + }; + + let Some(content) = &root_widget.content else { + return Command::none(); + }; + + let widget_id = match content { + RootWidgetMembers::List(widget) => { + match &widget.content.search_bar { + None => { + return Command::none() + } + Some(widget) => widget.__id__ + } + } + RootWidgetMembers::Grid(widget) => { + match &widget.content.search_bar { + None => { + return Command::none() + } + Some(widget) => widget.__id__ + } + } + _ => return Command::none() + }; + + let TextFieldState { text_input_id, state_value } = ComponentWidgets::text_field_state_mut_on_state(&mut self.state, widget_id); + + if let Some(value) = text.chars().next().filter(|c| !c.is_control()) { + *state_value = format!("{}{}", state_value, value); + + text_input::focus(text_input_id.clone()) + } else { + Command::none() + } + } + + pub fn backspace_text(&mut self) -> Command { + let Some(root_widget) = &self.root_widget else { + return Command::none(); + }; + + let Some(content) = &root_widget.content else { + return Command::none(); + }; + + let widget_id = match content { + RootWidgetMembers::List(widget) => { + match &widget.content.search_bar { + None => { + return Command::none() + } + Some(widget) => widget.__id__ + } + } + RootWidgetMembers::Grid(widget) => { + match &widget.content.search_bar { + None => { + return Command::none() + } + Some(widget) => widget.__id__ + } + } + _ => return Command::none() + }; + + let TextFieldState { text_input_id, state_value } = ComponentWidgets::text_field_state_mut_on_state(&mut self.state, widget_id); + + let mut chars = state_value.chars(); + chars.next_back(); + *state_value = chars.as_str().to_owned(); + + text_input::focus(text_input_id.clone()) + } + + pub fn first_open(&self) -> AppMsg { + let Some(root_widget) = &self.root_widget else { + return AppMsg::Noop; + }; + + let Some(content) = &root_widget.content else { + return AppMsg::Noop; + }; + + let widget_id = match content { + RootWidgetMembers::List(widget) => { + match &widget.content.search_bar { + None => { + return AppMsg::Noop + } + Some(widget) => widget.__id__ + } + } + RootWidgetMembers::Grid(widget) => { + match &widget.content.search_bar { + None => { + return AppMsg::Noop + } + Some(widget) => widget.__id__ + } + } + _ => return AppMsg::Noop + }; + + AppMsg::FocusPluginViewSearchBar { + widget_id + } + } + + pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Command { + let TextFieldState { text_input_id, .. } = self.text_field_state(widget_id); + + text_input::focus(text_input_id.clone()) + } + pub fn focus_up(&mut self) -> Command { let Some(root_widget) = &self.root_widget else { return Command::none(); @@ -1017,7 +1148,7 @@ impl<'b> ComponentWidgets<'b> { fn render_text_field_widget<'a>(&self, widget: &TextFieldWidget) -> Element<'a, ComponentWidgetEvent> { let widget_id = widget.__id__; - let TextFieldState { state_value } = self.text_field_state(widget.__id__); + let TextFieldState { state_value, .. } = self.text_field_state(widget.__id__); text_input("", state_value) .on_input(move |value| ComponentWidgetEvent::OnChangeTextField { widget_id, value }) @@ -1026,7 +1157,7 @@ impl<'b> ComponentWidgets<'b> { fn render_password_field_widget<'a>(&self, widget: &PasswordFieldWidget) -> Element<'a, ComponentWidgetEvent> { let widget_id = widget.__id__; - let TextFieldState { state_value } = self.text_field_state(widget_id); + let TextFieldState { state_value, .. } = self.text_field_state(widget_id); text_input("", state_value) .secure(true) @@ -1309,9 +1440,11 @@ impl<'b> ComponentWidgets<'b> { fn render_search_bar_widget<'a>(&self, widget: &SearchBarWidget) -> Element<'a, ComponentWidgetEvent> { let widget_id = widget.__id__; - let TextFieldState { state_value } = self.text_field_state(widget_id); + let TextFieldState { state_value, text_input_id } = self.text_field_state(widget_id); text_input(widget.placeholder.as_deref().unwrap_or_default(), state_value) + .id(text_input_id.clone()) + .ignore_with_modifiers(true) .on_input(move |value| ComponentWidgetEvent::OnChangeSearchBar { widget_id, value }) .themed(TextInputStyle::PluginSearchBar) } @@ -2561,7 +2694,7 @@ impl ComponentWidgetEvent { } ComponentWidgetEvent::OnChangeTextField { widget_id, value } => { { - let ComponentWidgetState::TextField(TextFieldState { state_value }) = state else { + let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; @@ -2572,7 +2705,7 @@ impl ComponentWidgetEvent { } ComponentWidgetEvent::OnChangePasswordField { widget_id, value } => { { - let ComponentWidgetState::TextField(TextFieldState { state_value }) = state else { + let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; @@ -2583,7 +2716,7 @@ impl ComponentWidgetEvent { } ComponentWidgetEvent::OnChangeSearchBar { widget_id, value } => { { - let ComponentWidgetState::TextField(TextFieldState { state_value }) = state else { + let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 8a8be43..c4b8b9f 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -51,7 +51,7 @@ impl PluginWidgetContainer { plugin_name: &str, entrypoint_id: &EntrypointId, entrypoint_name: &str - ) { + ) -> AppMsg { tracing::trace!("replace_view is called. container: {:?}", container); self.plugin_id = Some(plugin_id.clone()); @@ -76,7 +76,19 @@ impl PluginWidgetContainer { } } + let first_open = match root_widget.as_ref() { + None => true, + Some(root_widget) => root_widget.content.is_none() + }; + *root_widget = Some(container); + + if first_open { + ComponentWidgets::new(&mut root_widget, &mut state, &self.images) + .first_open() + } else { + AppMsg::Noop + } } pub fn handle_event(&self, plugin_id: PluginId, event: ComponentWidgetEvent) -> Option { @@ -107,6 +119,27 @@ impl PluginWidgetContainer { .render_root_inline_widget(self.plugin_name.as_ref(), self.entrypoint_name.as_ref()) } + pub fn append_text(&self, text: &str) -> Command { + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).append_text(text) + } + + pub fn backspace_text(&self) -> Command { + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).backspace_text() + } + + pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Command { + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_search_bar(widget_id) + } + pub fn toggle_action_panel(&self) { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); From 6a56604e898768adb02fb6d30232f4f44a3506a1 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:13:59 +0100 Subject: [PATCH 173/540] Fix padding of grid and list section being too far if it is first in the view --- rust/client/src/ui/theme/row.rs | 14 +++++++++++++- rust/client/src/ui/widget.rs | 26 ++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/rust/client/src/ui/theme/row.rs b/rust/client/src/ui/theme/row.rs index 12ad44a..d20f404 100644 --- a/rust/client/src/ui/theme/row.rs +++ b/rust/client/src/ui/theme/row.rs @@ -1,11 +1,13 @@ use crate::ui::theme::{get_theme, Element, GauntletTheme, ThemableWidget}; use iced::widget::Row; -use iced::Renderer; +use iced::{Padding, Renderer}; pub enum RowStyle { ActionShortcut, FormInput, + ListFirstSectionTitle, ListSectionTitle, + GridFirstSectionTitle, GridSectionTitle, GridItemTitle, RootBottomPanel, @@ -33,6 +35,16 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletT self.padding(theme.grid_section_title.padding.to_iced()) .spacing(theme.grid_section_title.spacing) } + RowStyle::ListFirstSectionTitle => { + let padding = theme.list_section_title.padding.to_iced(); + self.padding(Padding::from([padding.bottom, padding.right, padding.bottom, padding.left])) + .spacing(theme.list_section_title.spacing) + } + RowStyle::GridFirstSectionTitle => { + let padding = theme.grid_section_title.padding.to_iced(); + self.padding(Padding::from([0.0, padding.right, padding.bottom, padding.left])) + .spacing(theme.grid_section_title.spacing) + } RowStyle::GridItemTitle => { self.padding(theme.grid_item_title.padding.to_iced()) } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 3177379..3f4e0bb 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1525,10 +1525,12 @@ impl<'b> ComponentWidgets<'b> { let mut pending: Vec<&ListItemWidget> = vec![]; let mut items: Vec> = vec![]; let index_counter = &Cell::new(0); + let mut first_section = true; for members in &list_widget.content.ordered_members { match &members { ListWidgetOrderedMembers::ListItem(widget) => { + first_section = false; pending.push(widget) }, ListWidgetOrderedMembers::ListSection(widget) => { @@ -1546,7 +1548,9 @@ impl<'b> ComponentWidgets<'b> { pending = vec![]; } - items.push(self.render_list_section_widget(widget, focused_item.index, index_counter)) + items.push(self.render_list_section_widget(widget, focused_item.index, index_counter, first_section)); + + first_section = false; }, } } @@ -1627,7 +1631,8 @@ impl<'b> ComponentWidgets<'b> { &self, widget: &ListSectionWidget, item_focus_index: Option, - index_counter: &Cell + index_counter: &Cell, + first_section: bool, ) -> Element<'a, ComponentWidgetEvent> { let content: Vec<_> = widget.content.ordered_members .iter() @@ -1641,7 +1646,9 @@ impl<'b> ComponentWidgets<'b> { let content = column(content) .into(); - render_section(content, Some(&widget.title), &widget.subtitle, RowStyle::ListSectionTitle, TextStyle::ListSectionTitle, TextStyle::ListSectionSubtitle) + let section_title_style = if first_section { RowStyle::ListFirstSectionTitle } else { RowStyle::ListSectionTitle }; + + render_section(content, Some(&widget.title), &widget.subtitle, section_title_style, TextStyle::ListSectionTitle, TextStyle::ListSectionSubtitle) } fn render_list_item_widget<'a>( @@ -1735,10 +1742,12 @@ impl<'b> ComponentWidgets<'b> { let mut pending: Vec<&GridItemWidget> = vec![]; let mut items: Vec> = vec![]; let index_counter = &Cell::new(0); + let mut first_section = true; for members in &grid_widget.content.ordered_members { match &members { GridWidgetOrderedMembers::GridItem(widget) => { + first_section = false; pending.push(widget) } GridWidgetOrderedMembers::GridSection(widget) => { @@ -1750,7 +1759,9 @@ impl<'b> ComponentWidgets<'b> { pending = vec![]; } - items.push(self.render_grid_section_widget(widget, focused_item.index, index_counter)) + items.push(self.render_grid_section_widget(widget, focused_item.index, index_counter, first_section)); + + first_section = false; } } } @@ -1794,7 +1805,8 @@ impl<'b> ComponentWidgets<'b> { &self, widget: &GridSectionWidget, item_focus_index: Option, - index_counter: &Cell + index_counter: &Cell, + first_section: bool ) -> Element<'a, ComponentWidgetEvent> { let items: Vec<_> = widget.content.ordered_members .iter() @@ -1807,7 +1819,9 @@ impl<'b> ComponentWidgets<'b> { let content = self.render_grid(&items, &widget.columns, item_focus_index, index_counter); - render_section(content, Some(&widget.title), &widget.subtitle, RowStyle::GridSectionTitle, TextStyle::GridSectionTitle, TextStyle::GridSectionSubtitle) + let section_title_style = if first_section { RowStyle::GridFirstSectionTitle } else { RowStyle::GridSectionTitle }; + + render_section(content, Some(&widget.title), &widget.subtitle, section_title_style, TextStyle::GridSectionTitle, TextStyle::GridSectionSubtitle) } fn render_grid_item_widget<'a>( From 1ca022ebd0033f8c942a56a6d3cc188874b31a41 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 14 Nov 2024 17:20:08 +0100 Subject: [PATCH 174/540] Fix button clicking events not working after react renderer parser refactor --- rust/client/src/ui/widget.rs | 18 +++++++++++++++++- rust/client/src/ui/widget_container.rs | 6 +++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 3f4e0bb..2be77f2 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -2644,7 +2644,7 @@ pub enum ComponentWidgetEvent { include!(concat!(env!("OUT_DIR"), "/components.rs")); impl ComponentWidgetEvent { - pub fn handle(self, _plugin_id: PluginId, state: &mut ComponentWidgetState) -> Option { + pub fn handle(self, _plugin_id: PluginId, state: Option<&mut ComponentWidgetState>) -> Option { match self { ComponentWidgetEvent::LinkClick { widget_id: _, href } => { Some(UiViewEvent::Open { @@ -2658,6 +2658,8 @@ impl ComponentWidgetEvent { Some(create_action_on_action_event(widget_id)) } ComponentWidgetEvent::ToggleDatePicker { widget_id } => { + let state = state.expect("state should always exist for "); + let ComponentWidgetState::DatePicker(DatePickerState { state_value: _, show_picker }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; @@ -2666,6 +2668,8 @@ impl ComponentWidgetEvent { None } ComponentWidgetEvent::CancelDatePicker { widget_id } => { + let state = state.expect("state should always exist for "); + let ComponentWidgetState::DatePicker(DatePickerState { state_value: _, show_picker }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; @@ -2674,6 +2678,8 @@ impl ComponentWidgetEvent { None } ComponentWidgetEvent::SubmitDatePicker { widget_id, value } => { + let state = state.expect("state should always exist for "); + { let ComponentWidgetState::DatePicker(DatePickerState { state_value: _, show_picker }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) @@ -2685,6 +2691,8 @@ impl ComponentWidgetEvent { Some(create_date_picker_on_change_event(widget_id, Some(value))) } ComponentWidgetEvent::ToggleCheckbox { widget_id, value } => { + let state = state.expect("state should always exist for "); + { let ComponentWidgetState::Checkbox(CheckboxState { state_value }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) @@ -2696,6 +2704,8 @@ impl ComponentWidgetEvent { Some(create_checkbox_on_change_event(widget_id, value)) } ComponentWidgetEvent::SelectPickList { widget_id, value } => { + let state = state.expect("state should always exist for "); + { let ComponentWidgetState::Select(SelectState { state_value }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) @@ -2707,6 +2717,8 @@ impl ComponentWidgetEvent { Some(create_select_on_change_event(widget_id, Some(value))) } ComponentWidgetEvent::OnChangeTextField { widget_id, value } => { + let state = state.expect("state should always exist for "); + { let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) @@ -2718,6 +2730,8 @@ impl ComponentWidgetEvent { Some(create_text_field_on_change_event(widget_id, Some(value))) } ComponentWidgetEvent::OnChangePasswordField { widget_id, value } => { + let state = state.expect("state should always exist for "); + { let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) @@ -2729,6 +2743,8 @@ impl ComponentWidgetEvent { Some(create_password_field_on_change_event(widget_id, Some(value))) } ComponentWidgetEvent::OnChangeSearchBar { widget_id, value } => { + let state = state.expect("state should always exist for "); + { let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index c4b8b9f..bb2ec12 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -94,9 +94,9 @@ impl PluginWidgetContainer { pub fn handle_event(&self, plugin_id: PluginId, event: ComponentWidgetEvent) -> Option { let mut state = self.state.lock().expect("lock is poisoned"); - // TODO for events like typing, synchronous render should be implemented instead of ignoring events - state.get_mut(&event.widget_id()) - .and_then(|state| event.handle(plugin_id, state)) + let widget_id = event.widget_id(); + + event.handle(plugin_id, state.get_mut(&widget_id)) } pub fn render_root_widget<'a>( From ca609d58e3952bf93514b29d86f1dfa30e3ecc16 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 14 Nov 2024 17:42:20 +0100 Subject: [PATCH 175/540] Fix inability to move left-right on the grid view because search bar text input is always focused --- rust/client/src/ui/widget.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 2be77f2..ccf62dd 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -647,9 +647,20 @@ impl<'b> ComponentWidgets<'b> { .sum(); let Some(current_index) = &focused_item.index else { + let unfocus = match &grid_widget.content.search_bar { + None => Command::none(), + Some(_) => { + // there doesn't seem to be an unfocus command but focusing non-existing input will unfocus all + text_input::focus(text_input::Id::unique()) + } + }; + let _ = focused_item.focus_next(total); - return focused_item.scroll_to(0) + return Command::batch([ + unfocus, + focused_item.scroll_to(0) + ]) }; match grid_down_offset(*current_index, amount_per_section_total) { From 8dae5cb16cf415000a27704332b0c46ecb7c0ae2 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:01:30 +0100 Subject: [PATCH 176/540] Add "Indexing..." text on main view while load bar for currently running command generators is shown --- rust/client/src/ui/mod.rs | 9 +++++++++ rust/client/src/ui/widget.rs | 23 ++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 31ac186..977c2d3 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -1623,12 +1623,19 @@ impl Application for AppModel { } }; + let toast_text = if !self.loading_bar_state.is_empty() { + Some("Indexing...") + } else { + None + }; + let root = match sub_state { MainViewState::None => { render_root( false, input, separator, + toast_text, content, primary_action, action_panel, @@ -1645,6 +1652,7 @@ impl Application for AppModel { true, input, separator, + toast_text, content, primary_action, action_panel, @@ -1661,6 +1669,7 @@ impl Application for AppModel { true, input, separator, + toast_text, content, primary_action, action_panel, diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index ccf62dd..fd5365d 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -2030,6 +2030,7 @@ impl<'b> ComponentWidgets<'b> { show_action_panel, top_panel, top_separator, + None, content, primary_action, action_panel, @@ -2046,6 +2047,7 @@ impl<'b> ComponentWidgets<'b> { show_action_panel, top_panel, top_separator, + None, content, primary_action, action_panel, @@ -2409,6 +2411,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( show_action_panel: bool, top_panel: Element<'a, T>, top_separator: Element<'a, T>, + toast_text: Option<&str>, content: Element<'a, T>, primary_action: Option<(String, UiWidgetId, PhysicalShortcut)>, action_panel: Option, @@ -2469,6 +2472,13 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( let mut bottom_panel_content = vec![entrypoint_name]; + if let Some(toast_text) = toast_text { + let toast_text = text(toast_text) + .into(); + + bottom_panel_content.push(toast_text); + } + let space = horizontal_space() .into(); @@ -2509,7 +2519,18 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( let space: Element<_> = Space::new(Length::Fill, panel_height) .into(); - let mut bottom_panel_content = vec![entrypoint_name, space]; + let mut bottom_panel_content = vec![]; + + if let Some(toast_text) = toast_text { + let toast_text = text(toast_text) + .into(); + + bottom_panel_content.push(toast_text); + } else { + bottom_panel_content.push(entrypoint_name); + } + + bottom_panel_content.push(space); if let Some(primary_action) = primary_action { bottom_panel_content.push(primary_action); From bd76aa50991d2a77da6562ba8a59e32067dbf537 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:08:01 +0100 Subject: [PATCH 177/540] Ability to unset global shortcut, better error reporting for setting global shortcut --- rust/cli/src/lib.rs | 1 - rust/client/src/ui/mod.rs | 11 +- rust/common/src/model.rs | 2 +- rust/common/src/rpc/backend_api.rs | 40 ++++--- rust/common/src/rpc/backend_server.rs | 49 ++++---- rust/common/src/rpc/frontend_api.rs | 2 +- rust/common_ui/src/lib.rs | 10 +- .../src/components/shortcut_selector.rs | 86 ++++++++++---- rust/management_client/src/ui.rs | 2 +- rust/management_client/src/views/general.rs | 107 +++++++++++++----- rust/server/src/plugins/data_db_repository.rs | 80 +++++++------ rust/server/src/plugins/mod.rs | 50 +++++--- rust/server/src/rpc.rs | 7 +- schema/backend.proto | 13 ++- 14 files changed, 304 insertions(+), 156 deletions(-) diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs index 56e5a85..df62970 100644 --- a/rust/cli/src/lib.rs +++ b/rust/cli/src/lib.rs @@ -1,4 +1,3 @@ -use anyhow::{anyhow, Context}; use clap::Parser; use client::{generate_color_theme_sample, generate_theme_sample, open_window}; diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 977c2d3..5857f39 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -178,7 +178,7 @@ pub enum AppMsg { ResetMainViewState, OnAnyActionMainViewNoPanelKeyboardAtIndex { index: usize }, SetGlobalShortcut { - shortcut: PhysicalShortcut, + shortcut: Option, responder: Arc>>> }, UpdateLoadingBar { @@ -1162,10 +1162,13 @@ impl Application for AppModel { global_hotkey_manager.unregister(current_hotkey)?; } - let hotkey = convert_physical_shortcut_to_hotkey(shortcut); - *hotkey_guard = Some(hotkey); + if let Some(shortcut) = shortcut { + let hotkey = convert_physical_shortcut_to_hotkey(shortcut); - global_hotkey_manager.register(hotkey)?; + *hotkey_guard = Some(hotkey); + + global_hotkey_manager.register(hotkey)?; + } Ok(()) }; diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 9087d14..7750fae 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -162,7 +162,7 @@ pub enum UiRequestData { show: bool }, SetGlobalShortcut { - shortcut: PhysicalShortcut + shortcut: Option }, } diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index a834feb..9321b5b 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -7,7 +7,7 @@ use tonic::transport::Channel; use utils::channel::{RequestError, RequestSender}; use crate::model::{BackendRequestData, BackendResponseData, DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, UiPropertyValue, UiWidgetId}; -use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadStatus, RpcDownloadStatusRequest, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcPingRequest, RpcPluginsRequest, RpcRemovePluginRequest, RpcSaveLocalPluginRequest, RpcSetEntrypointStateRequest, RpcSetGlobalShortcutRequest, RpcSetPluginStateRequest, RpcSetPreferenceValueRequest, RpcShowSettingsWindowRequest, RpcShowWindowRequest}; +use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadStatus, RpcDownloadStatusRequest, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcPingRequest, RpcPluginsRequest, RpcRemovePluginRequest, RpcSaveLocalPluginRequest, RpcSetEntrypointStateRequest, RpcSetGlobalShortcutRequest, RpcSetPluginStateRequest, RpcSetPreferenceValueRequest, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowWindowRequest}; use crate::rpc::grpc::rpc_backend_client::RpcBackendClient; use crate::rpc::grpc_convert::{plugin_preference_from_rpc, plugin_preference_user_data_from_rpc, plugin_preference_user_data_to_rpc}; @@ -348,13 +348,17 @@ impl BackendApi { Ok(()) } - pub async fn set_global_shortcut(&mut self, shortcut: PhysicalShortcut) -> Result<(), BackendApiError> { + pub async fn set_global_shortcut(&mut self, shortcut: Option) -> Result<(), BackendApiError> { let request = RpcSetGlobalShortcutRequest { - physical_key: shortcut.physical_key.to_value(), - modifier_shift: shortcut.modifier_shift, - modifier_control: shortcut.modifier_control, - modifier_alt: shortcut.modifier_alt, - modifier_meta: shortcut.modifier_meta, + shortcut: shortcut.map(|shortcut| { + RpcShortcut { + physical_key: shortcut.physical_key.to_value(), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + } + }) }; self.client.set_global_shortcut(Request::new(request)) @@ -363,19 +367,25 @@ impl BackendApi { Ok(()) } - pub async fn get_global_shortcut(&mut self) -> Result { + pub async fn get_global_shortcut(&mut self) -> Result<(Option, Option), BackendApiError> { let response = self.client.get_global_shortcut(Request::new(RpcGetGlobalShortcutRequest::default())) .await?; let response = response.into_inner(); - Ok(PhysicalShortcut { - physical_key: PhysicalKey::from_value(response.physical_key), - modifier_shift: response.modifier_shift, - modifier_control: response.modifier_control, - modifier_alt: response.modifier_alt, - modifier_meta: response.modifier_meta, - }) + Ok(( + response.shortcut + .map(|shortcut| { + PhysicalShortcut { + physical_key: PhysicalKey::from_value(shortcut.physical_key), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + } + }), + response.error + )) } pub async fn set_preference_value(&mut self, plugin_id: PluginId, entrypoint_id: Option, id: String, user_data: PluginPreferenceUserData) -> Result<(), BackendApiError> { diff --git a/rust/common/src/rpc/backend_server.rs b/rust/common/src/rpc/backend_server.rs index 997b573..f79c519 100644 --- a/rust/common/src/rpc/backend_server.rs +++ b/rust/common/src/rpc/backend_server.rs @@ -7,7 +7,7 @@ use tonic::{Request, Response, Status}; use tonic::transport::Server; use crate::model::{DownloadStatus, EntrypointId, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SettingsEntrypointType, SettingsPlugin}; -use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadPluginResponse, RpcDownloadStatus, RpcDownloadStatusRequest, RpcDownloadStatusResponse, RpcDownloadStatusValue, RpcEntrypoint, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetGlobalShortcutResponse, RpcPingRequest, RpcPingResponse, RpcPlugin, RpcPluginsRequest, RpcPluginsResponse, RpcRemovePluginRequest, RpcRemovePluginResponse, RpcSaveLocalPluginRequest, RpcSaveLocalPluginResponse, RpcSetEntrypointStateRequest, RpcSetEntrypointStateResponse, RpcSetGlobalShortcutRequest, RpcSetGlobalShortcutResponse, RpcSetPluginStateRequest, RpcSetPluginStateResponse, RpcSetPreferenceValueRequest, RpcSetPreferenceValueResponse, RpcShowSettingsWindowRequest, RpcShowSettingsWindowResponse, RpcShowWindowRequest, RpcShowWindowResponse}; +use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadPluginResponse, RpcDownloadStatus, RpcDownloadStatusRequest, RpcDownloadStatusResponse, RpcDownloadStatusValue, RpcEntrypoint, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetGlobalShortcutResponse, RpcPingRequest, RpcPingResponse, RpcPlugin, RpcPluginsRequest, RpcPluginsResponse, RpcRemovePluginRequest, RpcRemovePluginResponse, RpcSaveLocalPluginRequest, RpcSaveLocalPluginResponse, RpcSetEntrypointStateRequest, RpcSetEntrypointStateResponse, RpcSetGlobalShortcutRequest, RpcSetGlobalShortcutResponse, RpcSetPluginStateRequest, RpcSetPluginStateResponse, RpcSetPreferenceValueRequest, RpcSetPreferenceValueResponse, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowSettingsWindowResponse, RpcShowWindowRequest, RpcShowWindowResponse}; use crate::rpc::grpc::rpc_backend_server::{RpcBackend, RpcBackendServer}; use crate::rpc::grpc_convert::{plugin_preference_to_rpc, plugin_preference_user_data_from_rpc, plugin_preference_user_data_to_rpc}; @@ -68,12 +68,12 @@ pub trait BackendServer { async fn set_global_shortcut( &self, - shortcut: PhysicalShortcut + shortcut: Option ) -> anyhow::Result<()>; async fn get_global_shortcut( &self, - ) -> anyhow::Result; + ) -> anyhow::Result<(Option, Option)>; async fn set_preference_value( &self, @@ -215,19 +215,23 @@ impl RpcBackend for RpcBackendServerImpl { async fn set_global_shortcut(&self, request: Request) -> Result, Status> { let request = request.into_inner(); - let physical_key = request.physical_key; - let modifier_shift = request.modifier_shift; - let modifier_control = request.modifier_control; - let modifier_alt = request.modifier_alt; - let modifier_meta = request.modifier_meta; - let shortcut = PhysicalShortcut { - physical_key: PhysicalKey::from_value(physical_key), - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - }; + let shortcut = request.shortcut + .map(|shortcut| { + let physical_key = shortcut.physical_key; + let modifier_shift = shortcut.modifier_shift; + let modifier_control = shortcut.modifier_control; + let modifier_alt = shortcut.modifier_alt; + let modifier_meta = shortcut.modifier_meta; + + PhysicalShortcut { + physical_key: PhysicalKey::from_value(physical_key), + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + } + }); self.server.set_global_shortcut(shortcut) .await @@ -237,16 +241,19 @@ impl RpcBackend for RpcBackendServerImpl { } async fn get_global_shortcut(&self, _request: Request) -> Result, Status> { - let shortcut = self.server.get_global_shortcut() + let (shortcut, error) = self.server.get_global_shortcut() .await .map_err(|err| Status::internal(format!("{:#}", err)))?; Ok(Response::new(RpcGetGlobalShortcutResponse { - physical_key: shortcut.physical_key.to_value(), - modifier_shift: shortcut.modifier_shift, - modifier_control: shortcut.modifier_control, - modifier_alt: shortcut.modifier_alt, - modifier_meta: shortcut.modifier_meta, + shortcut: shortcut.map(|shortcut| RpcShortcut { + physical_key: shortcut.physical_key.to_value(), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + }), + error, })) } diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index 6b17a4c..2aa901a 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -168,7 +168,7 @@ impl FrontendApi { pub async fn set_global_shortcut( &self, - shortcut: PhysicalShortcut + shortcut: Option ) -> anyhow::Result<()> { let request = UiRequestData::SetGlobalShortcut { shortcut, diff --git a/rust/common_ui/src/lib.rs b/rust/common_ui/src/lib.rs index d12399d..d4414a6 100644 --- a/rust/common_ui/src/lib.rs +++ b/rust/common_ui/src/lib.rs @@ -6,7 +6,15 @@ use iced_aw::core::icons; use common::model::{PhysicalKey, PhysicalShortcut}; -pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>(shortcut: &PhysicalShortcut) -> (Element<'a, Message, Theme>, Option>, Option>, Option>, Option>) { +pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>( + shortcut: &PhysicalShortcut +) -> ( + Element<'a, Message, Theme>, + Option>, + Option>, + Option>, + Option> +) { let (key_name, show_shift) = match shortcut.physical_key { PhysicalKey::Enter => { let key_name = if cfg!(target_os = "macos") { diff --git a/rust/management_client/src/components/shortcut_selector.rs b/rust/management_client/src/components/shortcut_selector.rs index 85307dc..bb08b23 100644 --- a/rust/management_client/src/components/shortcut_selector.rs +++ b/rust/management_client/src/components/shortcut_selector.rs @@ -23,7 +23,8 @@ where style: ::Style, - on_shortcut_captured: Box Message + 'a>, + on_shortcut_captured: Box) -> Message + 'a>, + on_capturing_change: Box Message + 'a>, content: Element<'a, Message, Theme>, } @@ -32,34 +33,46 @@ impl<'a, Message: 'a, Theme> ShortcutSelector<'a, Message, Theme> where Theme: StyleSheet + text::StyleSheet + container::StyleSheet + 'a { - pub fn new(current_shortcut: &PhysicalShortcut, on_shortcut_captured: F, style: ::Style) -> Self + pub fn new( + current_shortcut: &Option, + on_shortcut_captured: F, + on_capturing_change: F2, + style: ::Style + ) -> Self where - F: 'a + Fn(PhysicalShortcut) -> Message, + F: 'a + Fn(Option) -> Message, + F2: 'a + Fn(bool) -> Message, { - - let (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) = shortcut_to_text(current_shortcut); - let mut content: Vec> = vec![]; + if let Some(current_shortcut) = current_shortcut { + let ( + key_name, + alt_modifier_text, + meta_modifier_text, + control_modifier_text, + shift_modifier_text + ) = shortcut_to_text(current_shortcut); - if let Some(meta_modifier_text) = meta_modifier_text { - content.push(meta_modifier_text); + if let Some(meta_modifier_text) = meta_modifier_text { + content.push(meta_modifier_text); + } + + if let Some(control_modifier_text) = control_modifier_text { + content.push(control_modifier_text); + } + + if let Some(shift_modifier_text) = shift_modifier_text { + content.push(shift_modifier_text); + } + + if let Some(alt_modifier_text) = alt_modifier_text { + content.push(alt_modifier_text); + } + + content.push(key_name); } - if let Some(control_modifier_text) = control_modifier_text { - content.push(control_modifier_text); - } - - if let Some(shift_modifier_text) = shift_modifier_text { - content.push(shift_modifier_text); - } - - if let Some(alt_modifier_text) = alt_modifier_text { - content.push(alt_modifier_text); - } - - content.push(key_name); - let content: Element<_, _> = row(content) .spacing(8.0) .into(); @@ -79,6 +92,7 @@ where style, on_shortcut_captured: Box::new(on_shortcut_captured), + on_capturing_change: Box::new(on_capturing_change), content, } @@ -191,20 +205,36 @@ where match event { keyboard::Event::KeyReleased { physical_key, modifiers, .. } => { match physical_key { - keyboard::key::PhysicalKey::Backspace | keyboard::key::PhysicalKey::Escape => { + keyboard::key::PhysicalKey::Backspace => { state.is_capturing = false; + let message = (self.on_capturing_change)(false); + shell.publish(message); + + let message = (self.on_shortcut_captured)(None); + shell.publish(message); + + event::Status::Ignored + } + keyboard::key::PhysicalKey::Escape => { + state.is_capturing = false; + + let message = (self.on_capturing_change)(false); + shell.publish(message); + event::Status::Ignored } _ => { match physical_key_model(physical_key, modifiers) { None => event::Status::Ignored, Some(shortcut) => { - let message = (self.on_shortcut_captured)(shortcut); + state.is_capturing = false; + let message = (self.on_capturing_change)(false); shell.publish(message); - state.is_capturing = false; + let message = (self.on_shortcut_captured)(Some(shortcut)); + shell.publish(message); event::Status::Captured } @@ -225,10 +255,16 @@ where if cursor.is_over(layout.bounds()) { state.is_capturing = true; + let message = (self.on_capturing_change)(true); + shell.publish(message); + event::Status::Captured } else { state.is_capturing = false; + let message = (self.on_capturing_change)(false); + shell.publish(message); + event::Status::Ignored } } diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index bdc1c37..0aac6b6 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -126,7 +126,7 @@ impl Application for ManagementAppModel { None => ManagementAppMsg::General(ManagementAppGeneralMsgIn::Noop), Some(shortcut) => { match shortcut { - Ok(shortcut) => ManagementAppMsg::General(ManagementAppGeneralMsgIn::SetShortcut(shortcut)), + Ok((shortcut, error)) => ManagementAppMsg::General(ManagementAppGeneralMsgIn::RefreshShortcut { shortcut, error }), Err(err) => ManagementAppMsg::HandleBackendError(err) } } diff --git a/rust/management_client/src/views/general.rs b/rust/management_client/src/views/general.rs index a4736f8..570bf5d 100644 --- a/rust/management_client/src/views/general.rs +++ b/rust/management_client/src/views/general.rs @@ -1,23 +1,32 @@ -use iced::alignment::Horizontal; -use iced::widget::{column, container, row, text, Space}; -use iced::{Alignment, Command, Length}; -use iced::widget::text::Shaping; -use common::model::{PhysicalKey, PhysicalShortcut}; -use common::rpc::backend_api::{BackendApi, BackendApiError}; - use crate::components::shortcut_selector::ShortcutSelector; use crate::theme::shortcut_selector::ShortcutSelectorStyle; +use crate::theme::text::TextStyle; use crate::theme::Element; +use common::model::PhysicalShortcut; +use common::rpc::backend_api::{BackendApi, BackendApiError}; +use iced::alignment::Horizontal; +use iced::widget::text::Shaping; +use iced::widget::tooltip::Position; +use iced::widget::{column, container, row, text, tooltip, Space}; +use iced::{alignment, Alignment, Command, Length, Padding}; +use iced_aw::core::icons; +use crate::theme::container::ContainerStyle; pub struct ManagementAppGeneralState { backend_api: Option, - current_shortcut: PhysicalShortcut + current_shortcut: Option, + current_shortcut_error: Option, + currently_capturing: bool } #[derive(Debug, Clone)] pub enum ManagementAppGeneralMsgIn { - ShortcutCaptured(PhysicalShortcut), - SetShortcut(PhysicalShortcut), + ShortcutCaptured(Option), + CapturingChanged(bool), + RefreshShortcut { + shortcut: Option, + error: Option + }, Noop } @@ -29,17 +38,11 @@ pub enum ManagementAppGeneralMsgOut { impl ManagementAppGeneralState { pub fn new(backend_api: Option) -> Self { - let shortcut = PhysicalShortcut { - physical_key: PhysicalKey::Space, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: true, - }; - Self { backend_api, - current_shortcut: shortcut + current_shortcut: None, + current_shortcut_error: None, + currently_capturing: false, } } @@ -67,22 +70,26 @@ impl ManagementAppGeneralState { ManagementAppGeneralMsgIn::Noop => { Command::none() } - ManagementAppGeneralMsgIn::SetShortcut(shortcut) => { - self.current_shortcut = shortcut.clone(); + ManagementAppGeneralMsgIn::RefreshShortcut { shortcut, error } => { + self.current_shortcut = shortcut; + self.current_shortcut_error = error; Command::perform(async move {}, |_| ManagementAppGeneralMsgOut::Noop) } + ManagementAppGeneralMsgIn::CapturingChanged(capturing) => { + self.currently_capturing = capturing; + + Command::none() + } } } pub fn view(&self) -> Element { - let on_shortcut_captured = Box::new(move |value| { - ManagementAppGeneralMsgIn::ShortcutCaptured(value) - }); let shortcut_selector: Element<_> = ShortcutSelector::new( &self.current_shortcut, - on_shortcut_captured, + move |value| { ManagementAppGeneralMsgIn::ShortcutCaptured(value) }, + move |value| { ManagementAppGeneralMsgIn::CapturingChanged(value) }, ShortcutSelectorStyle::Default ).into(); @@ -124,8 +131,54 @@ impl ManagementAppGeneralState { .padding(4) .into(); - let after = Space::with_width(Length::FillPortion(2)) - .into(); + let after = if self.currently_capturing { + let hint1: Element<_> = text("Backspace - Unset Shortcut") + .width(Length::Fill) + .style(TextStyle::Subtitle) + .into(); + + let hint2: Element<_> = text("Escape - Stop Capturing") + .width(Length::Fill) + .style(TextStyle::Subtitle) + .into(); + + column(vec![hint1, hint2]) + .width(Length::FillPortion(3)) + .align_items(Alignment::Center) + .padding(Padding::from([0.0, 8.0])) + .into() + } else { + if let Some(current_shortcut_error) = &self.current_shortcut_error { + let error_icon: Element<_> = text(icons::Bootstrap::ExclamationTriangleFill) + .font(icons::BOOTSTRAP_FONT) + .style(TextStyle::Destructive) + .into(); + + let error_text: Element<_> = text(current_shortcut_error) + .style(TextStyle::Destructive) + .into(); + + let error_text: Element<_> = container(error_text) + .padding(16.0) + .max_width(300) + .style(ContainerStyle::Box) + .into(); + + let tooltip: Element<_> = tooltip(error_icon, error_text, Position::Bottom) + .into(); + + let content = container(tooltip) + .width(Length::FillPortion(3)) + .align_y(alignment::Vertical::Center) + .padding(Padding::from([0.0, 8.0])) + .into(); + + content + } else { + Space::with_width(Length::FillPortion(3)) + .into() + } + }; let content = vec![ label, diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 219305d..fc29f11 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -227,7 +227,11 @@ pub struct DbSettingsGlobalShortcutData { pub modifier_shift: bool, pub modifier_control: bool, pub modifier_alt: bool, - pub modifier_meta: bool + pub modifier_meta: bool, + #[serde(default)] + pub unset: bool, + #[serde(default)] + pub error: Option } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -831,7 +835,7 @@ impl DataDbRepository { Ok(()) } - pub async fn set_global_shortcut(&self, shortcut: PhysicalShortcut) -> anyhow::Result<()> { + pub async fn set_global_shortcut(&self, shortcut: Option, error: Option) -> anyhow::Result<()> { // language=SQLite let sql = r#" INSERT INTO settings_data (id, global_shortcut) @@ -842,12 +846,29 @@ impl DataDbRepository { let id = "settings_data"; // only one row in the table - let shortcut_data = DbSettingsGlobalShortcutData { - physical_key: shortcut.physical_key.to_value(), - modifier_shift: shortcut.modifier_shift, - modifier_control: shortcut.modifier_control, - modifier_alt: shortcut.modifier_alt, - modifier_meta: shortcut.modifier_meta, + let shortcut_data = match shortcut { + None => { + DbSettingsGlobalShortcutData { + physical_key: "".to_string(), + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + unset: true, + error, + } + } + Some(shortcut) => { + DbSettingsGlobalShortcutData { + physical_key: shortcut.physical_key.to_value(), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + unset: false, + error, + } + } }; sqlx::query(sql) @@ -859,7 +880,7 @@ impl DataDbRepository { Ok(()) } - pub async fn get_global_shortcut(&self) -> anyhow::Result { + pub async fn get_global_shortcut(&self) -> anyhow::Result, Option)>> { // language=SQLite let data = sqlx::query_as::<_, DbSettingsData>("SELECT * FROM settings_data") .fetch_optional(&self.pool) @@ -869,33 +890,24 @@ impl DataDbRepository { Ok(Some(data)) => { let shortcut_data = data.global_shortcut; - Ok(PhysicalShortcut { - physical_key: PhysicalKey::from_value(shortcut_data.physical_key), - modifier_shift: shortcut_data.modifier_shift, - modifier_control: shortcut_data.modifier_control, - modifier_alt: shortcut_data.modifier_alt, - modifier_meta: shortcut_data.modifier_meta, - }) - }, - Ok(None) => { - if cfg!(target_os = "windows") { - Ok(PhysicalShortcut { - physical_key: PhysicalKey::Space, - modifier_shift: false, - modifier_control: false, - modifier_alt: true, - modifier_meta: false, - }) + let shortcut = if shortcut_data.unset { + None } else { - Ok(PhysicalShortcut { - physical_key: PhysicalKey::Space, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: true, + Some(PhysicalShortcut { + physical_key: PhysicalKey::from_value(shortcut_data.physical_key), + modifier_shift: shortcut_data.modifier_shift, + modifier_control: shortcut_data.modifier_control, + modifier_alt: shortcut_data.modifier_alt, + modifier_meta: shortcut_data.modifier_meta, }) - } - } + }; + + Ok(Some(( + shortcut, + shortcut_data.error, + ))) + }, + Ok(None) => Ok(None), Err(err) => Err(anyhow!("Unable to get global shortcut from db: {:?}", err)) } } diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 1215013..3405965 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -78,9 +78,32 @@ impl ApplicationManager { dirs }; - if let Err(err) = manager.register_global_shortcut().await { - tracing::warn!(target = "rpc", "error occurred when registering shortcut {:?}", err) - } + match manager.get_global_shortcut().await? { + None => { + let shortcut = if cfg!(target_os = "windows") { + PhysicalShortcut { + physical_key: PhysicalKey::Space, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + } + } else { + PhysicalShortcut { + physical_key: PhysicalKey::Space, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: true, + } + }; + + manager.set_global_shortcut(Some(shortcut)).await? + } + Some((shortcut, _)) => { + manager.set_global_shortcut(shortcut).await? + } + }; Ok(manager) } @@ -250,17 +273,18 @@ impl ApplicationManager { Ok(()) } - pub async fn set_global_shortcut(&self, shortcut: PhysicalShortcut) -> anyhow::Result<()> { - self.db_repository.set_global_shortcut(shortcut) + pub async fn set_global_shortcut(&self, shortcut: Option) -> anyhow::Result<()> { + let err = self.frontend_api.set_global_shortcut(shortcut.clone()).await; + + let db_err = err.as_ref().map_err(|err| format!("{:#}", err)).err(); + + self.db_repository.set_global_shortcut(shortcut, db_err) .await?; - self.register_global_shortcut() - .await?; - - Ok(()) + err } - pub async fn get_global_shortcut(&self) -> anyhow::Result { + pub async fn get_global_shortcut(&self) -> anyhow::Result, Option)>> { self.db_repository.get_global_shortcut().await } @@ -275,12 +299,6 @@ impl ApplicationManager { Ok(()) } - async fn register_global_shortcut(&self) -> anyhow::Result<()> { - let shortcut = self.db_repository.get_global_shortcut().await?; - - self.frontend_api.set_global_shortcut(shortcut).await - } - pub async fn reload_config(&self) -> anyhow::Result<()> { self.config_reader.reload_config().await?; diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index 66e7e55..289fed9 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -67,7 +67,7 @@ impl BackendServer for BackendServerImpl { Ok(()) } - async fn set_global_shortcut(&self, shortcut: PhysicalShortcut) -> anyhow::Result<()> { + async fn set_global_shortcut(&self, shortcut: Option) -> anyhow::Result<()> { let result = self.application_manager.set_global_shortcut(shortcut) .await; @@ -78,9 +78,10 @@ impl BackendServer for BackendServerImpl { result } - async fn get_global_shortcut(&self) -> anyhow::Result { + async fn get_global_shortcut(&self) -> anyhow::Result<(Option, Option)> { let result = self.application_manager.get_global_shortcut() - .await?; + .await? + .unwrap_or((None, None)); Ok(result) } diff --git a/schema/backend.proto b/schema/backend.proto index f9f1074..b19ac39 100644 --- a/schema/backend.proto +++ b/schema/backend.proto @@ -68,7 +68,7 @@ message RpcSetEntrypointStateRequest { message RpcSetEntrypointStateResponse { } -message RpcSetGlobalShortcutRequest { +message RpcShortcut { string physical_key = 1; bool modifier_shift = 2; bool modifier_control = 3; @@ -76,6 +76,10 @@ message RpcSetGlobalShortcutRequest { bool modifier_meta = 5; } +message RpcSetGlobalShortcutRequest { + optional RpcShortcut shortcut = 1; +} + message RpcSetGlobalShortcutResponse { } @@ -83,11 +87,8 @@ message RpcGetGlobalShortcutRequest { } message RpcGetGlobalShortcutResponse { - string physical_key = 1; - bool modifier_shift = 2; - bool modifier_control = 3; - bool modifier_alt = 4; - bool modifier_meta = 5; + optional RpcShortcut shortcut = 1; + optional string error = 2; } message RpcSetPreferenceValueRequest { From 220fb27eca0450e77b0598cf36c12f6c19561e9a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:31:38 +0100 Subject: [PATCH 178/540] Enable experimental optional types in grpc schema files --- rust/common/build.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/common/build.rs b/rust/common/build.rs index af2fff9..bea5763 100644 --- a/rust/common/build.rs +++ b/rust/common/build.rs @@ -12,6 +12,7 @@ use indexmap::IndexMap; fn main() -> Result<(), Box> { tonic_build::configure() + .protoc_arg("--experimental_allow_proto3_optional") .compile( &["./../../schema/backend.proto"], &["./../../schema/"], From 318895d241e1c53e8ecd5572350454ee921c1558 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:43:32 +0100 Subject: [PATCH 179/540] Fix build broken by accidental import removal --- rust/cli/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs index df62970..56e5a85 100644 --- a/rust/cli/src/lib.rs +++ b/rust/cli/src/lib.rs @@ -1,3 +1,4 @@ +use anyhow::{anyhow, Context}; use clap::Parser; use client::{generate_color_theme_sample, generate_theme_sample, open_window}; From 84ccf5980adc69549f96f212f6819dd2d434afab Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:57:41 +0100 Subject: [PATCH 180/540] Rename themes from "theme" and "color theme" into "complex theme" and "theme" respectively --- rust/cli/src/lib.rs | 8 +- rust/client/src/lib.rs | 36 ++--- rust/client/src/ui/mod.rs | 8 +- rust/client/src/ui/search_list.rs | 4 +- rust/client/src/ui/theme/button.rs | 8 +- rust/client/src/ui/theme/checkbox.rs | 6 +- rust/client/src/ui/theme/container.rs | 6 +- rust/client/src/ui/theme/date_picker.rs | 6 +- rust/client/src/ui/theme/grid.rs | 4 +- rust/client/src/ui/theme/loading_bar.rs | 6 +- rust/client/src/ui/theme/mod.rs | 202 ++++++++++++------------ rust/client/src/ui/theme/pick_list.rs | 8 +- rust/client/src/ui/theme/row.rs | 4 +- rust/client/src/ui/theme/rule.rs | 6 +- rust/client/src/ui/theme/scrollable.rs | 4 +- rust/client/src/ui/theme/text.rs | 6 +- rust/client/src/ui/theme/text_input.rs | 6 +- rust/client/src/ui/theme/tooltip.rs | 4 +- rust/common/src/dirs.rs | 20 +-- 19 files changed, 174 insertions(+), 178 deletions(-) diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs index 56e5a85..c6e60fa 100644 --- a/rust/cli/src/lib.rs +++ b/rust/cli/src/lib.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Context}; use clap::Parser; -use client::{generate_color_theme_sample, generate_theme_sample, open_window}; +use client::{generate_simplified_theme_sample, generate_complex_theme_sample, open_window}; use management_client::start_management_client; use server::start; @@ -18,8 +18,8 @@ struct Cli { enum Commands { Open, Settings, + GenerateSampleComplexTheme, GenerateSampleTheme, - GenerateSampleColorTheme, } pub fn init() { @@ -48,8 +48,8 @@ pub fn init() { match command { Commands::Open => open_window(), Commands::Settings => start_management_client(), - Commands::GenerateSampleTheme => generate_theme_sample().expect("Unable to generate sample theme"), - Commands::GenerateSampleColorTheme => generate_color_theme_sample().expect("Unable to generate sample color theme") + Commands::GenerateSampleComplexTheme => generate_complex_theme_sample().expect("Unable to generate complex theme sample"), + Commands::GenerateSampleTheme => generate_simplified_theme_sample().expect("Unable to generate simplified theme sample") }; } } diff --git a/rust/client/src/lib.rs b/rust/client/src/lib.rs index 2b5a189..ecc0c9a 100644 --- a/rust/client/src/lib.rs +++ b/rust/client/src/lib.rs @@ -2,7 +2,7 @@ use common::dirs::Dirs; use common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; use common::rpc::backend_api::BackendApi; use utils::channel::{RequestReceiver, RequestSender}; -use crate::ui::GauntletTheme; +use crate::ui::GauntletComplexTheme; pub(in crate) mod ui; pub(in crate) mod model; @@ -60,50 +60,50 @@ pub fn open_settings_window() { }) } -pub fn generate_theme_sample() -> anyhow::Result<()> { +pub fn generate_complex_theme_sample() -> anyhow::Result<()> { let dirs = Dirs::new(); - let sample_theme_file = dirs.sample_theme_file(); - let theme_file = dirs.theme_file(); + let sample_complex_theme_file = dirs.sample_complex_theme_file(); + let complex_theme_file = dirs.complex_theme_file(); - let theme = GauntletTheme::default_theme(GauntletTheme::default_color_theme()); + let theme_complex = GauntletComplexTheme::default_theme(GauntletComplexTheme::default_simplified_theme()); - let string = serde_json::to_string_pretty(&theme)?; + let string = serde_json::to_string_pretty(&theme_complex)?; - let sample_theme_parent = sample_theme_file + let sample_theme_parent = sample_complex_theme_file .parent() .expect("no parent?"); std::fs::create_dir_all(sample_theme_parent)?; - std::fs::write(&sample_theme_file, string)?; + std::fs::write(&sample_complex_theme_file, string)?; - println!("Created sample using default theme at {:?}", sample_theme_file); - println!("Make changes and rename file to {:?}", theme_file.file_name().unwrap()); + println!("Created sample using default complex theme at {:?}", sample_complex_theme_file); + println!("Make changes and rename file to {:?}", complex_theme_file.file_name().unwrap()); Ok(()) } -pub fn generate_color_theme_sample() -> anyhow::Result<()> { +pub fn generate_simplified_theme_sample() -> anyhow::Result<()> { let dirs = Dirs::new(); - let sample_theme_color_file = dirs.sample_theme_color_file(); - let theme_color_file = dirs.theme_color_file(); + let sample_simplified_theme_file = dirs.sample_simplified_theme_color_file(); + let simplified_theme_file = dirs.theme_simplified_file(); - let theme = GauntletTheme::default_color_theme(); + let theme = GauntletComplexTheme::default_simplified_theme(); let string = serde_json::to_string_pretty(&theme)?; - let sample_theme_parent = sample_theme_color_file + let sample_theme_parent = sample_simplified_theme_file .parent() .expect("no parent?"); std::fs::create_dir_all(sample_theme_parent)?; - std::fs::write(&sample_theme_color_file, string)?; + std::fs::write(&sample_simplified_theme_file, string)?; - println!("Created sample using default color theme at {:?}", sample_theme_color_file); - println!("Make changes and rename file to {:?}", theme_color_file.file_name().unwrap()); + println!("Created sample using default simplified theme at {:?}", sample_simplified_theme_file); + println!("Make changes and rename file to {:?}", simplified_theme_file.file_name().unwrap()); Ok(()) } \ No newline at end of file diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 5857f39..6131966 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -59,7 +59,7 @@ use crate::ui::hud::{close_hud_window, show_hud_window}; use crate::ui::scroll_handle::ScrollHandle; use crate::ui::state::{ErrorViewData, Focus, GlobalState, LoadingBarState, MainViewState, PluginViewData, PluginViewState}; use crate::ui::widget_container::PluginWidgetContainer; -pub use theme::GauntletTheme; +pub use theme::GauntletComplexTheme; pub struct AppModel { // logic @@ -68,7 +68,7 @@ pub struct AppModel { current_hotkey: Arc>>, frontend_receiver: Arc>>, focused: bool, - theme: GauntletTheme, + theme: GauntletComplexTheme, wayland: bool, #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: tray_icon::TrayIcon, @@ -290,7 +290,7 @@ pub fn run( impl Application for AppModel { type Executor = executor::Default; type Message = AppMsg; - type Theme = GauntletTheme; + type Theme = GauntletComplexTheme; type Flags = AppFlags; fn new(flags: Self::Flags) -> (Self, Command) { @@ -417,7 +417,7 @@ impl Application for AppModel { current_hotkey: Arc::new(StdMutex::new(None)), frontend_receiver: Arc::new(TokioRwLock::new(frontend_receiver)), focused: false, - theme: GauntletTheme::new(), + theme: GauntletComplexTheme::new(), wayland, #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: sys_tray::create_tray(), diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index c633ed8..e304bea 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -8,7 +8,7 @@ use iced::widget::text; use iced::widget::text::Shaping; use common::model::SearchResult; use crate::ui::scroll_handle::ScrollHandle; -use crate::ui::theme::{Element, GauntletTheme, ThemableWidget}; +use crate::ui::theme::{Element, GauntletComplexTheme, ThemableWidget}; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; use crate::ui::theme::image::ImageStyle; @@ -46,7 +46,7 @@ impl<'a, Message> SearchList<'a, Message> { } } -impl<'a, Message> Component for SearchList<'a, Message> { +impl<'a, Message> Component for SearchList<'a, Message> { type State = (); type Event = SelectItemEvent; diff --git a/rust/client/src/ui/theme/button.rs b/rust/client/src/ui/theme/button.rs index 49e582f..9eee65d 100644 --- a/rust/client/src/ui/theme/button.rs +++ b/rust/client/src/ui/theme/button.rs @@ -1,7 +1,7 @@ use button::Appearance; use iced::{Border, Padding, Renderer}; use iced::widget::{button, Button}; -use crate::ui::theme::{Element, GauntletTheme, get_theme, NOT_INTENDED_TO_BE_USED, padding_all, ThemableWidget, TRANSPARENT}; +use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, NOT_INTENDED_TO_BE_USED, padding_all, ThemableWidget, TRANSPARENT}; #[derive(Default, Debug, Clone, Copy)] pub enum ButtonStyle { @@ -88,7 +88,7 @@ impl ButtonStyle { } } - fn appearance(&self, theme: &GauntletTheme, state: ButtonState) -> Appearance { + fn appearance(&self, theme: &GauntletComplexTheme, state: ButtonState) -> Appearance { let (background_color, background_color_hover, background_color_pressed, text_color, text_color_hover, border_radius, border_width, border_color) = match &self { ButtonStyle::RootBottomPanelPrimaryActionButton | ButtonStyle::RootBottomPanelActionToggleButton => { let theme = &theme.root_bottom_panel_action_toggle_button; @@ -178,7 +178,7 @@ impl ButtonStyle { } } -impl button::StyleSheet for GauntletTheme { +impl button::StyleSheet for GauntletComplexTheme { type Style = ButtonStyle; fn active(&self, style: &Self::Style) -> Appearance { @@ -194,7 +194,7 @@ impl button::StyleSheet for GauntletTheme { } } -impl<'a, Message: 'a + Clone> ThemableWidget<'a, Message> for Button<'a, Message, GauntletTheme, Renderer> { +impl<'a, Message: 'a + Clone> ThemableWidget<'a, Message> for Button<'a, Message, GauntletComplexTheme, Renderer> { type Kind = ButtonStyle; fn themed(self, kind: ButtonStyle) -> Element<'a, Message> { diff --git a/rust/client/src/ui/theme/checkbox.rs b/rust/client/src/ui/theme/checkbox.rs index 5ee3017..8c9f032 100644 --- a/rust/client/src/ui/theme/checkbox.rs +++ b/rust/client/src/ui/theme/checkbox.rs @@ -2,7 +2,7 @@ use checkbox::Appearance; use iced::{Border, Renderer}; use iced::widget::{checkbox, Checkbox}; -use crate::ui::theme::{Element, GauntletTheme, get_theme, NOT_INTENDED_TO_BE_USED, ThemableWidget}; +use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, NOT_INTENDED_TO_BE_USED, ThemableWidget}; #[derive(Default)] pub enum CheckboxStyle { @@ -10,7 +10,7 @@ pub enum CheckboxStyle { Default, } -impl checkbox::StyleSheet for GauntletTheme { +impl checkbox::StyleSheet for GauntletComplexTheme { type Style = CheckboxStyle; fn active(&self, _: &Self::Style, is_checked: bool) -> Appearance { @@ -69,7 +69,7 @@ impl checkbox::StyleSheet for GauntletTheme { } } -impl<'a, Message: 'a> ThemableWidget<'a, Message> for Checkbox<'a, Message, GauntletTheme, Renderer> { +impl<'a, Message: 'a> ThemableWidget<'a, Message> for Checkbox<'a, Message, GauntletComplexTheme, Renderer> { type Kind = CheckboxStyle; fn themed(self, style: CheckboxStyle) -> Element<'a, Message> { diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 83afb7a..d02857a 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -3,7 +3,7 @@ use iced::{Border, Color, Length, Padding, Renderer}; use iced::border::Radius; use iced::widget::{Container, container}; -use crate::ui::theme::{Element, GauntletTheme, get_theme, ThemableWidget}; +use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; pub enum ContainerStyle { ActionPanel, @@ -81,7 +81,7 @@ pub enum ContainerStyleInner { } -impl container::StyleSheet for GauntletTheme { +impl container::StyleSheet for GauntletComplexTheme { type Style = ContainerStyleInner; fn appearance(&self, style: &Self::Style) -> Appearance { @@ -242,7 +242,7 @@ impl container::StyleSheet for GauntletTheme { } } -impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, GauntletTheme, Renderer> { +impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, GauntletComplexTheme, Renderer> { type Kind = ContainerStyle; fn themed(self, name: ContainerStyle) -> Element<'a, Message> { diff --git a/rust/client/src/ui/theme/date_picker.rs b/rust/client/src/ui/theme/date_picker.rs index 3c49394..5bed502 100644 --- a/rust/client/src/ui/theme/date_picker.rs +++ b/rust/client/src/ui/theme/date_picker.rs @@ -2,7 +2,7 @@ use iced::Color; use iced_aw::{date_picker, DatePicker}; use iced_aw::date_picker::Appearance; -use crate::ui::theme::{Element, GauntletTheme, get_theme, ThemableWidget}; +use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; #[derive(Clone, Default)] pub enum DatePickerStyle { @@ -10,7 +10,7 @@ pub enum DatePickerStyle { Default, } -impl date_picker::StyleSheet for GauntletTheme { +impl date_picker::StyleSheet for GauntletComplexTheme { type Style = DatePickerStyle; fn active(&self, _: &Self::Style) -> Appearance { @@ -58,7 +58,7 @@ impl date_picker::StyleSheet for GauntletTheme { } } -impl<'a, Message: 'a + Clone + 'static> ThemableWidget<'a, Message> for DatePicker<'a, Message, GauntletTheme> { +impl<'a, Message: 'a + Clone + 'static> ThemableWidget<'a, Message> for DatePicker<'a, Message, GauntletComplexTheme> { type Kind = DatePickerStyle; fn themed(self, kind: DatePickerStyle) -> Element<'a, Message> { diff --git a/rust/client/src/ui/theme/grid.rs b/rust/client/src/ui/theme/grid.rs index dc10b7d..b4bec5c 100644 --- a/rust/client/src/ui/theme/grid.rs +++ b/rust/client/src/ui/theme/grid.rs @@ -1,12 +1,12 @@ use iced::Renderer; use iced_aw::Grid; -use crate::ui::theme::{Element, GauntletTheme, get_theme, ThemableWidget}; +use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; pub enum GridStyle { Default, } -impl<'a, Message: 'a + 'static> ThemableWidget<'a, Message> for Grid<'a, Message, GauntletTheme, Renderer> { +impl<'a, Message: 'a + 'static> ThemableWidget<'a, Message> for Grid<'a, Message, GauntletComplexTheme, Renderer> { type Kind = GridStyle; fn themed(self, kind: GridStyle) -> Element<'a, Message> { diff --git a/rust/client/src/ui/theme/loading_bar.rs b/rust/client/src/ui/theme/loading_bar.rs index 5f94901..8098600 100644 --- a/rust/client/src/ui/theme/loading_bar.rs +++ b/rust/client/src/ui/theme/loading_bar.rs @@ -1,7 +1,7 @@ use crate::ui::custom_widgets::loading_bar; use crate::ui::custom_widgets::loading_bar::{Appearance, LoadingBar}; use crate::ui::theme::{Element, ThemableWidget}; -use crate::ui::GauntletTheme; +use crate::ui::GauntletComplexTheme; #[derive(Default)] pub enum LoadingBarStyle { @@ -9,7 +9,7 @@ pub enum LoadingBarStyle { Default, } -impl loading_bar::StyleSheet for GauntletTheme { +impl loading_bar::StyleSheet for GauntletComplexTheme { type Style = LoadingBarStyle; fn appearance(&self, _style: &Self::Style) -> Appearance { @@ -20,7 +20,7 @@ impl loading_bar::StyleSheet for GauntletTheme { } } -impl<'a, Message: 'a> ThemableWidget<'a, Message> for LoadingBar { +impl<'a, Message: 'a> ThemableWidget<'a, Message> for LoadingBar { type Kind = LoadingBarStyle; fn themed(self, _kind: LoadingBarStyle) -> Element<'a, Message> { diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 35132ee..71c6373 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -21,13 +21,13 @@ pub mod grid; pub mod tooltip; mod loading_bar; -pub type Element<'a, Message> = iced::Element<'a, Message, GauntletTheme>; +pub type Element<'a, Message> = iced::Element<'a, Message, GauntletComplexTheme>; -const CURRENT_COLOR_THEME_VERSION: u64 = 3; -const CURRENT_THEME_VERSION: u64 = 4; +const CURRENT_SIMPLIFIED_THEME_VERSION: u64 = 4; +const CURRENT_COMPLEX_THEME_VERSION: u64 = 4; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GauntletColorTheme { +pub struct GauntletSimplifiedTheme { version: u64, background_darkest_color: ThemeColor, background_darker_color: ThemeColor, @@ -37,13 +37,12 @@ pub struct GauntletColorTheme { text_lighter_color: ThemeColor, text_darker_color: ThemeColor, text_darkest_color: ThemeColor, - primary_color: ThemeColor, - primary_hovered_color: ThemeColor, - date_picker_text_darker: ThemeColor + primary_darker_color: ThemeColor, + primary_lighter_color: ThemeColor, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GauntletTheme { +pub struct GauntletComplexTheme { version: u64, text: ThemeColor, root: ThemeRoot, @@ -126,25 +125,81 @@ pub struct GauntletTheme { hud_content: ThemePaddingOnly } -impl Default for GauntletTheme { +impl Default for GauntletComplexTheme { fn default() -> Self { unreachable!() } } +fn parse_json_theme(theme_file: PathBuf, theme_name: &str) -> Option { + match std::fs::read_to_string(theme_file) { + Ok(value) => { + let result = serde_json::from_str::(&value); + + match result { + Ok(value) => { + match value.get("version") { + Some(serde_json::Value::Number(number)) => { + match number.as_u64() { + None => { + tracing::warn!("Version of read {} file is invalid", theme_name); + None + } + Some(CURRENT_SIMPLIFIED_THEME_VERSION) => { + match serde_json::from_value::(value) { + Ok(value) => Some(value), + Err(err) => { + tracing::warn!("Unable to parse {} file: {}", theme_name, err); + None + } + } + } + Some(_) => { + tracing::warn!("Version of read {} file doesn't match expected, theme: {}, expected: {}", theme_name, number, CURRENT_SIMPLIFIED_THEME_VERSION); + None + } + } + } + _ => { + tracing::warn!("Version of read {} file is not a number", theme_name); + None + } + } + } + Err(err) => { + tracing::warn!("Unable to parse {} file: {}", theme_name, err); + None + } + } + } + Err(err) => { + match err.kind() { + ErrorKind::NotFound => { + tracing::debug!("No {} file was found", theme_name); + None + } + err @ _ => { + tracing::warn!("Unable to read {} file: {}", theme_name, err); + None + } + } + } + } +} + // TODO add border on focus, lighter background on hover // TODO padding on button is padding, not margin, a lot of margins missing? -impl GauntletTheme { +impl GauntletComplexTheme { pub fn new() -> Self { let dirs = Dirs::new(); - let theme = GauntletTheme::parse_file(dirs.theme_file(), "theme") + let theme = parse_json_theme(dirs.complex_theme_file(), "complex theme") .unwrap_or_else(|| { - let color_theme = GauntletTheme::parse_file(dirs.theme_color_file(), "color theme") - .unwrap_or_else(|| GauntletTheme::default_color_theme()); + let simplified_theme = parse_json_theme(dirs.theme_simplified_file(), "simplified theme") + .unwrap_or_else(|| GauntletComplexTheme::default_simplified_theme()); - GauntletTheme::default_theme(color_theme) + GauntletComplexTheme::default_theme(simplified_theme) }); init_theme(theme.clone()); @@ -152,65 +207,9 @@ impl GauntletTheme { theme } - fn parse_file(theme_file: PathBuf, theme_name: &str) -> Option { - match std::fs::read_to_string(theme_file) { - Ok(value) => { - let result = serde_json::from_str::(&value); - - match result { - Ok(value) => { - match value.get("version") { - Some(serde_json::Value::Number(number)) => { - match number.as_u64() { - None => { - tracing::warn!("Version of read {} file is invalid", theme_name); - None - } - Some(CURRENT_COLOR_THEME_VERSION) => { - match serde_json::from_value::(value) { - Ok(value) => Some(value), - Err(err) => { - tracing::warn!("Unable to parse {} file: {}", theme_name, err); - None - } - } - } - Some(_) => { - tracing::warn!("Version of read {} file doesn't match expected, theme: {}, expected: {}", theme_name, number, CURRENT_COLOR_THEME_VERSION); - None - } - } - } - _ => { - tracing::warn!("Version of read {} file is not a number", theme_name); - None - } - } - } - Err(err) => { - tracing::warn!("Unable to parse {} file: {}", theme_name, err); - None - } - } - } - Err(err) => { - match err.kind() { - ErrorKind::NotFound => { - tracing::debug!("No {} file was found", theme_name); - None - } - err @ _ => { - tracing::warn!("Unable to read {} file: {}", theme_name, err); - None - } - } - } - } - } - - pub fn default_color_theme() -> GauntletColorTheme { - GauntletColorTheme { - version: CURRENT_COLOR_THEME_VERSION, + pub fn default_simplified_theme() -> GauntletSimplifiedTheme { + GauntletSimplifiedTheme { + version: CURRENT_SIMPLIFIED_THEME_VERSION, background_lightest_color: BACKGROUND_LIGHTEST, background_lighter_color: BACKGROUND_LIGHTER, background_darker_color: BACKGROUND_DARKER, @@ -219,14 +218,13 @@ impl GauntletTheme { text_lighter_color: TEXT_LIGHTER, text_darker_color: TEXT_DARKER, text_darkest_color: TEXT_DARKEST, - primary_color: PRIMARY, - primary_hovered_color: PRIMARY_HOVERED, - date_picker_text_darker: DATE_PICKER_TEXT_DARKER + primary_darker_color: PRIMARY, + primary_lighter_color: PRIMARY_HOVERED, } } - pub fn default_theme(color_theme: GauntletColorTheme) -> GauntletTheme { - let GauntletColorTheme { + pub fn default_theme(simplified_theme: GauntletSimplifiedTheme) -> GauntletComplexTheme { + let GauntletSimplifiedTheme { version: _, background_darkest_color, background_darker_color, @@ -236,13 +234,12 @@ impl GauntletTheme { text_lighter_color, text_darker_color, text_darkest_color, - primary_color, - primary_hovered_color, - date_picker_text_darker - } = color_theme; + primary_darker_color, + primary_lighter_color, + } = simplified_theme; - GauntletTheme { - version: CURRENT_THEME_VERSION, + GauntletComplexTheme { + version: CURRENT_COMPLEX_THEME_VERSION, text: text_lightest_color, root: ThemeRoot { background_color: background_darkest_color, @@ -287,9 +284,9 @@ impl GauntletTheme { }, metadata_tag_item_button: ThemeButton { padding: padding_axis(2.0, 8.0), - background_color: primary_color, - background_color_focused: primary_hovered_color, - background_color_hovered: primary_hovered_color, + background_color: primary_darker_color, + background_color_focused: primary_lighter_color, + background_color_hovered: primary_lighter_color, text_color: text_darkest_color, text_color_hovered: text_darkest_color, border_radius: BUTTON_BORDER_RADIUS, @@ -539,16 +536,16 @@ impl GauntletTheme { text_color: text_lightest_color, text_color_selected: text_darker_color, text_color_hovered: text_darker_color, - text_attenuated_color: date_picker_text_darker, + text_attenuated_color: text_darker_color, day_background_color: background_lighter_color, day_background_color_selected: background_lighter_color, day_background_color_hovered: background_lighter_color, }, form_input_date_picker_buttons: ThemeButton { padding: padding_all(8.0), - background_color: primary_color, - background_color_focused: primary_hovered_color, - background_color_hovered: primary_hovered_color, + background_color: primary_darker_color, + background_color_focused: primary_lighter_color, + background_color_hovered: primary_lighter_color, text_color: text_darkest_color, text_color_hovered: text_darkest_color, border_radius: BUTTON_BORDER_RADIUS, @@ -556,18 +553,18 @@ impl GauntletTheme { border_color: TRANSPARENT, }, form_input_checkbox: ThemeCheckbox { - background_color_checked: primary_color, + background_color_checked: primary_darker_color, background_color_unchecked: background_darkest_color, - background_color_checked_hovered: primary_hovered_color, + background_color_checked_hovered: primary_lighter_color, background_color_unchecked_hovered: background_lighter_color, border_radius: 4.0, border_width: 1.0, - border_color: primary_color, + border_color: primary_darker_color, icon_color: background_darkest_color, }, form_input_select: ThemeSelect { - background_color: primary_color, - background_color_hovered: primary_hovered_color, + background_color: primary_darker_color, + background_color_hovered: primary_lighter_color, text_color: text_darkest_color, text_color_hovered: text_darkest_color, border_radius: BUTTON_BORDER_RADIUS, @@ -598,7 +595,7 @@ impl GauntletTheme { color: background_lighter_color }, scrollbar: ThemeScrollbar { - color: primary_color, + color: primary_darker_color, border_radius: 4.0, border_width: 0.0, border_color: TRANSPARENT, @@ -608,7 +605,7 @@ impl GauntletTheme { background_color: background_darker_color, }, loading_bar: ThemeLoadingBar { - loading_bar_color: primary_color, + loading_bar_color: primary_darker_color, background_color: background_lighter_color, }, text_accessory: ThemePaddingTextColorSpacing { @@ -633,15 +630,15 @@ impl GauntletTheme { } } -fn init_theme(theme: GauntletTheme) { +fn init_theme(theme: GauntletComplexTheme) { THEME.set(theme).expect("already set"); } -fn get_theme() -> &'static GauntletTheme { +fn get_theme() -> &'static GauntletComplexTheme { &THEME.get().expect("theme global var was not set") } -static THEME: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); +static THEME: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); const NOT_INTENDED_TO_BE_USED: ThemeColor = ThemeColor::new(0xAF5BFF, 1.0); @@ -657,7 +654,6 @@ const TEXT_DARKER: ThemeColor = ThemeColor::new(0x6B7785, 1.0); const TEXT_DARKEST: ThemeColor = ThemeColor::new(0x1D242C, 1.0); const PRIMARY: ThemeColor = ThemeColor::new(0xC79F60, 1.0); const PRIMARY_HOVERED: ThemeColor = ThemeColor::new(0xD7B37A, 1.0); -const DATE_PICKER_TEXT_DARKER: ThemeColor = ThemeColor::new(0xCAC2B6, 0.3); const BUTTON_BORDER_RADIUS: f32 = 6.0; @@ -998,7 +994,7 @@ pub trait ThemableWidget<'a, Message> { fn themed(self, name: Self::Kind) -> Element<'a, Message>; } -impl application::StyleSheet for GauntletTheme { +impl application::StyleSheet for GauntletComplexTheme { type Style = (); fn appearance(&self, _: &Self::Style) -> application::Appearance { diff --git a/rust/client/src/ui/theme/pick_list.rs b/rust/client/src/ui/theme/pick_list.rs index 05412ed..5e00c3f 100644 --- a/rust/client/src/ui/theme/pick_list.rs +++ b/rust/client/src/ui/theme/pick_list.rs @@ -3,7 +3,7 @@ use std::borrow::Borrow; use iced::{Border, overlay}; use iced::widget::{pick_list, PickList}; -use crate::ui::theme::{Element, GauntletTheme, get_theme, NOT_INTENDED_TO_BE_USED, ThemableWidget}; +use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, NOT_INTENDED_TO_BE_USED, ThemableWidget}; #[derive(Clone, Default)] pub enum PickListStyle { @@ -17,7 +17,7 @@ pub enum MenuStyle { Default, } -impl pick_list::StyleSheet for GauntletTheme { +impl pick_list::StyleSheet for GauntletComplexTheme { type Style = PickListStyle; fn active(&self, _: &Self::Style) -> pick_list::Appearance { @@ -61,7 +61,7 @@ fn pick_list_appearance(state: PickListState) -> pick_list::Appearance { } } -impl overlay::menu::StyleSheet for GauntletTheme { +impl overlay::menu::StyleSheet for GauntletComplexTheme { type Style = MenuStyle; fn appearance(&self, _: &Self::Style) -> overlay::menu::Appearance { @@ -90,7 +90,7 @@ impl From for MenuStyle { } } -impl<'a, Message: 'a + Clone + 'static, T, L, V> ThemableWidget<'a, Message> for PickList<'a, T, L, V, Message, GauntletTheme> +impl<'a, Message: 'a + Clone + 'static, T, L, V> ThemableWidget<'a, Message> for PickList<'a, T, L, V, Message, GauntletComplexTheme> where T: ToString + PartialEq + Clone + 'a, L: Borrow<[T]> + 'a, diff --git a/rust/client/src/ui/theme/row.rs b/rust/client/src/ui/theme/row.rs index d20f404..07acca1 100644 --- a/rust/client/src/ui/theme/row.rs +++ b/rust/client/src/ui/theme/row.rs @@ -1,4 +1,4 @@ -use crate::ui::theme::{get_theme, Element, GauntletTheme, ThemableWidget}; +use crate::ui::theme::{get_theme, Element, GauntletComplexTheme, ThemableWidget}; use iced::widget::Row; use iced::{Padding, Renderer}; @@ -14,7 +14,7 @@ pub enum RowStyle { RootTopPanel, } -impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletTheme, Renderer> { +impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletComplexTheme, Renderer> { type Kind = RowStyle; fn themed(self, name: RowStyle) -> Element<'a, Message> { diff --git a/rust/client/src/ui/theme/rule.rs b/rust/client/src/ui/theme/rule.rs index b316614..bd033d2 100644 --- a/rust/client/src/ui/theme/rule.rs +++ b/rust/client/src/ui/theme/rule.rs @@ -1,7 +1,7 @@ use iced::widget::{rule, Rule}; use rule::Appearance; -use crate::ui::theme::{Element, GauntletTheme, get_theme, ThemableWidget}; +use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; #[derive(Default)] pub enum RuleStyle { @@ -11,7 +11,7 @@ pub enum RuleStyle { PrimaryActionSeparator, } -impl rule::StyleSheet for GauntletTheme { +impl rule::StyleSheet for GauntletComplexTheme { type Style = RuleStyle; fn appearance(&self, style: &Self::Style) -> Appearance { @@ -46,7 +46,7 @@ impl rule::StyleSheet for GauntletTheme { } } -impl<'a, Message: 'a> ThemableWidget<'a, Message> for Rule { +impl<'a, Message: 'a> ThemableWidget<'a, Message> for Rule { type Kind = RuleStyle; fn themed(self, kind: RuleStyle) -> Element<'a, Message> { diff --git a/rust/client/src/ui/theme/scrollable.rs b/rust/client/src/ui/theme/scrollable.rs index addb199..5ba8a79 100644 --- a/rust/client/src/ui/theme/scrollable.rs +++ b/rust/client/src/ui/theme/scrollable.rs @@ -2,9 +2,9 @@ use iced::{Border, Color}; use iced::widget::scrollable; use scrollable::Appearance; -use crate::ui::theme::{GauntletTheme, get_theme}; +use crate::ui::theme::{GauntletComplexTheme, get_theme}; -impl scrollable::StyleSheet for GauntletTheme { +impl scrollable::StyleSheet for GauntletComplexTheme { type Style = (); fn active(&self, _: &Self::Style) -> Appearance { diff --git a/rust/client/src/ui/theme/text.rs b/rust/client/src/ui/theme/text.rs index 1615864..02c8295 100644 --- a/rust/client/src/ui/theme/text.rs +++ b/rust/client/src/ui/theme/text.rs @@ -2,7 +2,7 @@ use iced::Renderer; use iced::widget::{Text, text}; use text::Appearance; -use crate::ui::theme::{Element, GauntletTheme, get_theme, ThemableWidget}; +use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; #[derive(Clone, Default)] pub enum TextStyle { @@ -27,7 +27,7 @@ pub enum TextStyle { RootBottomPanelActionToggleText, } -impl<'a, Message: 'a> ThemableWidget<'a, Message> for Text<'a, GauntletTheme, Renderer> { +impl<'a, Message: 'a> ThemableWidget<'a, Message> for Text<'a, GauntletComplexTheme, Renderer> { type Kind = TextStyle; fn themed(self, kind: TextStyle) -> Element<'a, Message> { @@ -52,7 +52,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Text<'a, GauntletTheme, Re } } -impl text::StyleSheet for GauntletTheme { +impl text::StyleSheet for GauntletComplexTheme { type Style = TextStyle; fn appearance(&self, style: Self::Style) -> Appearance { diff --git a/rust/client/src/ui/theme/text_input.rs b/rust/client/src/ui/theme/text_input.rs index 3f0eebe..80f9d80 100644 --- a/rust/client/src/ui/theme/text_input.rs +++ b/rust/client/src/ui/theme/text_input.rs @@ -2,7 +2,7 @@ use iced::widget::{text_input, TextInput}; use iced::{Border, Color, Renderer}; use text_input::Appearance; -use crate::ui::theme::{Element, GauntletTheme, ThemableWidget, NOT_INTENDED_TO_BE_USED}; +use crate::ui::theme::{Element, GauntletComplexTheme, ThemableWidget, NOT_INTENDED_TO_BE_USED}; #[derive(Default)] pub enum TextInputStyle { @@ -15,7 +15,7 @@ pub enum TextInputStyle { } // noinspection RsSortImplTraitMembers -impl text_input::StyleSheet for GauntletTheme { +impl text_input::StyleSheet for GauntletComplexTheme { type Style = TextInputStyle; fn active(&self, style: &Self::Style) -> Appearance { @@ -123,7 +123,7 @@ impl text_input::StyleSheet for GauntletTheme { } } -impl<'a, Message: 'a + Clone> ThemableWidget<'a, Message> for TextInput<'a, Message, GauntletTheme, Renderer> { +impl<'a, Message: 'a + Clone> ThemableWidget<'a, Message> for TextInput<'a, Message, GauntletComplexTheme, Renderer> { type Kind = TextInputStyle; fn themed(self, kind: TextInputStyle) -> Element<'a, Message> { diff --git a/rust/client/src/ui/theme/tooltip.rs b/rust/client/src/ui/theme/tooltip.rs index f879971..3747d61 100644 --- a/rust/client/src/ui/theme/tooltip.rs +++ b/rust/client/src/ui/theme/tooltip.rs @@ -1,14 +1,14 @@ use iced::Renderer; use iced::widget::Tooltip; -use crate::ui::theme::{Element, GauntletTheme, get_theme, ThemableWidget}; +use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; use crate::ui::theme::container::ContainerStyleInner; pub enum TooltipStyle { Tooltip, } -impl<'a, Message: 'a> ThemableWidget<'a, Message> for Tooltip<'a, Message, GauntletTheme, Renderer> { +impl<'a, Message: 'a> ThemableWidget<'a, Message> for Tooltip<'a, Message, GauntletComplexTheme, Renderer> { type Kind = TooltipStyle; fn themed(self, kind: TooltipStyle) -> Element<'a, Message> { diff --git a/rust/common/src/dirs.rs b/rust/common/src/dirs.rs index 18c9b7f..20f9cf9 100644 --- a/rust/common/src/dirs.rs +++ b/rust/common/src/dirs.rs @@ -55,22 +55,22 @@ impl Dirs { self.config_dir().join("config.toml") } - pub fn theme_file(&self) -> PathBuf { + pub fn complex_theme_file(&self) -> PathBuf { + self.config_dir().join("complex-theme.json") + } + + pub fn sample_complex_theme_file(&self) -> PathBuf { + self.config_dir().join("complex-theme.sample.json") + } + + pub fn theme_simplified_file(&self) -> PathBuf { self.config_dir().join("theme.json") } - pub fn theme_color_file(&self) -> PathBuf { - self.config_dir().join("color_theme.json") - } - - pub fn sample_theme_file(&self) -> PathBuf { + pub fn sample_simplified_theme_color_file(&self) -> PathBuf { self.config_dir().join("theme.sample.json") } - pub fn sample_theme_color_file(&self) -> PathBuf { - self.config_dir().join("color_theme.sample.json") - } - pub fn config_dir(&self) -> PathBuf { let config_dir = if cfg!(feature = "release") || cfg!(feature = "scenario_runner") { self.inner.config_dir().to_path_buf() From 3a095cfda6f5a6ff4f384199b65fd62753103509 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:48:25 +0100 Subject: [PATCH 181/540] Allow to specify border styling in simplified theme config --- rust/client/src/ui/theme/container.rs | 2 +- rust/client/src/ui/theme/date_picker.rs | 7 ++- rust/client/src/ui/theme/mod.rs | 73 +++++++++++++------------ 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index d02857a..170193b 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -250,7 +250,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau match name { ContainerStyle::RootInner => { - self.padding(theme.root_content.padding.to_iced()) + self.padding(0.0) } ContainerStyle::ActionPanelTitle => { self.padding(theme.action_panel_title.padding.to_iced()) diff --git a/rust/client/src/ui/theme/date_picker.rs b/rust/client/src/ui/theme/date_picker.rs index 5bed502..993a7f7 100644 --- a/rust/client/src/ui/theme/date_picker.rs +++ b/rust/client/src/ui/theme/date_picker.rs @@ -15,13 +15,14 @@ impl date_picker::StyleSheet for GauntletComplexTheme { fn active(&self, _: &Self::Style) -> Appearance { let theme = get_theme(); + let root_theme = &theme.root; let theme = &theme.form_input_date_picker; Appearance { background: theme.background_color.to_iced().into(), - border_radius: theme.border_radius, - border_width: theme.border_width, - border_color: theme.border_color.to_iced(), + border_radius: root_theme.border_radius, + border_width: root_theme.border_width, + border_color: root_theme.border_color.to_iced(), text_color: theme.text_color.to_iced(), text_attenuated_color: theme.text_attenuated_color.to_iced(), day_background: theme.day_background_color.to_iced().into(), diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 71c6373..8aef34f 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -39,6 +39,10 @@ pub struct GauntletSimplifiedTheme { text_darkest_color: ThemeColor, primary_darker_color: ThemeColor, primary_lighter_color: ThemeColor, + root_border_radius: f32, + root_border_width: f32, + root_border_color: ThemeColor, + content_border_radius: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -111,7 +115,6 @@ pub struct GauntletComplexTheme { root_bottom_panel_action_toggle_button: ThemeButton, root_bottom_panel_action_toggle_text: ThemePaddingTextColor, root_bottom_panel_primary_action_text: ThemePaddingTextColor, - root_content: ThemePaddingOnly, root_top_panel: ThemePaddingSpacing, root_top_panel_button: ThemeButton, metadata_link: ThemeLink, @@ -220,6 +223,10 @@ impl GauntletComplexTheme { text_darkest_color: TEXT_DARKEST, primary_darker_color: PRIMARY, primary_lighter_color: PRIMARY_HOVERED, + root_border_radius: 10.0, + root_border_width: 1.0, + root_border_color: BACKGROUND_LIGHTER, + content_border_radius: BUTTON_BORDER_RADIUS, } } @@ -236,6 +243,10 @@ impl GauntletComplexTheme { text_darkest_color, primary_darker_color, primary_lighter_color, + root_border_width, + root_border_radius, + root_border_color, + content_border_radius, } = simplified_theme; GauntletComplexTheme { @@ -243,9 +254,9 @@ impl GauntletComplexTheme { text: text_lightest_color, root: ThemeRoot { background_color: background_darkest_color, - border_radius: 10.0, - border_width: 1.0, - border_color: background_lighter_color, + border_radius: root_border_radius, + border_width: root_border_width, + border_color: root_border_color, }, action_panel: ThemePaddingBackgroundColor { padding: padding_all(8.0), @@ -261,7 +272,7 @@ impl GauntletComplexTheme { background_color_hovered: background_lighter_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, - border_radius: BUTTON_BORDER_RADIUS, + border_radius: content_border_radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -272,7 +283,7 @@ impl GauntletComplexTheme { padding: padding_axis(0.0, 8.0), spacing: 8.0, background_color: background_lightest_color, - border_radius: 4.0, + border_radius: content_border_radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -289,7 +300,7 @@ impl GauntletComplexTheme { background_color_hovered: primary_lighter_color, text_color: text_darkest_color, text_color_hovered: text_darkest_color, - border_radius: BUTTON_BORDER_RADIUS, + border_radius: content_border_radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -319,7 +330,7 @@ impl GauntletComplexTheme { }, content_image: ThemeImage { padding: padding_all(0.0), - border_radius: 6.0, + border_radius: content_border_radius, }, inline: ThemePaddingOnly { padding: padding_axis(0.0, 8.0), @@ -334,7 +345,7 @@ impl GauntletComplexTheme { inline_inner: ThemeInline { padding: padding_all(8.0), background_color: background_lighter_color, - border_radius: BUTTON_BORDER_RADIUS, + border_radius: content_border_radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -352,7 +363,7 @@ impl GauntletComplexTheme { background_color_hovered: background_lightest_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, - border_radius: BUTTON_BORDER_RADIUS, + border_radius: content_border_radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -369,7 +380,7 @@ impl GauntletComplexTheme { content_code_block_text: ThemeCode { padding: padding_axis(4.0, 8.0), background_color: background_lighter_color, - border_radius: 4.0, + border_radius: content_border_radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -387,7 +398,7 @@ impl GauntletComplexTheme { background_color_hovered: background_lightest_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, - border_radius: 6.0, + border_radius: content_border_radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -403,7 +414,7 @@ impl GauntletComplexTheme { background_color_hovered: background_lighter_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, - border_radius: 6.0, + border_radius: content_border_radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -422,16 +433,13 @@ impl GauntletComplexTheme { background_color_hovered: background_darker_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, - border_radius: BUTTON_BORDER_RADIUS, + border_radius: content_border_radius, border_width: 0.0, border_color: TRANSPARENT, }, list_item_icon: ThemePaddingOnly { padding: padding_axis(0.0, 4.0) }, - root_content: ThemePaddingOnly { - padding: padding_all(0.0), // TODO hardcode this? - }, detail_metadata: ThemePaddingOnly { padding: padding_axis(0.0, 12.0), }, @@ -489,7 +497,7 @@ impl GauntletComplexTheme { background_color_hovered: background_darker_color, text_color: text_lightest_color, text_color_hovered: text_lightest_color, - border_radius: BUTTON_BORDER_RADIUS, + border_radius: content_border_radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -530,9 +538,6 @@ impl GauntletComplexTheme { }, form_input_date_picker: ThemeDatePicker { background_color: background_darkest_color, - border_radius: 10.0, - border_width: 1.0, - border_color: background_lighter_color, text_color: text_lightest_color, text_color_selected: text_darker_color, text_color_hovered: text_darker_color, @@ -548,7 +553,7 @@ impl GauntletComplexTheme { background_color_hovered: primary_lighter_color, text_color: text_darkest_color, text_color_hovered: text_darkest_color, - border_radius: BUTTON_BORDER_RADIUS, + border_radius: content_border_radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -557,8 +562,8 @@ impl GauntletComplexTheme { background_color_unchecked: background_darkest_color, background_color_checked_hovered: primary_lighter_color, background_color_unchecked_hovered: background_lighter_color, - border_radius: 4.0, - border_width: 1.0, + border_radius: content_border_radius, + border_width: root_border_width, border_color: primary_darker_color, icon_color: background_darkest_color, }, @@ -567,8 +572,8 @@ impl GauntletComplexTheme { background_color_hovered: primary_lighter_color, text_color: text_darkest_color, text_color_hovered: text_darkest_color, - border_radius: BUTTON_BORDER_RADIUS, - border_width: 1.0, + border_radius: content_border_radius, + border_width: root_border_width, border_color: background_lighter_color, }, form_input_select_menu: ThemeSelectMenu { @@ -576,8 +581,8 @@ impl GauntletComplexTheme { background_color_selected: background_lighter_color, text_color: text_lightest_color, text_color_selected: text_lightest_color, - border_radius: BUTTON_BORDER_RADIUS, - border_width: 1.0, + border_radius: content_border_radius, + border_width: root_border_width, border_color: background_lighter_color, }, form_input_text_field: ThemeTextField { @@ -586,8 +591,8 @@ impl GauntletComplexTheme { text_color: text_lightest_color, text_color_placeholder: text_darker_color, selection_color: background_lighter_color, - border_radius: 4.0, - border_width: 1.0, + border_radius: content_border_radius, + border_width: root_border_width, border_color: background_lighter_color, border_color_hovered: background_lighter_color, }, @@ -596,7 +601,7 @@ impl GauntletComplexTheme { }, scrollbar: ThemeScrollbar { color: primary_darker_color, - border_radius: 4.0, + border_radius: content_border_radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -655,7 +660,7 @@ const TEXT_DARKEST: ThemeColor = ThemeColor::new(0x1D242C, 1.0); const PRIMARY: ThemeColor = ThemeColor::new(0xC79F60, 1.0); const PRIMARY_HOVERED: ThemeColor = ThemeColor::new(0xD7B37A, 1.0); -const BUTTON_BORDER_RADIUS: f32 = 6.0; +const BUTTON_BORDER_RADIUS: f32 = 4.0; const fn padding(top: f32, right: f32, bottom: f32, left: f32) -> ThemePadding { ThemePadding::Each { @@ -814,10 +819,6 @@ pub struct ThemeInline { pub struct ThemeDatePicker { background_color: ThemeColor, - border_radius: f32, - border_width: f32, - border_color: ThemeColor, - text_color: ThemeColor, text_color_selected: ThemeColor, text_color_hovered: ThemeColor, From 6fe97d7afe8c7f33f276368c86d935dbbe13c4c9 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:00:39 +0100 Subject: [PATCH 182/540] Split assetData helper function into sync and async variants --- js/api/src/helpers.ts | 8 ++++++-- rust/client/src/ui/scroll_handle.rs | 2 -- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index b6567ca..9091279 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -1,14 +1,18 @@ // @ts-ignore TODO how to add declaration for this? -import { getAssetDataSync, getPluginPreferences, getEntrypointPreferences, showHudWindow } from "gauntlet:renderer"; +import { getAssetData, getAssetDataSync, getPluginPreferences, getEntrypointPreferences, showHudWindow } from "gauntlet:renderer"; // @ts-expect-error does typescript support such symbol declarations? const denoCore: DenoCore = Deno[Deno.internal].core; const InternalApi = denoCore.ops; -export function assetData(path: string): ArrayBuffer { +export function assetDataSync(path: string): ArrayBuffer { return getAssetDataSync(path) } +export function assetData(path: string): Promise { + return getAssetData(path) +} + export function pluginPreferences>(): T { return getPluginPreferences() } diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs index 4c71b0d..6ef6c29 100644 --- a/rust/client/src/ui/scroll_handle.rs +++ b/rust/client/src/ui/scroll_handle.rs @@ -116,8 +116,6 @@ impl ScrollHandle { pub fn scroll_to(&self, row_index: usize) -> Command { let mut pos_y = row_index as f32 * self.item_height - (self.offset as f32 * self.item_height); - println!("pos_y: {}, row_index: {}, item_height: {}, offset: {}", pos_y, row_index, self.item_height, self.offset); - scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) } } \ No newline at end of file From 72bc21716b17d246c8c9d88dd9011a5352c23759 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:14:04 +0100 Subject: [PATCH 183/540] Implement plugin environment --- dev_plugin/src/list-view.tsx | 7 +++++ js/api/src/helpers.ts | 23 +++++++++++++++ js/typings/index.d.ts | 5 ++++ rust/server/src/plugins/js/environment.rs | 36 +++++++++++++++++++++++ rust/server/src/plugins/js/mod.rs | 35 +++++++++++++++++++++- 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 rust/server/src/plugins/js/environment.rs diff --git a/dev_plugin/src/list-view.tsx b/dev_plugin/src/list-view.tsx index 81e0d45..91173c5 100644 --- a/dev_plugin/src/list-view.tsx +++ b/dev_plugin/src/list-view.tsx @@ -1,5 +1,6 @@ import { IconAccessory, Icons, List, TextAccessory } from "@project-gauntlet/api/components"; import { ReactElement, useState } from "react"; +import { Environment } from "@project-gauntlet/api/helpers"; export default function ListView(): ReactElement { const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; @@ -35,6 +36,12 @@ export default function ListView(): ReactElement { + { + console.log(Environment.gauntletVersion); + console.log(Environment.isDevelopment); + console.log(Environment.pluginCacheDir); + console.log(Environment.pluginDataDir); + }}/> diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index 9091279..b832bc2 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -85,3 +85,26 @@ export interface Clipboard { writeText(data: string): Promise; clear(): Promise; } + +export const Environment: Environment = { + get gauntletVersion(): number { + return InternalApi.environment_gauntlet_version() + }, + get isDevelopment(): boolean { + return InternalApi.environment_is_development() + }, + get pluginDataDir(): string { + return InternalApi.environment_plugin_data_dir() + }, + get pluginCacheDir(): string { + return InternalApi.environment_plugin_cache_dir() + }, +} + +export interface Environment { + get gauntletVersion(): number; + get isDevelopment(): boolean; + get pluginDataDir(): string; + get pluginCacheDir(): string; +} + diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 7e9cf3e..c190871 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -132,6 +132,11 @@ interface InternalApi { clipboard_write(data: { text_data?: string, png_data?: number[] }): Promise; clipboard_write_text(data: string): Promise; clipboard_clear(): Promise; + + environment_gauntlet_version(): number; + environment_is_development(): boolean; + environment_plugin_data_dir(): string; + environment_plugin_cache_dir(): string; } // component model types diff --git a/rust/server/src/plugins/js/environment.rs b/rust/server/src/plugins/js/environment.rs new file mode 100644 index 0000000..352cdb2 --- /dev/null +++ b/rust/server/src/plugins/js/environment.rs @@ -0,0 +1,36 @@ +use crate::plugins::js::PluginData; +use deno_core::{op, OpState}; + +#[op] +fn environment_gauntlet_version() -> u16 { + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../VERSION")) + .parse() + .expect("version is not a number?") +} + +#[op] +fn environment_is_development(state: &mut OpState) -> bool { + let plugin_id = state + .borrow::() + .plugin_id(); + + plugin_id + .to_string() + .starts_with("file://") +} + +#[op] +fn environment_plugin_data_dir(state: &mut OpState) -> String { + state + .borrow::() + .plugin_data_dir() + .to_string() +} + +#[op] +fn environment_plugin_cache_dir(state: &mut OpState) -> String { + state + .borrow::() + .plugin_cache_dir() + .to_string() +} \ No newline at end of file diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 173e168..bd1b5e0 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -38,6 +38,7 @@ use crate::plugins::icon_cache::IconCache; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text, Clipboard}; use crate::plugins::js::command_generators::{get_command_generator_entrypoint_ids}; +use crate::plugins::js::environment::{environment_gauntlet_version, environment_is_development, environment_plugin_cache_dir, environment_plugin_data_dir}; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; use crate::plugins::js::permissions::{permissions_to_deno, PluginPermissions, PluginPermissionsClipboard}; use crate::plugins::js::plugins::numbat::{run_numbat, NumbatContext}; @@ -57,6 +58,7 @@ mod search; mod command_generators; pub mod clipboard; pub mod permissions; +mod environment; pub struct PluginRuntimeData { pub id: PluginId, @@ -314,6 +316,8 @@ async fn start_js_runtime( }; let local_storage_dir = dirs.plugin_local_storage(&plugin_uuid); + let plugin_cache_dir = dirs.plugin_cache(&plugin_uuid)?.to_str().expect("non-uft8 paths are not supported").to_string(); + let plugin_data_dir = dirs.plugin_data(&plugin_uuid)?.to_str().expect("non-uft8 paths are not supported").to_string(); // let _inspector_server = Arc::new( // InspectorServer::new( @@ -348,7 +352,16 @@ async fn start_js_runtime( module_loader: Rc::new(CustomModuleLoader::new(code, dev_plugin)), extensions: vec![plugin_ext::init_ops_and_esm( EventReceiver::new(event_stream), - PluginData::new(plugin_id, plugin_uuid, plugin_name, entrypoint_names, inline_view_entrypoint_id, runtime_permissions), + PluginData::new( + plugin_id, + plugin_uuid, + plugin_name, + entrypoint_names, + plugin_cache_dir, + plugin_data_dir, + inline_view_entrypoint_id, + runtime_permissions, + ), frontend_api, ComponentModel::new(component_model), repository, @@ -565,6 +578,12 @@ deno_core::extension!( // plugins settings open_settings, + + // plugin environment + environment_gauntlet_version, + environment_is_development, + environment_plugin_data_dir, + environment_plugin_cache_dir, ], options = { event_receiver: EventReceiver, @@ -761,6 +780,8 @@ pub struct PluginData { plugin_uuid: String, plugin_name: String, entrypoint_names: HashMap, + plugin_cache_dir: String, + plugin_data_dir: String, inline_view_entrypoint_id: Option, permissions: PluginRuntimePermissions } @@ -771,6 +792,8 @@ impl PluginData { plugin_uuid: String, plugin_name: String, entrypoint_names: HashMap, + plugin_cache_dir: String, + plugin_data_dir: String, inline_view_entrypoint_id: Option, permissions: PluginRuntimePermissions ) -> Self { @@ -779,6 +802,8 @@ impl PluginData { plugin_uuid, plugin_name, entrypoint_names, + plugin_cache_dir, + plugin_data_dir, inline_view_entrypoint_id, permissions } @@ -796,6 +821,14 @@ impl PluginData { &self.plugin_name } + fn plugin_cache_dir(&self) -> &str { + &self.plugin_cache_dir + } + + fn plugin_data_dir(&self) -> &str { + &self.plugin_data_dir + } + fn inline_view_entrypoint_id(&self) -> Option { self.inline_view_entrypoint_id.clone() } From c340a69b5b686600070a99d0b031915b2a9a7463 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 16 Nov 2024 09:53:51 +0100 Subject: [PATCH 184/540] Restrict plugin id schema to http(s), ssh and git, exclude file and unknown --- rust/common/src/model.rs | 12 +++++++++++- rust/management_client/src/views/plugins.rs | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 7750fae..4216c84 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -26,7 +26,17 @@ impl PluginId { pub fn try_to_git_url(&self) -> anyhow::Result { let url = self.try_to_url()?; - Ok(url.to_bstring().to_string()) + match url.scheme { + Scheme::Git | Scheme::Ssh | Scheme::Http | Scheme::Https => { + Ok(url.to_bstring().to_string()) + } + Scheme::File => { + Err(anyhow!("'file' schema is not supported")) + } + Scheme::Ext(schema) => { + Err(anyhow!("'{}' schema is not supported", schema)) + } + } } pub fn try_to_path(&self) -> anyhow::Result { diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index 8270d1e..fc5d898 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -498,7 +498,7 @@ impl ManagementAppPluginsState { let content: Element<_> = column(vec![ url_input, text("Supported protocols:").into(), - text("file, http(s), ssh").into(), + text("http(s), ssh, git").into(), ]).into(); container(content) From 585599095216f2030bead748c02e9c05973ec3fa Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 16 Nov 2024 19:33:11 +0100 Subject: [PATCH 185/540] Rename Theme into Simplified theme to avoid confusion --- rust/cli/src/lib.rs | 6 +++--- rust/client/src/lib.rs | 18 +++++++++--------- rust/client/src/ui/theme/mod.rs | 26 +++++++++++++------------- rust/common/src/dirs.rs | 8 ++++---- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs index c6e60fa..c774bce 100644 --- a/rust/cli/src/lib.rs +++ b/rust/cli/src/lib.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Context}; use clap::Parser; -use client::{generate_simplified_theme_sample, generate_complex_theme_sample, open_window}; +use client::{generate_simple_theme_sample, generate_complex_theme_sample, open_window}; use management_client::start_management_client; use server::start; @@ -19,7 +19,7 @@ enum Commands { Open, Settings, GenerateSampleComplexTheme, - GenerateSampleTheme, + GenerateSampleSimpleTheme, } pub fn init() { @@ -49,7 +49,7 @@ pub fn init() { Commands::Open => open_window(), Commands::Settings => start_management_client(), Commands::GenerateSampleComplexTheme => generate_complex_theme_sample().expect("Unable to generate complex theme sample"), - Commands::GenerateSampleTheme => generate_simplified_theme_sample().expect("Unable to generate simplified theme sample") + Commands::GenerateSampleSimpleTheme => generate_simple_theme_sample().expect("Unable to generate simple theme sample") }; } } diff --git a/rust/client/src/lib.rs b/rust/client/src/lib.rs index ecc0c9a..c8dde8d 100644 --- a/rust/client/src/lib.rs +++ b/rust/client/src/lib.rs @@ -66,7 +66,7 @@ pub fn generate_complex_theme_sample() -> anyhow::Result<()> { let sample_complex_theme_file = dirs.sample_complex_theme_file(); let complex_theme_file = dirs.complex_theme_file(); - let theme_complex = GauntletComplexTheme::default_theme(GauntletComplexTheme::default_simplified_theme()); + let theme_complex = GauntletComplexTheme::default_theme(GauntletComplexTheme::default_simple_theme()); let string = serde_json::to_string_pretty(&theme_complex)?; @@ -84,26 +84,26 @@ pub fn generate_complex_theme_sample() -> anyhow::Result<()> { Ok(()) } -pub fn generate_simplified_theme_sample() -> anyhow::Result<()> { +pub fn generate_simple_theme_sample() -> anyhow::Result<()> { let dirs = Dirs::new(); - let sample_simplified_theme_file = dirs.sample_simplified_theme_color_file(); - let simplified_theme_file = dirs.theme_simplified_file(); + let sample_simple_theme_file = dirs.sample_simple_theme_color_file(); + let simple_theme_file = dirs.theme_simple_file(); - let theme = GauntletComplexTheme::default_simplified_theme(); + let theme = GauntletComplexTheme::default_simple_theme(); let string = serde_json::to_string_pretty(&theme)?; - let sample_theme_parent = sample_simplified_theme_file + let sample_theme_parent = sample_simple_theme_file .parent() .expect("no parent?"); std::fs::create_dir_all(sample_theme_parent)?; - std::fs::write(&sample_simplified_theme_file, string)?; + std::fs::write(&sample_simple_theme_file, string)?; - println!("Created sample using default simplified theme at {:?}", sample_simplified_theme_file); - println!("Make changes and rename file to {:?}", simplified_theme_file.file_name().unwrap()); + println!("Created sample using default simple theme at {:?}", sample_simple_theme_file); + println!("Make changes and rename file to {:?}", simple_theme_file.file_name().unwrap()); Ok(()) } \ No newline at end of file diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 8aef34f..84413de 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -23,11 +23,11 @@ mod loading_bar; pub type Element<'a, Message> = iced::Element<'a, Message, GauntletComplexTheme>; -const CURRENT_SIMPLIFIED_THEME_VERSION: u64 = 4; +const CURRENT_SIMPLE_THEME_VERSION: u64 = 4; const CURRENT_COMPLEX_THEME_VERSION: u64 = 4; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GauntletSimplifiedTheme { +pub struct GauntletSimpleTheme { version: u64, background_darkest_color: ThemeColor, background_darker_color: ThemeColor, @@ -148,7 +148,7 @@ fn parse_json_theme(theme_file: PathBuf, theme_ tracing::warn!("Version of read {} file is invalid", theme_name); None } - Some(CURRENT_SIMPLIFIED_THEME_VERSION) => { + Some(CURRENT_SIMPLE_THEME_VERSION) => { match serde_json::from_value::(value) { Ok(value) => Some(value), Err(err) => { @@ -158,7 +158,7 @@ fn parse_json_theme(theme_file: PathBuf, theme_ } } Some(_) => { - tracing::warn!("Version of read {} file doesn't match expected, theme: {}, expected: {}", theme_name, number, CURRENT_SIMPLIFIED_THEME_VERSION); + tracing::warn!("Version of read {} file doesn't match expected, theme: {}, expected: {}", theme_name, number, CURRENT_SIMPLE_THEME_VERSION); None } } @@ -199,10 +199,10 @@ impl GauntletComplexTheme { let theme = parse_json_theme(dirs.complex_theme_file(), "complex theme") .unwrap_or_else(|| { - let simplified_theme = parse_json_theme(dirs.theme_simplified_file(), "simplified theme") - .unwrap_or_else(|| GauntletComplexTheme::default_simplified_theme()); + let simple_theme = parse_json_theme(dirs.theme_simple_file(), "simple theme") + .unwrap_or_else(|| GauntletComplexTheme::default_simple_theme()); - GauntletComplexTheme::default_theme(simplified_theme) + GauntletComplexTheme::default_theme(simple_theme) }); init_theme(theme.clone()); @@ -210,9 +210,9 @@ impl GauntletComplexTheme { theme } - pub fn default_simplified_theme() -> GauntletSimplifiedTheme { - GauntletSimplifiedTheme { - version: CURRENT_SIMPLIFIED_THEME_VERSION, + pub fn default_simple_theme() -> GauntletSimpleTheme { + GauntletSimpleTheme { + version: CURRENT_SIMPLE_THEME_VERSION, background_lightest_color: BACKGROUND_LIGHTEST, background_lighter_color: BACKGROUND_LIGHTER, background_darker_color: BACKGROUND_DARKER, @@ -230,8 +230,8 @@ impl GauntletComplexTheme { } } - pub fn default_theme(simplified_theme: GauntletSimplifiedTheme) -> GauntletComplexTheme { - let GauntletSimplifiedTheme { + pub fn default_theme(simple_theme: GauntletSimpleTheme) -> GauntletComplexTheme { + let GauntletSimpleTheme { version: _, background_darkest_color, background_darker_color, @@ -247,7 +247,7 @@ impl GauntletComplexTheme { root_border_radius, root_border_color, content_border_radius, - } = simplified_theme; + } = simple_theme; GauntletComplexTheme { version: CURRENT_COMPLEX_THEME_VERSION, diff --git a/rust/common/src/dirs.rs b/rust/common/src/dirs.rs index 20f9cf9..994bf9e 100644 --- a/rust/common/src/dirs.rs +++ b/rust/common/src/dirs.rs @@ -63,12 +63,12 @@ impl Dirs { self.config_dir().join("complex-theme.sample.json") } - pub fn theme_simplified_file(&self) -> PathBuf { - self.config_dir().join("theme.json") + pub fn theme_simple_file(&self) -> PathBuf { + self.config_dir().join("simple-theme.json") } - pub fn sample_simplified_theme_color_file(&self) -> PathBuf { - self.config_dir().join("theme.sample.json") + pub fn sample_simple_theme_color_file(&self) -> PathBuf { + self.config_dir().join("simple-theme.sample.json") } pub fn config_dir(&self) -> PathBuf { From ef38ad4a5e8e171970b49411ec533b688e4b69f9 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 16 Nov 2024 20:16:52 +0100 Subject: [PATCH 186/540] Fix generated command entrypoint not being removed after disabling entrypoint in Settings UI --- js/core/src/command-generator.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index eca654e..2076f0b 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -44,6 +44,9 @@ export async function runCommandGenerators(): Promise { storedGeneratedCommands = {} generatorCleanups = {} + // noinspection ES6MissingAwait + reloadSearchIndex(true) + const entrypointIds = await InternalApi.get_command_generator_entrypoint_ids(); for (const generatorEntrypointId of entrypointIds) { try { From 3bb5be6cafd40b97de72c80a6be34248af299d56 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 16 Nov 2024 20:20:18 +0100 Subject: [PATCH 187/540] Update CHANGELOG.md and THEME.md --- CHANGELOG.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++-- docs/THEME.md | 25 +++++++++++++--------- 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eee2448..10c6d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,64 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +### General +- Primary action label on bottom bar is now a clickable button +- It is now possible to unset global shortcut in settings +- Implemented keyboard navigation support for `` and `` + - Note: `` scrolling while using keyboard navigation is still quite buggy and is work in progress + +### Bundled plugin +#### `Applications` +- Applications commands are now automatically added or removed when application is installed or uninstalled respectively +- When loading the list of applications from OS, loading bar and "Indexing..." text in bottom panel is shown in main window + +### Plugin API +- Added `` component in `` and `` which is text input field above content of the respective view +- `"command-generator"` entrypoints have been reworked + - Now it is possible to update list of generated entrypoints (add or remove) after the main command generator function has finished running + - **BREAKING CHANGE**: Command Generator entrypoint function now accepts an object with `add: (id: string, data: GeneratedCommand) => void` and `remove: (id: string) => void` functions + - **BREAKING CHANGE**: Command Generator entrypoint function now should return nothing or a cleanup function e.g. close file watcher. Currently, it is called when disabling/enabling any of entrypoints in plugin, but it is not called when whole plugin is stopped + - While generator function itself is running (given that the function is async) the loading bar and "Indexing..." text in bottom panel will be shown in main window +- **BREAKING CHANGE**: Validation of React Component children is now a lot more strict with respect to what amount of children of specific type is allowed +- **BREAKING CHANGE**: `assetData` helper function renamed to `assetDataSync` +- Added async variant of `assetDataSync` helper function named `assetData` which returns `Promise` +- Added Plugin Environment API + - `Environment.gauntletVersion` - `number`, current Gauntlet version + - `Environment.isDevelopment` - `boolean`, `true` if plugin was added with `npm run dev` as opposed to Settings UI + - `Environment.pluginCacheDir` - `string`, path to plugin cache directory, corresponds to `{common:plugin-cache}` variable in permissions + - `Environment.pluginDataDir` - `string`, path to plugin data directory, corresponds to `{common:plugin-data}` variable in permissions + +### Theming API +- Themes slightly reworked + - "Color Theme" is renamed into "Simple Theme" + - **BREAKING CHANGE**: Sample generation CLI command was changed to `gauntlet generate-sample-simple-theme` + - **BREAKING CHANGE**: Theme file is renamed from `color_theme.json` to `simple-theme.json` + - "Everything Theme" or just "Theme" is renamed into "Complex Theme" + - **BREAKING CHANGE**: Sample generation CLI command was changed to `gauntlet generate-sample-complex-theme` + - **BREAKING CHANGE**: Theme file is renamed from `theme.json` to `complex-theme.json` + - It is now possible to customize color, width and radius of borders in Simple Theme +- **BREAKING CHANGE**: Current Simple Theme version increased to `4` +- **BREAKING CHANGE**: Current Complex Theme version increased to `4` + +### UI/UX Improvements +- Loading bar is now shown if opening plugin view takes more than 300 milliseconds +- Pressed button state now has distinct styling, providing more clear indication that button was pressed +- When registering global shortcut in settings fails, instead of showing error on whole settings screen now icon with on hover text is shown to the right of the setting field +- If registering global shortcut on application startup fails, error is now also shown in settings +- Fixed padding of grid and list section being too far down if it is first in the view +- Fixed incorrect supported schemas label in Settings UI, http(s), ssh and git are the only supported schemas for plugin IDs +- Better error for not supported plugin ID schemas + +### Fixes +- Fixed global shortcut not working on Windows +- Fixed emojis not working in a lot of places across the application +- Fixed hud window not disappearing on Wayland +- Fixed clipboard operations not working on KDE + - Note: `Clipboard.clear()` or `Clipboard.writeText("")` is still not working on KDE due to upstream bug +- Fixed clipboard operations not working on Wayland +- Fix primary action of first search result being called if primary action of inline view is called using enter key +- Fix scrollable resetting when clicking action panel button in bottom panel + ## [10] - 2024-10-13 ### General - Main view now has action bar and action panel diff --git a/README.md b/README.md index 43e8e59..a7bd5bb 100644 --- a/README.md +++ b/README.md @@ -304,8 +304,8 @@ The Application has a simple command line interface - `gauntlet --minimized` - starts server without opening main window - `gauntlet open` - opens application window, can be used instead of global shortcut - `gauntlet settings` - settings, plugin installation and removal, preferences, etc -- `gauntlet generate-sample-color-theme` - generate sample color theme. See: [THEME.md](./docs/THEME.md) -- `gauntlet generate-sample-theme` - generate sample theme. See: [THEME.md](./docs/THEME.md) +- `gauntlet generate-sample-simple-theme` - generate sample of simple theme. See: [THEME.md](./docs/THEME.md) +- `gauntlet generate-sample-complex-theme` - generate sample of complex theme. See: [THEME.md](./docs/THEME.md) ### Dev Tools diff --git a/docs/THEME.md b/docs/THEME.md index 3d61706..b911de6 100644 --- a/docs/THEME.md +++ b/docs/THEME.md @@ -3,8 +3,13 @@ Gauntlet has extensive theming possibilities There are 2 types of themes: -- Color only -- Everything: Color, paddings, borders, etc +- Simple Theme + - Global colors + - Global border width and radius +- Complex Theme + - Colors on per widget type bases + - Paddings and spacing on per widget type bases + - Borders colors, width and radius on per widget type bases Unfortunately due to the internally invasive nature of themes, it is perpetually unstable feature. Themes are versioned and only one version is supported by application at the same time. @@ -12,15 +17,15 @@ Meaning if there were some changes made in the release and theme version was inc theme will stop working until it is updated. This may change in the future -Current theme version: -- Color: `3` -- Everything: `3` +Current version: +- Simple Theme: `4` +- Complex Theme: `4` Theming is only applied to main window and doesn't affect settings ### Creating a custom theme -Gauntlet provides 2 CLI commands to generate sample: `generate-sample-color-theme` and `generate-sample-color-theme`. Sample is just a default theme that has been saved to file. +Gauntlet provides 2 CLI commands to generate sample: `generate-sample-simple-theme` and `generate-sample-complex-theme`. Sample is just a default theme that has been saved to file. Running the command will create sample file, print location of that sample file and will print location to which theme file will need to be saved to be detected by application @@ -30,10 +35,10 @@ Currently, theme change is only applied after application restart Any errors in theme parsing will be shown in application logs #### Linux -- `gauntlet generate-sample-color-theme` -- `gauntlet generate-sample-theme` +- `gauntlet generate-sample-simple-theme` +- `gauntlet generate-sample-complex-theme` #### macOS Note: the binary is not on the PATH -- `/Applications/Gauntlet.app/Contents/MacOS/Gauntlet generate-sample-color-theme` -- `/Applications/Gauntlet.app/Contents/MacOS/Gauntlet generate-sample-theme` +- `/Applications/Gauntlet.app/Contents/MacOS/Gauntlet generate-sample-simple-theme` +- `/Applications/Gauntlet.app/Contents/MacOS/Gauntlet generate-sample-complex-theme` From 5c8f13184b0946585a7ca0bd40cd59229fc08a9c Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 16 Nov 2024 20:23:05 +0100 Subject: [PATCH 188/540] Await on reloadSearchIndex after running cleanup functions and before running generators --- js/core/src/command-generator.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index 2076f0b..58e861c 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -44,8 +44,7 @@ export async function runCommandGenerators(): Promise { storedGeneratedCommands = {} generatorCleanups = {} - // noinspection ES6MissingAwait - reloadSearchIndex(true) + await reloadSearchIndex(true) const entrypointIds = await InternalApi.get_command_generator_entrypoint_ids(); for (const generatorEntrypointId of entrypointIds) { From 12857247196ccb5ff9c36bb66a6038a26428b14a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 16 Nov 2024 19:34:08 +0000 Subject: [PATCH 189/540] Prepare for v11 release --- CHANGELOG.md | 2 ++ VERSION | 2 +- js/api/package.json | 2 +- js/deno/package.json | 2 +- package-lock.json | 4 ++-- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10c6d33..4388769 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +## [11] - 2024-11-16 + ### General - Primary action label on bottom bar is now a clickable button - It is now possible to unset global shortcut in settings diff --git a/VERSION b/VERSION index 9a03714..9d60796 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -10 \ No newline at end of file +11 \ No newline at end of file diff --git a/js/api/package.json b/js/api/package.json index 2feac9d..55547d7 100644 --- a/js/api/package.json +++ b/js/api/package.json @@ -1,6 +1,6 @@ { "name": "@project-gauntlet/api", - "version": "0.10.0", + "version": "0.11.0", "type": "module", "exports": { "./components": { diff --git a/js/deno/package.json b/js/deno/package.json index 7452087..910dc3a 100644 --- a/js/deno/package.json +++ b/js/deno/package.json @@ -1,6 +1,6 @@ { "name": "@project-gauntlet/deno", - "version": "0.10.0", + "version": "0.11.0", "type": "module", "exports": { ".": { diff --git a/package-lock.json b/package-lock.json index 438baee..d791e13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ }, "js/api": { "name": "@project-gauntlet/api", - "version": "0.10.0", + "version": "0.11.0", "devDependencies": { "@project-gauntlet/typings": "*", "typescript": "^5.3.3" @@ -101,7 +101,7 @@ }, "js/deno": { "name": "@project-gauntlet/deno", - "version": "0.10.0", + "version": "0.11.0", "devDependencies": { "@types/node": "^18.17.1", "typescript": "^5.3.3" From 950f8ebfa39d36103bec398d72982f7db16de541 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 16 Nov 2024 21:24:02 +0100 Subject: [PATCH 190/540] Add missed changelog item --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4388769..7bd4d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git - Fixed padding of grid and list section being too far down if it is first in the view - Fixed incorrect supported schemas label in Settings UI, http(s), ssh and git are the only supported schemas for plugin IDs - Better error for not supported plugin ID schemas +- `` height is now dynamic and is based on `` or `` `columns` property ### Fixes - Fixed global shortcut not working on Windows From c492783f0befa905d5d128f37a506b013686e9f6 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:29:54 +0100 Subject: [PATCH 191/540] Remove docs-code-segment from most scenarios --- scenarios/plugins/docs_detail/src/content.tsx | 2 -- scenarios/plugins/docs_detail/src/content_code_block.tsx | 3 --- scenarios/plugins/docs_detail/src/content_header.tsx | 2 -- scenarios/plugins/docs_detail/src/content_horizontal_break.tsx | 2 -- scenarios/plugins/docs_detail/src/content_image.tsx | 2 -- scenarios/plugins/docs_detail/src/content_paragraph.tsx | 2 -- scenarios/plugins/docs_detail/src/main.tsx | 2 -- scenarios/plugins/docs_detail/src/metadata.tsx | 2 -- scenarios/plugins/docs_detail/src/metadata_icon.tsx | 2 -- scenarios/plugins/docs_detail/src/metadata_link.tsx | 2 -- scenarios/plugins/docs_detail/src/metadata_separator.tsx | 2 -- scenarios/plugins/docs_detail/src/metadata_tag_list.tsx | 2 -- scenarios/plugins/docs_detail/src/metadata_value.tsx | 2 -- scenarios/plugins/docs_form/src/checkbox.tsx | 2 -- scenarios/plugins/docs_form/src/date-picker.tsx | 2 -- scenarios/plugins/docs_form/src/main.tsx | 2 -- scenarios/plugins/docs_form/src/password-field.tsx | 2 -- scenarios/plugins/docs_form/src/select.tsx | 2 -- scenarios/plugins/docs_form/src/separator.tsx | 2 -- scenarios/plugins/docs_form/src/text-field.tsx | 2 -- scenarios/plugins/docs_grid/src/empty_view.tsx | 2 -- scenarios/plugins/docs_grid/src/main.tsx | 2 -- scenarios/plugins/docs_grid/src/section.tsx | 2 -- scenarios/plugins/docs_list/src/detail.tsx | 2 -- scenarios/plugins/docs_list/src/empty_view.tsx | 2 -- scenarios/plugins/docs_list/src/main.tsx | 2 -- scenarios/plugins/docs_list/src/section.tsx | 2 -- 27 files changed, 55 deletions(-) diff --git a/scenarios/plugins/docs_detail/src/content.tsx b/scenarios/plugins/docs_detail/src/content.tsx index d21df5a..3229f11 100644 --- a/scenarios/plugins/docs_detail/src/content.tsx +++ b/scenarios/plugins/docs_detail/src/content.tsx @@ -3,7 +3,6 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( - // docs-code-segment:start @@ -22,6 +21,5 @@ export default function Main(): ReactNode { - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/content_code_block.tsx b/scenarios/plugins/docs_detail/src/content_code_block.tsx index 40a50da..1513541 100644 --- a/scenarios/plugins/docs_detail/src/content_code_block.tsx +++ b/scenarios/plugins/docs_detail/src/content_code_block.tsx @@ -1,7 +1,6 @@ import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; - const code = ` fib :: Integer -> Integer fib 0 = 0 @@ -11,7 +10,6 @@ const code = ` export default function Main(): ReactNode { return ( - // docs-code-segment:start @@ -19,6 +17,5 @@ export default function Main(): ReactNode { - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/content_header.tsx b/scenarios/plugins/docs_detail/src/content_header.tsx index 5605c12..299defd 100644 --- a/scenarios/plugins/docs_detail/src/content_header.tsx +++ b/scenarios/plugins/docs_detail/src/content_header.tsx @@ -3,7 +3,6 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( - // docs-code-segment:start @@ -26,6 +25,5 @@ export default function Main(): ReactNode { - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/content_horizontal_break.tsx b/scenarios/plugins/docs_detail/src/content_horizontal_break.tsx index a127c08..b5f0a3d 100644 --- a/scenarios/plugins/docs_detail/src/content_horizontal_break.tsx +++ b/scenarios/plugins/docs_detail/src/content_horizontal_break.tsx @@ -3,7 +3,6 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( - // docs-code-segment:start @@ -15,6 +14,5 @@ export default function Main(): ReactNode { - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/content_image.tsx b/scenarios/plugins/docs_detail/src/content_image.tsx index be0a100..2109e1c 100644 --- a/scenarios/plugins/docs_detail/src/content_image.tsx +++ b/scenarios/plugins/docs_detail/src/content_image.tsx @@ -5,12 +5,10 @@ const imgUrl = "https://static.wikia.nocookie.net/starwars/images/a/ae/The_Whill export default function Main(): ReactNode { return ( - // docs-code-segment:start - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/content_paragraph.tsx b/scenarios/plugins/docs_detail/src/content_paragraph.tsx index 42c70da..e7bfdf4 100644 --- a/scenarios/plugins/docs_detail/src/content_paragraph.tsx +++ b/scenarios/plugins/docs_detail/src/content_paragraph.tsx @@ -3,7 +3,6 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( - // docs-code-segment:start @@ -13,6 +12,5 @@ export default function Main(): ReactNode { - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/main.tsx b/scenarios/plugins/docs_detail/src/main.tsx index 4c2fe95..8073b8f 100644 --- a/scenarios/plugins/docs_detail/src/main.tsx +++ b/scenarios/plugins/docs_detail/src/main.tsx @@ -3,7 +3,6 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( - // docs-code-segment:start Sentient @@ -35,6 +34,5 @@ export default function Main(): ReactNode { - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/metadata.tsx b/scenarios/plugins/docs_detail/src/metadata.tsx index 96b5e4c..5ea91c9 100644 --- a/scenarios/plugins/docs_detail/src/metadata.tsx +++ b/scenarios/plugins/docs_detail/src/metadata.tsx @@ -3,7 +3,6 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( - // docs-code-segment:start Ezaraa @@ -21,6 +20,5 @@ export default function Main(): ReactNode { - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/metadata_icon.tsx b/scenarios/plugins/docs_detail/src/metadata_icon.tsx index a523529..48b8b41 100644 --- a/scenarios/plugins/docs_detail/src/metadata_icon.tsx +++ b/scenarios/plugins/docs_detail/src/metadata_icon.tsx @@ -3,12 +3,10 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( - // docs-code-segment:start - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/metadata_link.tsx b/scenarios/plugins/docs_detail/src/metadata_link.tsx index 00246ff..bbaef8c 100644 --- a/scenarios/plugins/docs_detail/src/metadata_link.tsx +++ b/scenarios/plugins/docs_detail/src/metadata_link.tsx @@ -3,12 +3,10 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( - // docs-code-segment:start Link - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/metadata_separator.tsx b/scenarios/plugins/docs_detail/src/metadata_separator.tsx index a0abcb7..1b0921f 100644 --- a/scenarios/plugins/docs_detail/src/metadata_separator.tsx +++ b/scenarios/plugins/docs_detail/src/metadata_separator.tsx @@ -3,7 +3,6 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( - // docs-code-segment:start Ezaraa @@ -11,6 +10,5 @@ export default function Main(): ReactNode { Sentient - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/metadata_tag_list.tsx b/scenarios/plugins/docs_detail/src/metadata_tag_list.tsx index 233d892..858a784 100644 --- a/scenarios/plugins/docs_detail/src/metadata_tag_list.tsx +++ b/scenarios/plugins/docs_detail/src/metadata_tag_list.tsx @@ -3,7 +3,6 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( - // docs-code-segment:start @@ -15,6 +14,5 @@ export default function Main(): ReactNode { - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_detail/src/metadata_value.tsx b/scenarios/plugins/docs_detail/src/metadata_value.tsx index 7c66025..0f4c7b4 100644 --- a/scenarios/plugins/docs_detail/src/metadata_value.tsx +++ b/scenarios/plugins/docs_detail/src/metadata_value.tsx @@ -3,12 +3,10 @@ import { ReactNode } from "react"; export default function Main(): ReactNode { return ( - // docs-code-segment:start David Tennant - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_form/src/checkbox.tsx b/scenarios/plugins/docs_form/src/checkbox.tsx index fc5c1ab..ddb21cc 100644 --- a/scenarios/plugins/docs_form/src/checkbox.tsx +++ b/scenarios/plugins/docs_form/src/checkbox.tsx @@ -3,7 +3,6 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - // docs-code-segment:start
- // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_form/src/date-picker.tsx b/scenarios/plugins/docs_form/src/date-picker.tsx index ac388c9..7707525 100644 --- a/scenarios/plugins/docs_form/src/date-picker.tsx +++ b/scenarios/plugins/docs_form/src/date-picker.tsx @@ -3,7 +3,6 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - // docs-code-segment:start
- // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_form/src/main.tsx b/scenarios/plugins/docs_form/src/main.tsx index 66d1fe0..42fccd1 100644 --- a/scenarios/plugins/docs_form/src/main.tsx +++ b/scenarios/plugins/docs_form/src/main.tsx @@ -3,7 +3,6 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - // docs-code-segment:start
- // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_form/src/password-field.tsx b/scenarios/plugins/docs_form/src/password-field.tsx index 05e8b2e..f77cfe8 100644 --- a/scenarios/plugins/docs_form/src/password-field.tsx +++ b/scenarios/plugins/docs_form/src/password-field.tsx @@ -3,11 +3,9 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - // docs-code-segment:start
- // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_form/src/select.tsx b/scenarios/plugins/docs_form/src/select.tsx index 033839c..ef6d34f 100644 --- a/scenarios/plugins/docs_form/src/select.tsx +++ b/scenarios/plugins/docs_form/src/select.tsx @@ -3,7 +3,6 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - // docs-code-segment:start
Burger @@ -14,7 +13,6 @@ export default function Main(): ReactElement { Seafood
- // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_form/src/separator.tsx b/scenarios/plugins/docs_form/src/separator.tsx index 3f73598..5c2b458 100644 --- a/scenarios/plugins/docs_form/src/separator.tsx +++ b/scenarios/plugins/docs_form/src/separator.tsx @@ -3,7 +3,6 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - // docs-code-segment:start
- // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_form/src/text-field.tsx b/scenarios/plugins/docs_form/src/text-field.tsx index 53553c2..d30a05b 100644 --- a/scenarios/plugins/docs_form/src/text-field.tsx +++ b/scenarios/plugins/docs_form/src/text-field.tsx @@ -3,7 +3,6 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - // docs-code-segment:start
- // docs-code-segment:end ); }; diff --git a/scenarios/plugins/docs_grid/src/empty_view.tsx b/scenarios/plugins/docs_grid/src/empty_view.tsx index ad04a63..eec1cc6 100644 --- a/scenarios/plugins/docs_grid/src/empty_view.tsx +++ b/scenarios/plugins/docs_grid/src/empty_view.tsx @@ -5,10 +5,8 @@ const alderaanImage = "https://static.wikia.nocookie.net/starwars/images/4/4a/Al export default function Main(): ReactElement { return ( - // docs-code-segment:start - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_grid/src/main.tsx b/scenarios/plugins/docs_grid/src/main.tsx index 6317c17..96b59bb 100644 --- a/scenarios/plugins/docs_grid/src/main.tsx +++ b/scenarios/plugins/docs_grid/src/main.tsx @@ -13,7 +13,6 @@ const dantooineImage = "https://static.wikia.nocookie.net/starwars/images/a/a5/D export default function Main(): ReactElement { return ( - // docs-code-segment:start @@ -61,6 +60,5 @@ export default function Main(): ReactElement { - // docs-code-segment:end ) } \ No newline at end of file diff --git a/scenarios/plugins/docs_grid/src/section.tsx b/scenarios/plugins/docs_grid/src/section.tsx index a562bba..55e79ab 100644 --- a/scenarios/plugins/docs_grid/src/section.tsx +++ b/scenarios/plugins/docs_grid/src/section.tsx @@ -16,7 +16,6 @@ const vader7 = "https://static.wikia.nocookie.net/starwars/images/f/fa/DarthVade export default function Main(): ReactElement { return ( - // docs-code-segment:start @@ -78,6 +77,5 @@ export default function Main(): ReactElement { - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_list/src/detail.tsx b/scenarios/plugins/docs_list/src/detail.tsx index 40d8b77..d5d5854 100644 --- a/scenarios/plugins/docs_list/src/detail.tsx +++ b/scenarios/plugins/docs_list/src/detail.tsx @@ -3,7 +3,6 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - // docs-code-segment:start @@ -51,6 +50,5 @@ export default function Main(): ReactElement { - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_list/src/empty_view.tsx b/scenarios/plugins/docs_list/src/empty_view.tsx index ad04a63..eec1cc6 100644 --- a/scenarios/plugins/docs_list/src/empty_view.tsx +++ b/scenarios/plugins/docs_list/src/empty_view.tsx @@ -5,10 +5,8 @@ const alderaanImage = "https://static.wikia.nocookie.net/starwars/images/4/4a/Al export default function Main(): ReactElement { return ( - // docs-code-segment:start - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_list/src/main.tsx b/scenarios/plugins/docs_list/src/main.tsx index ff80162..6bdafeb 100644 --- a/scenarios/plugins/docs_list/src/main.tsx +++ b/scenarios/plugins/docs_list/src/main.tsx @@ -3,7 +3,6 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - // docs-code-segment:start @@ -16,6 +15,5 @@ export default function Main(): ReactElement { - // docs-code-segment:end ) } diff --git a/scenarios/plugins/docs_list/src/section.tsx b/scenarios/plugins/docs_list/src/section.tsx index e2829f1..3d9db2b 100644 --- a/scenarios/plugins/docs_list/src/section.tsx +++ b/scenarios/plugins/docs_list/src/section.tsx @@ -3,7 +3,6 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - // docs-code-segment:start @@ -18,6 +17,5 @@ export default function Main(): ReactElement { - // docs-code-segment:end ) } From 2cb6be718a9f9ac6604778b4a4c33a34d4b57be3 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 21 Nov 2024 22:02:42 +0100 Subject: [PATCH 192/540] Split inline scenario into 3 --- package-lock.json | 66 +++++++++++++-- .../main/separator.json | 0 .../main/three-sections.json | 0 .../main/two-sections.json | 0 scenarios/plugins/docs_inline/src/main.tsx | 83 ------------------- .../.gitignore | 0 .../gauntlet.toml | 0 .../package.json | 2 +- .../docs_inline_separators/src/main.tsx | 20 +++++ .../tsconfig.json | 0 .../docs_inline_three_sections/.gitignore | 2 + .../docs_inline_three_sections/gauntlet.toml | 16 ++++ .../docs_inline_three_sections/package.json | 17 ++++ .../docs_inline_three_sections/src/main.tsx | 26 ++++++ .../docs_inline_three_sections/tsconfig.json | 12 +++ .../docs_inline_two_sections/.gitignore | 2 + .../docs_inline_two_sections/gauntlet.toml | 16 ++++ .../docs_inline_two_sections/package.json | 17 ++++ .../docs_inline_two_sections/src/main.tsx | 19 +++++ .../docs_inline_two_sections/tsconfig.json | 12 +++ 20 files changed, 220 insertions(+), 90 deletions(-) rename scenarios/data/{docs_inline => docs_inline_separators}/main/separator.json (100%) rename scenarios/data/{docs_inline => docs_inline_three_sections}/main/three-sections.json (100%) rename scenarios/data/{docs_inline => docs_inline_two_sections}/main/two-sections.json (100%) delete mode 100644 scenarios/plugins/docs_inline/src/main.tsx rename scenarios/plugins/{docs_inline => docs_inline_separators}/.gitignore (100%) rename scenarios/plugins/{docs_inline => docs_inline_separators}/gauntlet.toml (100%) rename scenarios/plugins/{docs_inline => docs_inline_separators}/package.json (87%) create mode 100644 scenarios/plugins/docs_inline_separators/src/main.tsx rename scenarios/plugins/{docs_inline => docs_inline_separators}/tsconfig.json (100%) create mode 100644 scenarios/plugins/docs_inline_three_sections/.gitignore create mode 100644 scenarios/plugins/docs_inline_three_sections/gauntlet.toml create mode 100644 scenarios/plugins/docs_inline_three_sections/package.json create mode 100644 scenarios/plugins/docs_inline_three_sections/src/main.tsx create mode 100644 scenarios/plugins/docs_inline_three_sections/tsconfig.json create mode 100644 scenarios/plugins/docs_inline_two_sections/.gitignore create mode 100644 scenarios/plugins/docs_inline_two_sections/gauntlet.toml create mode 100644 scenarios/plugins/docs_inline_two_sections/package.json create mode 100644 scenarios/plugins/docs_inline_two_sections/src/main.tsx create mode 100644 scenarios/plugins/docs_inline_two_sections/tsconfig.json diff --git a/package-lock.json b/package-lock.json index d791e13..1175e4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1038,8 +1038,16 @@ "resolved": "scenarios/plugins/docs_grid", "link": true }, - "node_modules/@project-gauntlet/docs-inline": { - "resolved": "scenarios/plugins/docs_inline", + "node_modules/@project-gauntlet/docs-inline-separators": { + "resolved": "scenarios/plugins/docs_inline_separators", + "link": true + }, + "node_modules/@project-gauntlet/docs-inline-three-sections": { + "resolved": "scenarios/plugins/docs_inline_three_sections", + "link": true + }, + "node_modules/@project-gauntlet/docs-inline-two-sections": { + "resolved": "scenarios/plugins/docs_inline_two_sections", "link": true }, "node_modules/@project-gauntlet/docs-list": { @@ -2979,6 +2987,7 @@ }, "scenarios/plugins/docs_inline": { "name": "@project-gauntlet/docs-inline", + "extraneous": true, "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -2989,16 +2998,61 @@ "typescript": "^5.3.3" } }, - "scenarios/plugins/docs_inline/node_modules/@project-gauntlet/api": { + "scenarios/plugins/docs_inline_separators": { + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/deno": "file:../../js/deno", + "@project-gauntlet/tools": "file:../../../tools", + "@types/react": "^18.2.14", + "typescript": "^5.3.3" + } + }, + "scenarios/plugins/docs_inline_separators/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_inline/node_modules/@project-gauntlet/deno": { + "scenarios/plugins/docs_inline_separators/node_modules/@project-gauntlet/deno": { "resolved": "scenarios/js/deno", "link": true }, - "scenarios/plugins/docs_inline/node_modules/@project-gauntlet/tools": { - "resolved": "scenarios/tools", + "scenarios/plugins/docs_inline_three_sections": { + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/deno": "file:../../js/deno", + "@project-gauntlet/tools": "file:../../../tools", + "@types/react": "^18.2.14", + "typescript": "^5.3.3" + } + }, + "scenarios/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/api": { + "resolved": "scenarios/js/api", + "link": true + }, + "scenarios/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/deno": { + "resolved": "scenarios/js/deno", + "link": true + }, + "scenarios/plugins/docs_inline_two_sections": { + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/deno": "file:../../js/deno", + "@project-gauntlet/tools": "file:../../../tools", + "@types/react": "^18.2.14", + "typescript": "^5.3.3" + } + }, + "scenarios/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/api": { + "resolved": "scenarios/js/api", + "link": true + }, + "scenarios/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/deno": { + "resolved": "scenarios/js/deno", "link": true }, "scenarios/plugins/docs_list": { diff --git a/scenarios/data/docs_inline/main/separator.json b/scenarios/data/docs_inline_separators/main/separator.json similarity index 100% rename from scenarios/data/docs_inline/main/separator.json rename to scenarios/data/docs_inline_separators/main/separator.json diff --git a/scenarios/data/docs_inline/main/three-sections.json b/scenarios/data/docs_inline_three_sections/main/three-sections.json similarity index 100% rename from scenarios/data/docs_inline/main/three-sections.json rename to scenarios/data/docs_inline_three_sections/main/three-sections.json diff --git a/scenarios/data/docs_inline/main/two-sections.json b/scenarios/data/docs_inline_two_sections/main/two-sections.json similarity index 100% rename from scenarios/data/docs_inline/main/two-sections.json rename to scenarios/data/docs_inline_two_sections/main/two-sections.json diff --git a/scenarios/plugins/docs_inline/src/main.tsx b/scenarios/plugins/docs_inline/src/main.tsx deleted file mode 100644 index 57ed895..0000000 --- a/scenarios/plugins/docs_inline/src/main.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { ReactElement } from "react"; -import { Inline, Icons } from "@project-gauntlet/api/components"; - -export default function Main(props: { text: string }): ReactElement { - const type = props.text; - - switch (type) { - case "separator": { - return Separator() - } - case "three-sections": { - return ThreeSection() - } - case "two-sections": { - return TwoSection() - } - } - - throw new Error("unknown type") -} - -function Separator() { - return ( - // docs-code-segment:start separator - - - - Left - - - - - - Right - - - - // docs-code-segment:end - ) -} - -function ThreeSection() { - return ( - // docs-code-segment:start three-section - - - - Left - - - - - Center - - - - - Right - - - - // docs-code-segment:end - ) -} - -function TwoSection() { - return ( - // docs-code-segment:start two-section - - - - Left - - - - - Right - - - - // docs-code-segment:end - ) -} \ No newline at end of file diff --git a/scenarios/plugins/docs_inline/.gitignore b/scenarios/plugins/docs_inline_separators/.gitignore similarity index 100% rename from scenarios/plugins/docs_inline/.gitignore rename to scenarios/plugins/docs_inline_separators/.gitignore diff --git a/scenarios/plugins/docs_inline/gauntlet.toml b/scenarios/plugins/docs_inline_separators/gauntlet.toml similarity index 100% rename from scenarios/plugins/docs_inline/gauntlet.toml rename to scenarios/plugins/docs_inline_separators/gauntlet.toml diff --git a/scenarios/plugins/docs_inline/package.json b/scenarios/plugins/docs_inline_separators/package.json similarity index 87% rename from scenarios/plugins/docs_inline/package.json rename to scenarios/plugins/docs_inline_separators/package.json index e65d2db..5dd69d6 100644 --- a/scenarios/plugins/docs_inline/package.json +++ b/scenarios/plugins/docs_inline_separators/package.json @@ -1,5 +1,5 @@ { - "name": "@project-gauntlet/docs-inline", + "name": "@project-gauntlet/docs-inline-separators", "private": true, "scripts": { "build": "gauntlet build", diff --git a/scenarios/plugins/docs_inline_separators/src/main.tsx b/scenarios/plugins/docs_inline_separators/src/main.tsx new file mode 100644 index 0000000..c2b4bb9 --- /dev/null +++ b/scenarios/plugins/docs_inline_separators/src/main.tsx @@ -0,0 +1,20 @@ +import { ReactElement } from "react"; +import { Inline, Icons } from "@project-gauntlet/api/components"; + +export default function Main({ text }: { text: string }): ReactElement { + return ( + + + + Left + + + + + + Right + + + + ) +} diff --git a/scenarios/plugins/docs_inline/tsconfig.json b/scenarios/plugins/docs_inline_separators/tsconfig.json similarity index 100% rename from scenarios/plugins/docs_inline/tsconfig.json rename to scenarios/plugins/docs_inline_separators/tsconfig.json diff --git a/scenarios/plugins/docs_inline_three_sections/.gitignore b/scenarios/plugins/docs_inline_three_sections/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/scenarios/plugins/docs_inline_three_sections/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/scenarios/plugins/docs_inline_three_sections/gauntlet.toml b/scenarios/plugins/docs_inline_three_sections/gauntlet.toml new file mode 100644 index 0000000..2521a6c --- /dev/null +++ b/scenarios/plugins/docs_inline_three_sections/gauntlet.toml @@ -0,0 +1,16 @@ +[gauntlet] +name = 'Docs Inline' +description = '' + +# docs-code-segment:start main +[[entrypoint]] +id = 'main' +name = 'Main' +path = 'src/main.tsx' +type = 'inline-view' +description = '' +# docs-code-segment:end + +[permissions] +main_search_bar = ["read"] + diff --git a/scenarios/plugins/docs_inline_three_sections/package.json b/scenarios/plugins/docs_inline_three_sections/package.json new file mode 100644 index 0000000..1a04f8a --- /dev/null +++ b/scenarios/plugins/docs_inline_three_sections/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-inline-three-sections", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "^18.2.14", + "@project-gauntlet/deno": "file:../../js/deno", + "@project-gauntlet/tools": "file:../../../tools", + "typescript": "^5.3.3" + } +} diff --git a/scenarios/plugins/docs_inline_three_sections/src/main.tsx b/scenarios/plugins/docs_inline_three_sections/src/main.tsx new file mode 100644 index 0000000..a0ff3fa --- /dev/null +++ b/scenarios/plugins/docs_inline_three_sections/src/main.tsx @@ -0,0 +1,26 @@ +import { ReactElement } from "react"; +import { Inline } from "@project-gauntlet/api/components"; + +export default function Main(props: { text: string }): ReactElement { + const type = props.text; + + return ( + + + + Left + + + + + Center: {type} + + + + + Right + + + + ) +} diff --git a/scenarios/plugins/docs_inline_three_sections/tsconfig.json b/scenarios/plugins/docs_inline_three_sections/tsconfig.json new file mode 100644 index 0000000..cbe7961 --- /dev/null +++ b/scenarios/plugins/docs_inline_three_sections/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "types": ["@project-gauntlet/deno"] + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/scenarios/plugins/docs_inline_two_sections/.gitignore b/scenarios/plugins/docs_inline_two_sections/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/scenarios/plugins/docs_inline_two_sections/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/scenarios/plugins/docs_inline_two_sections/gauntlet.toml b/scenarios/plugins/docs_inline_two_sections/gauntlet.toml new file mode 100644 index 0000000..2521a6c --- /dev/null +++ b/scenarios/plugins/docs_inline_two_sections/gauntlet.toml @@ -0,0 +1,16 @@ +[gauntlet] +name = 'Docs Inline' +description = '' + +# docs-code-segment:start main +[[entrypoint]] +id = 'main' +name = 'Main' +path = 'src/main.tsx' +type = 'inline-view' +description = '' +# docs-code-segment:end + +[permissions] +main_search_bar = ["read"] + diff --git a/scenarios/plugins/docs_inline_two_sections/package.json b/scenarios/plugins/docs_inline_two_sections/package.json new file mode 100644 index 0000000..5daa663 --- /dev/null +++ b/scenarios/plugins/docs_inline_two_sections/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-inline-two-sections", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "^18.2.14", + "@project-gauntlet/deno": "file:../../js/deno", + "@project-gauntlet/tools": "file:../../../tools", + "typescript": "^5.3.3" + } +} diff --git a/scenarios/plugins/docs_inline_two_sections/src/main.tsx b/scenarios/plugins/docs_inline_two_sections/src/main.tsx new file mode 100644 index 0000000..0534b90 --- /dev/null +++ b/scenarios/plugins/docs_inline_two_sections/src/main.tsx @@ -0,0 +1,19 @@ +import { ReactElement } from "react"; +import { Inline } from "@project-gauntlet/api/components"; + +export default function Main({ text }: { text: string }): ReactElement { + return ( + + + + Left + + + + + Right + + + + ) +} diff --git a/scenarios/plugins/docs_inline_two_sections/tsconfig.json b/scenarios/plugins/docs_inline_two_sections/tsconfig.json new file mode 100644 index 0000000..cbe7961 --- /dev/null +++ b/scenarios/plugins/docs_inline_two_sections/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "types": ["@project-gauntlet/deno"] + }, + "lib": ["ES2020"] +} \ No newline at end of file From f742358b58e1a4a1ed99e78d3b3429d347095dcf Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 23 Nov 2024 21:16:08 +0100 Subject: [PATCH 193/540] Add and improve some code examples --- docs/js/components/detail/props/actions.md | 3 +- docs/js/components/form/props/actions.md | 3 +- docs/js/components/grid/props/actions.md | 3 +- docs/js/components/inline/props/actions.md | 3 +- docs/js/components/list/props/actions.md | 3 +- .../components/list_item/props/accessories.md | 2 +- .../docs_detail/src/content_code_block.tsx | 12 +-- scenarios/plugins/docs_form/src/checkbox.tsx | 2 +- .../plugins/docs_form/src/date-picker.tsx | 4 +- scenarios/plugins/docs_form/src/main.tsx | 22 +++-- .../plugins/docs_form/src/password-field.tsx | 2 +- scenarios/plugins/docs_form/src/select.tsx | 14 +-- scenarios/plugins/docs_form/src/separator.tsx | 4 +- .../plugins/docs_form/src/text-field.tsx | 2 +- scenarios/plugins/docs_grid/gauntlet.toml | 44 +++++++++ .../docs_grid/src/content_code_block.tsx | 27 ++++++ .../plugins/docs_grid/src/content_headers.tsx | 51 +++++++++++ .../plugins/docs_grid/src/content_image.tsx | 47 ++++++++++ .../docs_grid/src/content_paragraph.tsx | 27 ++++++ .../plugins/docs_grid/src/empty_view.tsx | 8 +- scenarios/plugins/docs_grid/src/main.tsx | 91 ++++++++----------- .../plugins/docs_grid/src/search_bar.tsx | 37 ++++++++ scenarios/plugins/docs_list/gauntlet.toml | 9 ++ .../plugins/docs_list/src/empty_view.tsx | 2 +- .../plugins/docs_list/src/search_bar.tsx | 31 +++++++ 25 files changed, 353 insertions(+), 100 deletions(-) create mode 100644 scenarios/plugins/docs_grid/src/content_code_block.tsx create mode 100644 scenarios/plugins/docs_grid/src/content_headers.tsx create mode 100644 scenarios/plugins/docs_grid/src/content_image.tsx create mode 100644 scenarios/plugins/docs_grid/src/content_paragraph.tsx create mode 100644 scenarios/plugins/docs_grid/src/search_bar.tsx create mode 100644 scenarios/plugins/docs_list/src/search_bar.tsx diff --git a/docs/js/components/detail/props/actions.md b/docs/js/components/detail/props/actions.md index d28c707..536af18 100644 --- a/docs/js/components/detail/props/actions.md +++ b/docs/js/components/detail/props/actions.md @@ -1,2 +1 @@ -Allows to define an Action Panel for this view -Every root component has such property \ No newline at end of file +Allows to define an Action Panel for this view. Every root component has such property \ No newline at end of file diff --git a/docs/js/components/form/props/actions.md b/docs/js/components/form/props/actions.md index d28c707..536af18 100644 --- a/docs/js/components/form/props/actions.md +++ b/docs/js/components/form/props/actions.md @@ -1,2 +1 @@ -Allows to define an Action Panel for this view -Every root component has such property \ No newline at end of file +Allows to define an Action Panel for this view. Every root component has such property \ No newline at end of file diff --git a/docs/js/components/grid/props/actions.md b/docs/js/components/grid/props/actions.md index d28c707..536af18 100644 --- a/docs/js/components/grid/props/actions.md +++ b/docs/js/components/grid/props/actions.md @@ -1,2 +1 @@ -Allows to define an Action Panel for this view -Every root component has such property \ No newline at end of file +Allows to define an Action Panel for this view. Every root component has such property \ No newline at end of file diff --git a/docs/js/components/inline/props/actions.md b/docs/js/components/inline/props/actions.md index d28c707..536af18 100644 --- a/docs/js/components/inline/props/actions.md +++ b/docs/js/components/inline/props/actions.md @@ -1,2 +1 @@ -Allows to define an Action Panel for this view -Every root component has such property \ No newline at end of file +Allows to define an Action Panel for this view. Every root component has such property \ No newline at end of file diff --git a/docs/js/components/list/props/actions.md b/docs/js/components/list/props/actions.md index d28c707..536af18 100644 --- a/docs/js/components/list/props/actions.md +++ b/docs/js/components/list/props/actions.md @@ -1,2 +1 @@ -Allows to define an Action Panel for this view -Every root component has such property \ No newline at end of file +Allows to define an Action Panel for this view. Every root component has such property \ No newline at end of file diff --git a/docs/js/components/list_item/props/accessories.md b/docs/js/components/list_item/props/accessories.md index b24a937..343508b 100644 --- a/docs/js/components/list_item/props/accessories.md +++ b/docs/js/components/list_item/props/accessories.md @@ -1 +1 @@ -List of accessories displayed on the right side of the list item \ No newline at end of file +List of accessories displayed on the right-hand side of the list item \ No newline at end of file diff --git a/scenarios/plugins/docs_detail/src/content_code_block.tsx b/scenarios/plugins/docs_detail/src/content_code_block.tsx index 1513541..5326068 100644 --- a/scenarios/plugins/docs_detail/src/content_code_block.tsx +++ b/scenarios/plugins/docs_detail/src/content_code_block.tsx @@ -1,12 +1,12 @@ import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; -const code = ` - fib :: Integer -> Integer - fib 0 = 0 - fib 1 = 1 - fib n = fib (n-1) + fib (n-2) -`.trimStart().replace(/^ {4}/g, ''); +const code = `\ +fib :: Integer -> Integer +fib 0 = 0 +fib 1 = 1 +fib n = fib (n-1) + fib (n-2) +` export default function Main(): ReactNode { return ( diff --git a/scenarios/plugins/docs_form/src/checkbox.tsx b/scenarios/plugins/docs_form/src/checkbox.tsx index ddb21cc..e545fa4 100644 --- a/scenarios/plugins/docs_form/src/checkbox.tsx +++ b/scenarios/plugins/docs_form/src/checkbox.tsx @@ -5,7 +5,7 @@ export default function Main(): ReactElement { return (
{ console.log(`value: ${value}`) diff --git a/scenarios/plugins/docs_form/src/date-picker.tsx b/scenarios/plugins/docs_form/src/date-picker.tsx index 7707525..036252a 100644 --- a/scenarios/plugins/docs_form/src/date-picker.tsx +++ b/scenarios/plugins/docs_form/src/date-picker.tsx @@ -5,8 +5,8 @@ export default function Main(): ReactElement { return ( { console.log(`value: ${value}`) }} diff --git a/scenarios/plugins/docs_form/src/main.tsx b/scenarios/plugins/docs_form/src/main.tsx index 42fccd1..c5166b3 100644 --- a/scenarios/plugins/docs_form/src/main.tsx +++ b/scenarios/plugins/docs_form/src/main.tsx @@ -1,50 +1,52 @@ import { ReactElement } from 'react'; import { Form } from "@project-gauntlet/api/components"; +// TODO remake into starwars themed + export default function Main(): ReactElement { return ( - Burger + Burger { console.log(`value: ${value}`) }} /> { console.log(`value: ${value}`) }} /> { console.log(`value: ${value}`) }} /> { console.log(`value: ${value}`) }} /> { console.log(`value: ${value}`) }} /> { console.log(`value: ${value}`) diff --git a/scenarios/plugins/docs_form/src/password-field.tsx b/scenarios/plugins/docs_form/src/password-field.tsx index f77cfe8..f131e58 100644 --- a/scenarios/plugins/docs_form/src/password-field.tsx +++ b/scenarios/plugins/docs_form/src/password-field.tsx @@ -4,7 +4,7 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - + ); }; diff --git a/scenarios/plugins/docs_form/src/select.tsx b/scenarios/plugins/docs_form/src/select.tsx index ef6d34f..891975a 100644 --- a/scenarios/plugins/docs_form/src/select.tsx +++ b/scenarios/plugins/docs_form/src/select.tsx @@ -4,13 +4,13 @@ import { Form } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return (
- - Burger - Hot Dog - Croissant - Cookies - Steak - Seafood + + Burger + Hot Dog + Croissant + Cookies + Steak + Seafood ); diff --git a/scenarios/plugins/docs_form/src/separator.tsx b/scenarios/plugins/docs_form/src/separator.tsx index 5c2b458..4e39930 100644 --- a/scenarios/plugins/docs_form/src/separator.tsx +++ b/scenarios/plugins/docs_form/src/separator.tsx @@ -5,14 +5,14 @@ export default function Main(): ReactElement { return (
{ console.log(`value: ${value}`) }} /> { console.log(`value: ${value}`) }} diff --git a/scenarios/plugins/docs_form/src/text-field.tsx b/scenarios/plugins/docs_form/src/text-field.tsx index d30a05b..1d86337 100644 --- a/scenarios/plugins/docs_form/src/text-field.tsx +++ b/scenarios/plugins/docs_form/src/text-field.tsx @@ -5,7 +5,7 @@ export default function Main(): ReactElement { return ( { console.log(`value: ${value}`) }} diff --git a/scenarios/plugins/docs_grid/gauntlet.toml b/scenarios/plugins/docs_grid/gauntlet.toml index ad0833c..cf94c88 100644 --- a/scenarios/plugins/docs_grid/gauntlet.toml +++ b/scenarios/plugins/docs_grid/gauntlet.toml @@ -30,6 +30,50 @@ type = 'view' description = '' # docs-code-segment:end +# docs-code-segment:start search-bar +[[entrypoint]] +id = 'search-bar' +name = 'Grid Search bar' +path = 'src/search_bar.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start content-code-block +[[entrypoint]] +id = 'content-code' +name = 'Grid Content Code Block' +path = 'src/content_code_block.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start content-paragraph +[[entrypoint]] +id = 'content-paragraph' +name = 'Grid Content Paragraph' +path = 'src/content_paragraph.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start content-image +[[entrypoint]] +id = 'content-image' +name = 'Grid Content Image' +path = 'src/content_image.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start content-headers +[[entrypoint]] +id = 'content-headers' +name = 'Grid Content Headers' +path = 'src/content_headers.tsx' +type = 'view' +description = '' +# docs-code-segment:end [permissions] network = ["static.wikia.nocookie.net"] diff --git a/scenarios/plugins/docs_grid/src/content_code_block.tsx b/scenarios/plugins/docs_grid/src/content_code_block.tsx new file mode 100644 index 0000000..6e793e9 --- /dev/null +++ b/scenarios/plugins/docs_grid/src/content_code_block.tsx @@ -0,0 +1,27 @@ +import { ReactElement } from "react"; +import { Grid } from "@project-gauntlet/api/components"; + +const items = [ + "C-3PO", + "R2-D2", + "BB-8", + "IG-88", + "D-O", + "C1-10P", +] + +export default function Main(): ReactElement { + return ( + + {items.map(value => ( + + + + {value} + + + + ))} + + ) +} \ No newline at end of file diff --git a/scenarios/plugins/docs_grid/src/content_headers.tsx b/scenarios/plugins/docs_grid/src/content_headers.tsx new file mode 100644 index 0000000..1c9b0c7 --- /dev/null +++ b/scenarios/plugins/docs_grid/src/content_headers.tsx @@ -0,0 +1,51 @@ +import { ReactElement } from "react"; +import { Grid } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + + + + Episode I + + + + + + + Episode II + + + + + + + Episode III + + + + + + + Episode IV + + + + + + + Episode V + + + + + + + Episode VI + + + + + ) +} \ No newline at end of file diff --git a/scenarios/plugins/docs_grid/src/content_image.tsx b/scenarios/plugins/docs_grid/src/content_image.tsx new file mode 100644 index 0000000..e5a50ac --- /dev/null +++ b/scenarios/plugins/docs_grid/src/content_image.tsx @@ -0,0 +1,47 @@ +import { ReactElement } from "react"; +import { Grid } from "@project-gauntlet/api/components"; + +const items = [ + { + title: "Naboo", + image: "https://static.wikia.nocookie.net/star-wars-canon/images/2/24/NabooFull-SW.png/revision/latest/scale-to-width-down/150?cb=20151218205422" + }, + { + title: "Ryloth", + image: "https://static.wikia.nocookie.net/star-wars-canon/images/b/b7/Ryloth_Rebels.png/revision/latest/scale-to-width-down/150?cb=20161103040944" + }, + { + title: "Tatooine", + image: "https://static.wikia.nocookie.net/star-wars-canon/images/b/b0/Tatooine_TPM.png/revision/latest/scale-to-width-down/150?cb=20151124205032" + }, + { + title: "Dagobah", + image: "https://static.wikia.nocookie.net/star-wars-canon/images/4/48/Dagobah_ep3.jpg/revision/latest/scale-to-width-down/150?cb=20161103221846" + }, + { + title: "Endor", + image: "https://static.wikia.nocookie.net/star-wars-canon/images/9/96/Endor-DB.png/revision/latest/scale-to-width-down/150?cb=20160711234205" + }, + { + title: "Dathomir", + image: "https://static.wikia.nocookie.net/starwars/images/3/34/DathomirJFO.jpg/revision/latest/scale-to-width-down/150?cb=20200222032237" + }, + { + title: "Dantooine", + image: "https://static.wikia.nocookie.net/starwars/images/a/a5/Dantooine_Resistance.jpg/revision/latest/scale-to-width-down/150?cb=20200120190043" + }, +] + +export default function Main(): ReactElement { + return ( + + {items.map(value => ( + + + + + + ))} + + ) +} \ No newline at end of file diff --git a/scenarios/plugins/docs_grid/src/content_paragraph.tsx b/scenarios/plugins/docs_grid/src/content_paragraph.tsx new file mode 100644 index 0000000..3c4a339 --- /dev/null +++ b/scenarios/plugins/docs_grid/src/content_paragraph.tsx @@ -0,0 +1,27 @@ +import { ReactElement } from "react"; +import { Grid } from "@project-gauntlet/api/components"; + +const items = [ + "C-3PO", + "R2-D2", + "BB-8", + "IG-88", + "D-O", + "C1-10P", +] + +export default function Main(): ReactElement { + return ( + + {items.map(value => ( + + + + {value} + + + + ))} + + ) +} \ No newline at end of file diff --git a/scenarios/plugins/docs_grid/src/empty_view.tsx b/scenarios/plugins/docs_grid/src/empty_view.tsx index eec1cc6..418fed3 100644 --- a/scenarios/plugins/docs_grid/src/empty_view.tsx +++ b/scenarios/plugins/docs_grid/src/empty_view.tsx @@ -1,12 +1,12 @@ import { ReactElement } from "react"; -import { List } from "@project-gauntlet/api/components"; +import { Grid } from "@project-gauntlet/api/components"; const alderaanImage = "https://static.wikia.nocookie.net/starwars/images/4/4a/Alderaan.jpg/revision/latest?cb=20061211013805" export default function Main(): ReactElement { return ( - - - + + + ) } diff --git a/scenarios/plugins/docs_grid/src/main.tsx b/scenarios/plugins/docs_grid/src/main.tsx index 96b59bb..e5a50ac 100644 --- a/scenarios/plugins/docs_grid/src/main.tsx +++ b/scenarios/plugins/docs_grid/src/main.tsx @@ -1,64 +1,47 @@ import { ReactElement } from "react"; import { Grid } from "@project-gauntlet/api/components"; -const nabooImage = "https://static.wikia.nocookie.net/star-wars-canon/images/2/24/NabooFull-SW.png/revision/latest/scale-to-width-down/150?cb=20151218205422" -const rylothImage = "https://static.wikia.nocookie.net/star-wars-canon/images/4/48/Dagobah_ep3.jpg/revision/latest/scale-to-width-down/150?cb=20161103221846" -const tatooineImage = "https://static.wikia.nocookie.net/star-wars-canon/images/b/b7/Ryloth_Rebels.png/revision/latest/scale-to-width-down/150?cb=20161103040944" -const dagobahImage = "https://static.wikia.nocookie.net/star-wars-canon/images/b/b0/Tatooine_TPM.png/revision/latest/scale-to-width-down/150?cb=20151124205032" -const coruscantImage = "https://static.wikia.nocookie.net/star-wars-canon/images/7/7d/Death_Star_detail.png/revision/latest/scale-to-width-down/150?cb=20151216212148" -const endorImage = "https://static.wikia.nocookie.net/star-wars-canon/images/9/96/Endor-DB.png/revision/latest/scale-to-width-down/150?cb=20160711234205" -const deathstarImage = "https://static.wikia.nocookie.net/starwars/images/a/a6/Coruscant-SWJS.jpg/revision/latest/scale-to-width-down/150?cb=20240324185443" -const dathomirImage = "https://static.wikia.nocookie.net/starwars/images/3/34/DathomirJFO.jpg/revision/latest/scale-to-width-down/150?cb=20200222032237" -const dantooineImage = "https://static.wikia.nocookie.net/starwars/images/a/a5/Dantooine_Resistance.jpg/revision/latest/scale-to-width-down/150?cb=20200120190043" +const items = [ + { + title: "Naboo", + image: "https://static.wikia.nocookie.net/star-wars-canon/images/2/24/NabooFull-SW.png/revision/latest/scale-to-width-down/150?cb=20151218205422" + }, + { + title: "Ryloth", + image: "https://static.wikia.nocookie.net/star-wars-canon/images/b/b7/Ryloth_Rebels.png/revision/latest/scale-to-width-down/150?cb=20161103040944" + }, + { + title: "Tatooine", + image: "https://static.wikia.nocookie.net/star-wars-canon/images/b/b0/Tatooine_TPM.png/revision/latest/scale-to-width-down/150?cb=20151124205032" + }, + { + title: "Dagobah", + image: "https://static.wikia.nocookie.net/star-wars-canon/images/4/48/Dagobah_ep3.jpg/revision/latest/scale-to-width-down/150?cb=20161103221846" + }, + { + title: "Endor", + image: "https://static.wikia.nocookie.net/star-wars-canon/images/9/96/Endor-DB.png/revision/latest/scale-to-width-down/150?cb=20160711234205" + }, + { + title: "Dathomir", + image: "https://static.wikia.nocookie.net/starwars/images/3/34/DathomirJFO.jpg/revision/latest/scale-to-width-down/150?cb=20200222032237" + }, + { + title: "Dantooine", + image: "https://static.wikia.nocookie.net/starwars/images/a/a5/Dantooine_Resistance.jpg/revision/latest/scale-to-width-down/150?cb=20200120190043" + }, +] export default function Main(): ReactElement { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {items.map(value => ( + + + + + + ))} ) } \ No newline at end of file diff --git a/scenarios/plugins/docs_grid/src/search_bar.tsx b/scenarios/plugins/docs_grid/src/search_bar.tsx new file mode 100644 index 0000000..ca19caa --- /dev/null +++ b/scenarios/plugins/docs_grid/src/search_bar.tsx @@ -0,0 +1,37 @@ +import { ReactElement, useState } from "react"; +import { Grid } from "@project-gauntlet/api/components"; + +const results = [ + "Disturbances in the Force", + "Bounty hunters", + "Astromech droids", + "Celestials and their technology", + "What happened on holidays?", + "Ahsoka Tano", + "Mandalorian Culture" +] + +export default function Main(): ReactElement { + const [searchText, setSearchText] = useState(""); + + return ( + + + {results + .filter(value => !searchText ? true : value.toLowerCase().includes(searchText)) + .map(value => ( + + + + {value} + + + + )) + } + + ) +} diff --git a/scenarios/plugins/docs_list/gauntlet.toml b/scenarios/plugins/docs_list/gauntlet.toml index b50cffd..45fe43f 100644 --- a/scenarios/plugins/docs_list/gauntlet.toml +++ b/scenarios/plugins/docs_list/gauntlet.toml @@ -39,6 +39,15 @@ type = 'view' description = '' # docs-code-segment:end +# docs-code-segment:start search-bar +[[entrypoint]] +id = 'search-bar' +name = 'List Search bar' +path = 'src/search_bar.tsx' +type = 'view' +description = '' +# docs-code-segment:end + [permissions] network = ["static.wikia.nocookie.net"] diff --git a/scenarios/plugins/docs_list/src/empty_view.tsx b/scenarios/plugins/docs_list/src/empty_view.tsx index eec1cc6..0a41444 100644 --- a/scenarios/plugins/docs_list/src/empty_view.tsx +++ b/scenarios/plugins/docs_list/src/empty_view.tsx @@ -6,7 +6,7 @@ const alderaanImage = "https://static.wikia.nocookie.net/starwars/images/4/4a/Al export default function Main(): ReactElement { return ( - + ) } diff --git a/scenarios/plugins/docs_list/src/search_bar.tsx b/scenarios/plugins/docs_list/src/search_bar.tsx new file mode 100644 index 0000000..618f476 --- /dev/null +++ b/scenarios/plugins/docs_list/src/search_bar.tsx @@ -0,0 +1,31 @@ +import { ReactElement, useState } from "react"; +import { List } from "@project-gauntlet/api/components"; + +const results = [ + "Disturbances in the Force", + "Bounty hunters", + "Astromech droids", + "Celestials and their technology", + "What happened on holidays?", + "Ahsoka Tano", + "Mandalorian Culture" +] + +export default function Main(): ReactElement { + const [searchText, setSearchText] = useState(""); + + return ( + + + {results + .filter(value => !searchText ? true : value.toLowerCase().includes(searchText)) + .map(value => ( + + )) + } + + ) +} From 2f24d73ef5a78b5dc485524c8d942fe1a0de512f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 23 Nov 2024 21:17:30 +0100 Subject: [PATCH 194/540] Fix entrypoint id in code examples --- scenarios/plugins/docs_grid/gauntlet.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/plugins/docs_grid/gauntlet.toml b/scenarios/plugins/docs_grid/gauntlet.toml index cf94c88..23f9c12 100644 --- a/scenarios/plugins/docs_grid/gauntlet.toml +++ b/scenarios/plugins/docs_grid/gauntlet.toml @@ -41,7 +41,7 @@ description = '' # docs-code-segment:start content-code-block [[entrypoint]] -id = 'content-code' +id = 'content-code-block' name = 'Grid Content Code Block' path = 'src/content_code_block.tsx' type = 'view' From c8637733a7b1e11839a2ab523ebd515c875f2250 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 23 Nov 2024 21:29:50 +0100 Subject: [PATCH 195/540] Fix some code examples missing scenario data --- scenarios/data/docs_grid/content-code-block/default.json | 3 +++ scenarios/data/docs_grid/content-headers/default.json | 3 +++ scenarios/data/docs_grid/content-image/default.json | 3 +++ scenarios/data/docs_grid/content-paragraph/default.json | 3 +++ scenarios/data/docs_grid/search-bar/default.json | 3 +++ scenarios/data/docs_list/search-bar/default.json | 3 +++ 6 files changed, 18 insertions(+) create mode 100644 scenarios/data/docs_grid/content-code-block/default.json create mode 100644 scenarios/data/docs_grid/content-headers/default.json create mode 100644 scenarios/data/docs_grid/content-image/default.json create mode 100644 scenarios/data/docs_grid/content-paragraph/default.json create mode 100644 scenarios/data/docs_grid/search-bar/default.json create mode 100644 scenarios/data/docs_list/search-bar/default.json diff --git a/scenarios/data/docs_grid/content-code-block/default.json b/scenarios/data/docs_grid/content-code-block/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/scenarios/data/docs_grid/content-code-block/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/scenarios/data/docs_grid/content-headers/default.json b/scenarios/data/docs_grid/content-headers/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/scenarios/data/docs_grid/content-headers/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/scenarios/data/docs_grid/content-image/default.json b/scenarios/data/docs_grid/content-image/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/scenarios/data/docs_grid/content-image/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/scenarios/data/docs_grid/content-paragraph/default.json b/scenarios/data/docs_grid/content-paragraph/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/scenarios/data/docs_grid/content-paragraph/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/scenarios/data/docs_grid/search-bar/default.json b/scenarios/data/docs_grid/search-bar/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/scenarios/data/docs_grid/search-bar/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/scenarios/data/docs_list/search-bar/default.json b/scenarios/data/docs_list/search-bar/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/scenarios/data/docs_list/search-bar/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file From 7b41f1ac9bf9b40685d72b9f00700ccc6b98a447 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Mon, 25 Nov 2024 20:40:27 +0100 Subject: [PATCH 196/540] Update iced to 0.13. No wayland support. No macOS window settings --- Cargo.lock | 1663 ++++++---- Cargo.toml | 12 +- rust/client/Cargo.toml | 4 +- rust/client/src/ui/client_context.rs | 16 +- .../src/ui/custom_widgets/loading_bar.rs | 47 +- rust/client/src/ui/hud/mod.rs | 145 +- rust/client/src/ui/mod.rs | 2909 ++++++++--------- rust/client/src/ui/scroll_handle.rs | 10 +- rust/client/src/ui/search_list.rs | 2 +- rust/client/src/ui/state/mod.rs | 162 +- rust/client/src/ui/theme/button.rs | 53 +- rust/client/src/ui/theme/checkbox.rs | 119 +- rust/client/src/ui/theme/container.rs | 72 +- rust/client/src/ui/theme/date_picker.rs | 109 +- rust/client/src/ui/theme/loading_bar.rs | 16 +- rust/client/src/ui/theme/mod.rs | 7 +- rust/client/src/ui/theme/pick_list.rs | 100 +- rust/client/src/ui/theme/row.rs | 4 +- rust/client/src/ui/theme/rule.rs | 34 +- rust/client/src/ui/theme/scrollable.rs | 116 +- rust/client/src/ui/theme/text.rs | 53 +- rust/client/src/ui/theme/text_input.rs | 217 +- rust/client/src/ui/theme/tooltip.rs | 2 +- rust/client/src/ui/widget.rs | 713 ++-- rust/client/src/ui/widget_container.rs | 21 +- rust/common/build.rs | 3 +- rust/common_ui/Cargo.toml | 1 + rust/common_ui/src/lib.rs | 436 +-- rust/management_client/Cargo.toml | 1 + .../src/components/shortcut_selector.rs | 100 +- rust/management_client/src/theme.rs | 9 +- rust/management_client/src/theme/button.rs | 287 +- rust/management_client/src/theme/checkbox.rs | 113 +- rust/management_client/src/theme/container.rs | 26 +- .../src/theme/number_input.rs | 59 +- rust/management_client/src/theme/pick_list.rs | 66 +- rust/management_client/src/theme/rule.rs | 20 +- .../management_client/src/theme/scrollable.rs | 127 +- .../src/theme/shortcut_selector.rs | 36 +- rust/management_client/src/theme/spinner.rs | 16 - rust/management_client/src/theme/table.rs | 29 +- rust/management_client/src/theme/text.rs | 24 +- .../management_client/src/theme/text_input.rs | 91 +- rust/management_client/src/ui.rs | 1307 ++++---- rust/management_client/src/views/general.rs | 42 +- rust/management_client/src/views/plugins.rs | 96 +- .../src/views/plugins/preferences.rs | 78 +- .../src/views/plugins/table.rs | 42 +- rust/server/Cargo.toml | 3 + 49 files changed, 5050 insertions(+), 4568 deletions(-) delete mode 100644 rust/management_client/src/theme/spinner.rs diff --git a/Cargo.lock b/Cargo.lock index 93fe9d1..c7cf011 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,17 @@ dependencies = [ "aes", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.14", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -150,12 +161,12 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-activity" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee91c0c2905bae44f84bfa4e044536541df26b7703fd0888deeb9060fcc44289" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.5.0", + "bitflags 2.6.0", "cc", "cesu8", "jni", @@ -164,7 +175,7 @@ dependencies = [ "log", "ndk", "ndk-context", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "num_enum", "thiserror", ] @@ -294,9 +305,9 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -319,11 +330,11 @@ checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" [[package]] name = "ash" -version = "0.37.3+1.3.251" +version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.3", ] [[package]] @@ -348,7 +359,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "syn 1.0.109", "synstructure", @@ -360,7 +371,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "syn 1.0.109", ] @@ -372,10 +383,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c09c69dffe06d222d072c878c3afe86eee2179806f20503faec97250268b4c24" dependencies = [ "pmutil", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "swc_macros_common", - "syn 2.0.59", + "syn 2.0.89", +] + +[[package]] +name = "async-broadcast" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", ] [[package]] @@ -392,6 +427,108 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.3.1", + "futures-lite", + "rustix", + "tracing", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.36", + "syn 2.0.89", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -409,20 +546,26 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -642,18 +785,18 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bit_field" @@ -669,9 +812,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] @@ -715,32 +858,26 @@ dependencies = [ "generic-array 0.14.7", ] -[[package]] -name = "block-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7" -dependencies = [ - "objc-sys", -] - -[[package]] -name = "block2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b55663a85f33501257357e6421bb33e769d5c9ffb5ba0921c975a123e35e68" -dependencies = [ - "block-sys", - "objc2 0.4.1", -] - [[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2 0.5.2", + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", ] [[package]] @@ -809,9 +946,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.15.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" dependencies = [ "bytemuck_derive", ] @@ -822,9 +959,9 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -875,7 +1012,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cairo-sys-rs", "glib", "libc", @@ -900,7 +1037,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "log", "polling", "rustix", @@ -1007,9 +1144,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "cfg_aliases" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e53693616d3075149f4ead59bdeecd204ac6b8192d8969757601b74bddf00f" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" @@ -1066,9 +1203,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -1104,6 +1241,7 @@ dependencies = [ "global-hotkey", "iced", "iced_aw", + "iced_fonts", "image 0.25.1", "itertools 0.12.1", "once_cell", @@ -1155,7 +1293,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" dependencies = [ - "smithay-clipboard 0.7.1", + "smithay-clipboard", ] [[package]] @@ -1183,7 +1321,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block", "cocoa-foundation", "core-foundation 0.10.0", @@ -1199,7 +1337,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block", "core-foundation 0.10.0", "core-graphics-types 0.2.0", @@ -1229,37 +1367,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "com" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" -dependencies = [ - "com_macros", -] - -[[package]] -name = "com_macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" -dependencies = [ - "com_macros_support", - "proc-macro2 1.0.80", - "syn 1.0.109", -] - -[[package]] -name = "com_macros_support" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" -dependencies = [ - "proc-macro2 1.0.80", - "quote 1.0.36", - "syn 1.0.109", -] - [[package]] name = "combine" version = "4.6.7" @@ -1301,6 +1408,7 @@ dependencies = [ "common", "iced", "iced_aw", + "iced_fonts", ] [[package]] @@ -1315,9 +1423,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -1431,7 +1539,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation 0.10.0", "core-graphics-types 0.2.0", "foreign-types 0.5.0", @@ -1455,26 +1563,28 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation 0.10.0", "libc", ] [[package]] name = "cosmic-text" -version = "0.10.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75acbfb314aeb4f5210d379af45ed1ec2c98c7f1790bf57b8a4c562ac0c51b71" +checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" dependencies = [ + "bitflags 2.6.0", "fontdb", - "libm", "log", "rangemap", + "rayon", "rustc-hash 1.1.0", "rustybuzz", "self_cell", "swash", "sys-locale", + "ttf-parser 0.21.1", "unicode-bidi", "unicode-linebreak", "unicode-script", @@ -1605,7 +1715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c" dependencies = [ "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -1658,20 +1768,25 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] -name = "d3d12" -version = "0.19.0" +name = "dark-light" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" +checksum = "2a76fa97167fa740dcdbfe18e8895601e1bc36525f09b044e00916e717c03a3c" dependencies = [ - "bitflags 2.5.0", - "libloading 0.8.3", - "winapi", + "dconf_rs", + "detect-desktop-environment", + "dirs 4.0.0", + "objc", + "rust-ini 0.18.0", + "web-sys", + "winreg 0.10.1", + "zbus", ] [[package]] @@ -1692,10 +1807,10 @@ checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "strsim", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -1706,7 +1821,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -1734,6 +1849,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41b319d1b62ffbd002e057f36bebd1f42b9f97927c9577461d855f3513c4289f" +[[package]] +name = "dconf_rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" + [[package]] name = "debugid" version = "0.8.0" @@ -1761,8 +1882,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c65c2ffdafc1564565200967edc4851c7b55422d3913466688907efd05ea26f" dependencies = [ "deno-proc-macro-rules-macros", - "proc-macro2 1.0.80", - "syn 2.0.59", + "proc-macro2 1.0.92", + "syn 2.0.89", ] [[package]] @@ -1772,9 +1893,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3047b312b7451e3190865713a4dd6e1f821aed614ada219766ebc3024a690435" dependencies = [ "once_cell", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -2184,13 +2305,13 @@ dependencies = [ "once_cell", "pmutil", "proc-macro-crate 1.3.1", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "regex", "strum", "strum_macros", "syn 1.0.109", - "syn 2.0.59", + "syn 2.0.89", "thiserror", ] @@ -2409,7 +2530,7 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "syn 1.0.109", ] @@ -2430,9 +2551,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" dependencies = [ "darling", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -2442,7 +2563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" dependencies = [ "derive_builder_core", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -2452,12 +2573,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case 0.4.0", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "rustc_version 0.4.0", "syn 1.0.109", ] +[[package]] +name = "detect-desktop-environment" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" + [[package]] name = "digest" version = "0.8.1" @@ -2541,9 +2668,9 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -2578,6 +2705,12 @@ dependencies = [ "syn 0.15.44", ] +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "dlv-list" version = "0.5.2" @@ -2587,6 +2720,15 @@ dependencies = [ "const-random", ] +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -2605,6 +2747,11 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +[[package]] +name = "dpi" +version = "0.1.1" +source = "git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d#254d6b3420ce4e674f516f7a2bd440665e05484d" + [[package]] name = "dprint-swc-ext" version = "0.11.1" @@ -2627,7 +2774,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bytemuck", "drm-ffi", "drm-fourcc", @@ -2692,7 +2839,7 @@ dependencies = [ "byteorder", "lazy_static", "proc-macro-error", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "syn 1.0.109", ] @@ -2804,6 +2951,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enum-as-inner" version = "0.5.1" @@ -2811,20 +2964,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "syn 1.0.109", ] [[package]] -name = "enum-repr" -version = "0.2.6" +name = "enumflags2" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad30c9c0fa1aaf1ae5010dab11f1117b15d35faf62cda4bbbc53b9987950f18" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" dependencies = [ - "proc-macro2 1.0.80", + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +dependencies = [ + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 1.0.109", + "syn 2.0.89", ] [[package]] @@ -2926,6 +3089,27 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + [[package]] name = "exr" version = "1.72.0" @@ -3083,9 +3267,6 @@ name = "float-cmp" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" -dependencies = [ - "num-traits", -] [[package]] name = "flume" @@ -3117,9 +3298,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "font-types" -version = "0.5.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6784a76a9c2b136ea3b8462391e9328252e938eb706eb44d752723b4c3a533" +checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492" dependencies = [ "bytemuck", ] @@ -3135,16 +3316,16 @@ dependencies = [ [[package]] name = "fontdb" -version = "0.15.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020e203f177c0fb250fb19455a252e838d2bbbce1f80f25ecc42402aafa8cd38" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" dependencies = [ "fontconfig-parser", "log", - "memmap2 0.8.0", + "memmap2 0.9.4", "slotmap", "tinyvec", - "ttf-parser 0.19.2", + "ttf-parser 0.20.0", ] [[package]] @@ -3172,9 +3353,9 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -3206,7 +3387,7 @@ checksum = "a8ef34245e0540c9a3ce7a28340b98d2c12b75da0d446da4e8224923fcaa0c16" dependencies = [ "dirs 5.0.1", "once_cell", - "rust-ini", + "rust-ini 0.20.0", "thiserror", "xdg", ] @@ -3228,9 +3409,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03ec5dc38ee19078d84a692b1c41181ff9f94331c76cee66ff0208c770b5e54f" dependencies = [ "pmutil", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "swc_macros_common", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -3333,15 +3514,28 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-lite" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -3588,7 +3782,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", "libgit2-sys", "log", @@ -3674,7 +3868,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "futures-channel", "futures-core", "futures-executor", @@ -3700,9 +3894,9 @@ dependencies = [ "heck 0.4.1", "proc-macro-crate 2.0.0", "proc-macro-error", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -3731,9 +3925,9 @@ dependencies = [ [[package]] name = "glow" -version = "0.13.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" dependencies = [ "js-sys", "slotmap", @@ -3743,9 +3937,9 @@ dependencies = [ [[package]] name = "glutin_wgl_sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" +checksum = "0a4e1951bbd9434a81aa496fe59ccc2235af3820d27b85f9314e279609211e2c" dependencies = [ "gl_generator", ] @@ -3753,12 +3947,12 @@ dependencies = [ [[package]] name = "glyphon" version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a62d0338e4056db6a73221c2fb2e30619452f6ea9651bac4110f51b0f7a7581" +source = "git+https://github.com/hecrj/glyphon.git?rev=09712a70df7431e9a3b1ac1bbd4fb634096cb3b4#09712a70df7431e9a3b1ac1bbd4fb634096cb3b4" dependencies = [ "cosmic-text", "etagere", "lru", + "rustc-hash 2.0.0", "wgpu", ] @@ -3779,7 +3973,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "gpu-alloc-types", ] @@ -3789,40 +3983,39 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] name = "gpu-allocator" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" dependencies = [ "log", "presser", "thiserror", - "winapi", - "windows 0.52.0", + "windows 0.58.0", ] [[package]] name = "gpu-descriptor" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "gpu-descriptor-types", "hashbrown 0.14.3", ] [[package]] name = "gpu-descriptor-types" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -3894,9 +4087,9 @@ checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro-error", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -3943,6 +4136,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -3950,7 +4146,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] @@ -3969,21 +4165,6 @@ dependencies = [ "hashbrown 0.14.3", ] -[[package]] -name = "hassle-rs" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" -dependencies = [ - "bitflags 2.5.0", - "com", - "libc", - "libloading 0.8.3", - "thiserror", - "widestring", - "winapi", -] - [[package]] name = "heck" version = "0.4.1" @@ -4207,7 +4388,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -4221,14 +4402,12 @@ dependencies = [ [[package]] name = "iced" -version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" dependencies = [ "iced_core", "iced_futures", "iced_renderer", - "iced_runtime", - "iced_sctk", "iced_widget", "iced_winit", "image 0.24.9", @@ -4237,43 +4416,54 @@ dependencies = [ [[package]] name = "iced_aw" -version = "0.8.0" -source = "git+https://github.com/project-gauntlet/iced_aw.git?branch=gauntlet#e686410a358eaf08949f6d146393ef17988132b5" +version = "0.11.99" +source = "git+https://github.com/project-gauntlet/iced_aw.git?branch=gauntlet-0.13#789d481640fbaa83d8e5e00c67440271cabae46b" dependencies = [ "cfg-if", "chrono", "iced", - "itertools 0.12.1", + "iced_fonts", + "itertools 0.13.0", + "num-format", "num-traits", - "once_cell", ] [[package]] name = "iced_core" -version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", + "bytes", + "dark-light", "glam", "log", "num-traits", + "once_cell", "palette", - "raw-window-handle", - "smithay-client-toolkit 0.18.0", + "rustc-hash 2.0.0", "smol_str", "thiserror", "web-time", - "xxhash-rust", +] + +[[package]] +name = "iced_fonts" +version = "0.1.99" +source = "git+https://github.com/project-gauntlet/iced_fonts.git?branch=gauntlet-0.13#a4957aa4374477a26d9a8034a01b32ad961802c2" +dependencies = [ + "iced_core", ] [[package]] name = "iced_futures" -version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" dependencies = [ "futures", "iced_core", "log", + "rustc-hash 2.0.0", "tokio", "wasm-bindgen-futures", "wasm-timer", @@ -4281,10 +4471,10 @@ dependencies = [ [[package]] name = "iced_graphics" -version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bytemuck", "cosmic-text", "half", @@ -4295,16 +4485,15 @@ dependencies = [ "log", "once_cell", "raw-window-handle", - "rustc-hash 1.1.0", + "rustc-hash 2.0.0", "thiserror", "unicode-segmentation", - "xxhash-rust", ] [[package]] name = "iced_renderer" -version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -4315,84 +4504,46 @@ dependencies = [ [[package]] name = "iced_runtime" -version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" dependencies = [ + "bytes", "iced_core", "iced_futures", "raw-window-handle", - "smithay-client-toolkit 0.18.0", "thiserror", ] -[[package]] -name = "iced_sctk" -version = "0.1.0" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" -dependencies = [ - "enum-repr", - "float-cmp", - "futures", - "iced_futures", - "iced_graphics", - "iced_renderer", - "iced_runtime", - "iced_style", - "itertools 0.12.1", - "lazy_static", - "raw-window-handle", - "smithay-client-toolkit 0.18.0", - "smithay-clipboard 0.6.6", - "thiserror", - "tracing", - "wayland-backend", - "wayland-protocols 0.31.2", - "xkbcommon-dl", - "xkeysym", -] - -[[package]] -name = "iced_style" -version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" -dependencies = [ - "iced_core", - "once_cell", - "palette", -] - [[package]] name = "iced_table" -version = "0.12.0" -source = "git+https://github.com/project-gauntlet/iced_table.git?branch=gauntlet#56f588e09ecf2d6f42c81b417dae8479b9a3c568" +version = "0.13.0" +source = "git+https://github.com/project-gauntlet/iced_table.git?branch=gauntlet-0.13#392e36d40e04180bc89c313d3c6272bed2362677" dependencies = [ "iced_core", - "iced_style", "iced_widget", ] [[package]] name = "iced_tiny_skia" -version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" dependencies = [ "bytemuck", "cosmic-text", "iced_graphics", "kurbo 0.10.4", "log", - "rustc-hash 1.1.0", + "rustc-hash 2.0.0", "softbuffer", "tiny-skia", - "xxhash-rust", ] [[package]] name = "iced_wgpu" -version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bytemuck", "futures", "glam", @@ -4401,34 +4552,39 @@ dependencies = [ "iced_graphics", "log", "once_cell", + "rustc-hash 2.0.0", + "thiserror", "wgpu", ] [[package]] name = "iced_widget" -version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" dependencies = [ "iced_renderer", "iced_runtime", - "iced_style", "num-traits", + "once_cell", "ouroboros", + "rustc-hash 2.0.0", "thiserror", "unicode-segmentation", ] [[package]] name = "iced_winit" -version = "0.12.3" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet#056421da9e60f212fc948868e107626ddd04e13f" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" dependencies = [ + "iced_futures", "iced_graphics", "iced_runtime", - "iced_style", "log", + "rustc-hash 2.0.0", "thiserror", "tracing", + "wasm-bindgen-futures", "web-sys", "winapi", "window_clipboard", @@ -4445,17 +4601,6 @@ dependencies = [ "png 0.16.8", ] -[[package]] -name = "icrate" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d3aaff8a54577104bafdf686ff18565c3b6903ca5782a2026ef06e2c7aa319" -dependencies = [ - "block2 0.3.0", - "dispatch", - "objc2 0.4.1", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -4577,7 +4722,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", ] @@ -4651,9 +4796,9 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -4690,9 +4835,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59a85abdc13717906baccb5a1e435556ce0df215f242892f721dff62bf25288f" dependencies = [ "Inflector", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -4723,6 +4868,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -4798,9 +4952,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -4820,7 +4974,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "serde", "unicode-segmentation", ] @@ -4910,7 +5064,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8edfc11b8f56ce85e207e62ea21557cfa09bb24a8f6b04ae181b086ff8611c22" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "regex", "syn 1.0.109", @@ -4922,10 +5076,10 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "regex", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -4975,9 +5129,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "libffi" @@ -5055,7 +5209,7 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", "redox_syscall 0.4.1", ] @@ -5066,7 +5220,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", ] @@ -5141,6 +5295,12 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0b5399f6804fbab912acbd8878ed3532d506b7c951b8f9f164ef90fef39e3f4" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.11" @@ -5223,6 +5383,7 @@ dependencies = [ "common-ui", "iced", "iced_aw", + "iced_fonts", "iced_table", "itertools 0.12.1", "thiserror", @@ -5313,15 +5474,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memmap2" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" -dependencies = [ - "libc", -] - [[package]] name = "memmap2" version = "0.9.4" @@ -5375,11 +5527,11 @@ dependencies = [ [[package]] name = "metal" -version = "0.27.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block", "core-graphics-types 0.1.3", "foreign-types 0.5.0", @@ -5455,7 +5607,7 @@ checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" dependencies = [ "cocoa", "crossbeam-channel", - "dpi", + "dpi 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "gtk", "keyboard-types", "objc", @@ -5491,34 +5643,35 @@ checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" [[package]] name = "naga" -version = "0.19.2" +version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" +checksum = "3d5941e45a15b53aad4375eedf02033adb7a28931eedc31117faffa52e6a857e" dependencies = [ + "arrayvec", "bit-set", - "bitflags 2.5.0", + "bitflags 2.6.0", + "cfg_aliases 0.1.1", "codespan-reporting", "hexf-parse", "indexmap 2.2.6", "log", - "num-traits", "rustc-hash 1.1.0", "spirv", "termcolor", "thiserror", - "unicode-xid 0.2.4", + "unicode-xid 0.2.6", ] [[package]] name = "ndk" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "jni-sys", "log", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", "thiserror", @@ -5539,6 +5692,15 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + [[package]] name = "netif" version = "0.1.6" @@ -5581,6 +5743,19 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset 0.9.1", +] + [[package]] name = "nom" version = "7.1.3" @@ -5677,9 +5852,9 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -5726,9 +5901,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -5760,9 +5935,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate 3.1.0", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -5821,7 +5996,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", - "objc_exception", ] [[package]] @@ -5841,16 +6015,6 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" -[[package]] -name = "objc2" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" -dependencies = [ - "objc-sys", - "objc2-encode 3.0.0", -] - [[package]] name = "objc2" version = "0.5.2" @@ -5858,7 +6022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys", - "objc2-encode 4.0.3", + "objc2-encode", ] [[package]] @@ -5867,25 +6031,49 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.5.0", - "block2 0.5.1", + "bitflags 2.6.0", + "block2", "libc", - "objc2 0.5.2", + "objc2", "objc2-core-data", "objc2-core-image", "objc2-foundation", "objc2-quartz-core", ] +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-data" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.5.0", - "block2 0.5.1", - "objc2 0.5.2", + "bitflags 2.6.0", + "block2", + "objc2", "objc2-foundation", ] @@ -5895,17 +6083,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", + "block2", + "objc2", "objc2-foundation", "objc2-metal", ] [[package]] -name = "objc2-encode" -version = "3.0.0" +name = "objc2-core-location" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] [[package]] name = "objc2-encode" @@ -5919,11 +6113,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.5.0", - "block2 0.5.1", + "bitflags 2.6.0", + "block2", "dispatch", "libc", - "objc2 0.5.2", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", ] [[package]] @@ -5932,9 +6138,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.5.0", - "block2 0.5.1", - "objc2 0.5.2", + "bitflags 2.6.0", + "block2", + "objc2", "objc2-foundation", ] @@ -5944,20 +6150,66 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.5.0", - "block2 0.5.1", - "objc2 0.5.2", + "bitflags 2.6.0", + "block2", + "objc2", "objc2-foundation", "objc2-metal", ] [[package]] -name = "objc_exception" -version = "0.1.2" +name = "objc2-symbols" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "cc", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", ] [[package]] @@ -5989,9 +6241,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "once_map" @@ -5999,7 +6251,7 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed29bb6f7d6ac14023acb332a356f3891265d780e254057c866dbe7a909d2d2d" dependencies = [ - "ahash", + "ahash 0.8.11", "hashbrown 0.15.0", "parking_lot 0.12.1", "stable_deref_trait", @@ -6083,16 +6335,36 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list 0.3.0", + "hashbrown 0.12.3", +] + [[package]] name = "ordered-multimap" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ - "dlv-list", + "dlv-list 0.5.2", "hashbrown 0.14.3", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_info" version = "3.8.2" @@ -6133,10 +6405,10 @@ checksum = "b645dcde5f119c2c454a92d0dfa271a2a3b205da92e4292a68ead4bdbfde1f33" dependencies = [ "heck 0.4.1", "itertools 0.12.1", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "proc-macro2-diagnostics", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -6245,9 +6517,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8890702dbec0bad9116041ae586f84805b13eecd1d8b1df27c29998a9969d6d" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -6275,6 +6547,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.11.2" @@ -6446,7 +6724,7 @@ dependencies = [ "phf_generator 0.10.0", "phf_shared 0.10.0", "proc-macro-hack", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "syn 1.0.109", ] @@ -6459,9 +6737,9 @@ checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ "phf_generator 0.11.2", "phf_shared 0.11.2", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -6503,9 +6781,9 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -6520,6 +6798,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.4.1" @@ -6614,9 +6903,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69e940d8d8db30c6f4cc37dab9aab61f4c9cc1e6efb6d18902ab88fa09c03560" dependencies = [ "darling", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -6625,9 +6914,9 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -6721,7 +7010,7 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "syn 1.0.109", ] @@ -6731,8 +7020,8 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" dependencies = [ - "proc-macro2 1.0.80", - "syn 2.0.59", + "proc-macro2 1.0.92", + "syn 2.0.89", ] [[package]] @@ -6779,7 +7068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "syn 1.0.109", "version_check", @@ -6791,7 +7080,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "version_check", ] @@ -6813,9 +7102,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.80" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -6826,9 +7115,9 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", "version_check", "yansi", ] @@ -6849,7 +7138,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -6911,7 +7200,7 @@ dependencies = [ "prost 0.12.4", "prost-types 0.12.4", "regex", - "syn 2.0.59", + "syn 2.0.89", "tempfile", ] @@ -6923,7 +7212,7 @@ checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", "itertools 0.10.5", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "syn 1.0.109", ] @@ -6936,9 +7225,9 @@ checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" dependencies = [ "anyhow", "itertools 0.12.1", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -7013,7 +7302,7 @@ version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", ] [[package]] @@ -7155,9 +7444,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.19.0" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea75b5ec052843434d263ef7a4c31cf86db5908c729694afb1ad3c884252a1b6" +checksum = "4a04b892cb6f91951f144c33321843790c8574c825aafdb16d815fd7183b5229" dependencies = [ "bytemuck", "font-types", @@ -7172,15 +7461,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -7196,7 +7476,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -7415,12 +7695,12 @@ dependencies = [ "mime", "mime_guess", "once_map", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "rinja_parser", "rustc-hash 2.0.0", "serde", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -7496,7 +7776,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -7521,11 +7801,11 @@ version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "rust-embed-utils", "shellexpand", - "syn 2.0.59", + "syn 2.0.89", "walkdir", ] @@ -7539,6 +7819,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap 0.4.3", +] + [[package]] name = "rust-ini" version = "0.20.0" @@ -7546,7 +7836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" dependencies = [ "cfg-if", - "ordered-multimap", + "ordered-multimap 0.7.3", ] [[package]] @@ -7610,7 +7900,7 @@ version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno 0.3.8", "libc", "linux-raw-sys 0.4.13", @@ -7714,15 +8004,15 @@ checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "rustybuzz" -version = "0.11.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee8fe2a8461a0854a37101fe7a1b13998d0cfa987e43248e81d2a5f4570f6fa" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "bytemuck", "libm", "smallvec", - "ttf-parser 0.20.0", + "ttf-parser 0.21.1", "unicode-bidi-mirroring", "unicode-ccc", "unicode-properties", @@ -7817,14 +8107,14 @@ dependencies = [ [[package]] name = "sctk-adwaita" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b2eaf3a5b264a521b988b2e73042e742df700c4f962cde845d1541adb46550" +checksum = "7555fcb4f753d095d734fdefebb0ad8c98478a21db500492d87c55913d3b0086" dependencies = [ "ab_glyph", "log", "memmap2 0.9.4", - "smithay-client-toolkit 0.18.1", + "smithay-client-toolkit", "tiny-skia", ] @@ -7930,9 +8220,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] @@ -7958,13 +8248,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -7985,9 +8275,9 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -8052,9 +8342,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -8063,6 +8353,7 @@ version = "0.0.0" dependencies = [ "anyhow", "arboard", + "async-channel", "async-stream", "bytes", "cacao", @@ -8081,7 +8372,7 @@ dependencies = [ "itertools 0.10.5", "libc", "numbat", - "objc2 0.5.2", + "objc2", "objc2-app-kit", "objc2-foundation", "once_cell", @@ -8243,6 +8534,16 @@ dependencies = [ "serde", ] +[[package]] +name = "skrifa" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slab" version = "0.4.9" @@ -8278,58 +8579,13 @@ dependencies = [ "version_check", ] -[[package]] -name = "smithay-client-toolkit" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870427e30b8f2cbe64bf43ec4b86e88fe39b0a84b3f15efd9c9c2d020bc86eb9" -dependencies = [ - "bitflags 1.3.2", - "dlib", - "lazy_static", - "log", - "memmap2 0.5.10", - "nix 0.24.3", - "pkg-config", - "wayland-client 0.29.5", - "wayland-cursor 0.29.5", - "wayland-protocols 0.29.5", -] - -[[package]] -name = "smithay-client-toolkit" -version = "0.18.0" -source = "git+https://github.com/smithay/client-toolkit?rev=2e9bf9f#2e9bf9f31698851ca373e5f1e7ba3e6e804e4db1" -dependencies = [ - "bitflags 2.5.0", - "bytemuck", - "calloop", - "calloop-wayland-source", - "cursor-icon", - "libc", - "log", - "memmap2 0.9.4", - "pkg-config", - "rustix", - "thiserror", - "wayland-backend", - "wayland-client 0.31.2", - "wayland-csd-frame", - "wayland-cursor 0.31.1", - "wayland-protocols 0.31.2", - "wayland-protocols-wlr", - "wayland-scanner 0.31.1", - "xkbcommon", - "xkeysym", -] - [[package]] name = "smithay-client-toolkit" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -8341,23 +8597,13 @@ dependencies = [ "wayland-backend", "wayland-client 0.31.2", "wayland-csd-frame", - "wayland-cursor 0.31.1", + "wayland-cursor", "wayland-protocols 0.31.2", "wayland-protocols-wlr", "wayland-scanner 0.31.1", "xkeysym", ] -[[package]] -name = "smithay-clipboard" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a345c870a1fae0b1b779085e81b51e614767c239e93503588e54c5b17f4b0e8" -dependencies = [ - "smithay-client-toolkit 0.16.1", - "wayland-client 0.29.5", -] - [[package]] name = "smithay-clipboard" version = "0.7.1" @@ -8365,7 +8611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" dependencies = [ "libc", - "smithay-client-toolkit 0.18.1", + "smithay-client-toolkit", "wayland-backend", ] @@ -8396,7 +8642,7 @@ checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd" dependencies = [ "as-raw-xcb-connection", "bytemuck", - "cfg_aliases 0.2.0", + "cfg_aliases 0.2.1", "core-graphics 0.23.2", "drm", "fastrand", @@ -8404,7 +8650,7 @@ dependencies = [ "js-sys", "log", "memmap2 0.9.4", - "objc2 0.5.2", + "objc2", "objc2-app-kit", "objc2-foundation", "objc2-quartz-core", @@ -8458,7 +8704,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -8511,7 +8757,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" dependencies = [ - "ahash", + "ahash 0.8.11", "atoi", "byteorder", "bytes", @@ -8519,7 +8765,7 @@ dependencies = [ "crossbeam-queue", "dotenvy", "either", - "event-listener", + "event-listener 2.5.3", "futures-channel", "futures-core", "futures-intrusive", @@ -8551,7 +8797,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "sqlx-core", "sqlx-macros-core", @@ -8569,7 +8815,7 @@ dependencies = [ "heck 0.4.1", "hex", "once_cell", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "serde", "serde_json", @@ -8591,7 +8837,7 @@ checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "bytes", "crc", @@ -8633,7 +8879,7 @@ checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "crc", "dotenvy", @@ -8754,7 +9000,7 @@ checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ "phf_generator 0.10.0", "phf_shared 0.10.0", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", ] @@ -8765,10 +9011,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fa4d4f81d7c05b9161f8de839975d3326328b8ba2831164b465524cc2f55252" dependencies = [ "pmutil", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "swc_macros_common", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -8804,10 +9050,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "rustversion", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -8834,11 +9080,11 @@ dependencies = [ [[package]] name = "swash" -version = "0.1.15" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ec889a8e0a6fcb91041996c8f1f6be0fe1a09e94478785e07c32ce2bca2d2b" +checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2" dependencies = [ - "read-fonts", + "skrifa", "yazi", "zeno", ] @@ -8903,10 +9149,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5b5aaca9a0082be4515f0fbbecc191bf5829cd25b5b9c0a2810f6a2bb0d6829" dependencies = [ "pmutil", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "swc_macros_common", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -8915,7 +9161,7 @@ version = "0.107.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7191c8c57af059b75a2aadc927a2608c3962d19e4d09ce8f9c3f03739ddf833" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "is-macro", "num-bigint", "scoped-tls", @@ -8952,10 +9198,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcdff076dccca6cc6a0e0b2a2c8acfb066014382bc6df98ec99e755484814384" dependencies = [ "pmutil", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "swc_macros_common", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -8998,7 +9244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8d8ca5dd849cea79e6a9792d725f4082ad3ade7a9541fba960c42d55ae778f2" dependencies = [ "better_scoped_tls", - "bitflags 2.5.0", + "bitflags 2.6.0", "indexmap 1.9.3", "once_cell", "phf 0.10.1", @@ -9035,10 +9281,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f59c4b6ed5d78d3ad9fc7c6f8ab4f85bba99573d31d9a2c0a712077a6b45efd2" dependencies = [ "pmutil", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "swc_macros_common", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -9140,9 +9386,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05a95d367e228d52484c53336991fdcf47b6b553ef835d9159db4ba40efb0ee8" dependencies = [ "pmutil", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -9152,9 +9398,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a273205ccb09b51fabe88c49f3b34c5a4631c4c00a16ae20e03111d6a42e832" dependencies = [ "pmutil", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -9175,10 +9421,10 @@ checksum = "0f322730fb82f3930a450ac24de8c98523af7d34ab8cb2f46bcb405839891a99" dependencies = [ "Inflector", "pmutil", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "swc_macros_common", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -9198,18 +9444,18 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.59" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "unicode-ident", ] @@ -9226,10 +9472,10 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "syn 1.0.109", - "unicode-xid 0.2.4", + "unicode-xid 0.2.6", ] [[package]] @@ -9454,22 +9700,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -9634,9 +9880,9 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -9807,10 +10053,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4ef6dd70a610078cb4e338a0f79d06bc759ff1b22d2120c2ff02ae264ba9c2" dependencies = [ "prettyplease 0.2.19", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "prost-build 0.12.4", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -9863,9 +10109,9 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -9918,7 +10164,7 @@ dependencies = [ "dirs 5.0.1", "libappindicator", "muda", - "objc2 0.5.2", + "objc2", "objc2-app-kit", "objc2-foundation", "once_cell", @@ -10004,18 +10250,18 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ttf-parser" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" - [[package]] name = "ttf-parser" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + [[package]] name = "typed-arena" version = "2.0.2" @@ -10040,6 +10286,17 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset 0.9.1", + "tempfile", + "winapi", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -10098,15 +10355,15 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-bidi-mirroring" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" [[package]] name = "unicode-ccc" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" [[package]] name = "unicode-id" @@ -10167,9 +10424,9 @@ checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unicode_categories" @@ -10420,7 +10677,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", ] @@ -10469,34 +10726,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -10506,9 +10764,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote 1.0.36", "wasm-bindgen-macro-support", @@ -10516,22 +10774,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasm-streams" @@ -10585,7 +10843,6 @@ dependencies = [ "downcast-rs", "libc", "nix 0.24.3", - "scoped-tls", "wayland-commons", "wayland-scanner 0.29.5", "wayland-sys 0.29.5", @@ -10597,7 +10854,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "rustix", "wayland-backend", "wayland-scanner 0.31.1", @@ -10621,22 +10878,11 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cursor-icon", "wayland-backend", ] -[[package]] -name = "wayland-cursor" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" -dependencies = [ - "nix 0.24.3", - "wayland-client 0.29.5", - "xcursor", -] - [[package]] name = "wayland-cursor" version = "0.31.1" @@ -10666,7 +10912,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client 0.31.2", "wayland-scanner 0.31.1", @@ -10678,7 +10924,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client 0.31.2", "wayland-protocols 0.31.2", @@ -10691,7 +10937,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client 0.31.2", "wayland-protocols 0.31.2", @@ -10704,7 +10950,7 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", "xml-rs", ] @@ -10715,7 +10961,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quick-xml", "quote 1.0.36", ] @@ -10726,8 +10972,6 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" dependencies = [ - "dlib", - "lazy_static", "pkg-config", ] @@ -10745,9 +10989,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", @@ -10755,9 +10999,9 @@ dependencies = [ [[package]] name = "web-time" -version = "0.2.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -10786,13 +11030,13 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "wgpu" -version = "0.19.3" +version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b1213b52478a7631d6e387543ed8f642bc02c578ef4e3b49aca2a29a7df0cb" +checksum = "76ab52f2d3d18b70d5ab8dd270a1cff3ebe6dbe4a7d13c1cc2557138a9777fdc" dependencies = [ "arrayvec", - "cfg-if", "cfg_aliases 0.1.1", + "document-features", "js-sys", "log", "naga", @@ -10811,15 +11055,15 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "0.19.3" +version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f6b033c2f00ae0bc8ea872c5989777c60bc241aac4e58b24774faa8b391f78" +checksum = "0e0c68e7b6322a03ee5b83fcd92caeac5c2a932f6457818179f4652ad2a9c065" dependencies = [ "arrayvec", "bit-vec", - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg_aliases 0.1.1", - "codespan-reporting", + "document-features", "indexmap 2.2.6", "log", "naga", @@ -10830,32 +11074,30 @@ dependencies = [ "rustc-hash 1.1.0", "smallvec", "thiserror", - "web-sys", "wgpu-hal", "wgpu-types", ] [[package]] name = "wgpu-hal" -version = "0.19.3" +version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f972c280505ab52ffe17e94a7413d9d54b58af0114ab226b9fc4999a47082e" +checksum = "de6e7266b869de56c7e3ed72a954899f71d14fec6cc81c102b7530b92947601b" dependencies = [ "android_system_properties", "arrayvec", "ash", "bit-set", - "bitflags 2.5.0", + "bitflags 2.6.0", "block", + "bytemuck", "cfg_aliases 0.1.1", "core-graphics-types 0.1.3", - "d3d12", "glow", "glutin_wgl_sys", "gpu-alloc", "gpu-allocator", "gpu-descriptor", - "hassle-rs", "js-sys", "khronos-egl", "libc", @@ -10863,7 +11105,7 @@ dependencies = [ "log", "metal", "naga", - "ndk-sys", + "ndk-sys 0.5.0+25.2.9519653", "objc", "once_cell", "parking_lot 0.12.1", @@ -10877,16 +11119,17 @@ dependencies = [ "wasm-bindgen", "web-sys", "wgpu-types", - "winapi", + "windows 0.58.0", + "windows-core 0.58.0", ] [[package]] name = "wgpu-types" -version = "0.19.2" +version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805" +checksum = "610f6ff27778148c31093f3b03abc4840f9636d58d597ca2f5977433acfe0068" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "js-sys", "web-sys", ] @@ -10985,11 +11228,11 @@ dependencies = [ [[package]] name = "windows" -version = "0.52.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-core", + "windows-core 0.58.0", "windows-targets 0.52.6", ] @@ -11002,6 +11245,60 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.36", + "syn 2.0.89", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.36", + "syn 2.0.89", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -11218,37 +11515,40 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" -version = "0.29.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d59ad965a635657faf09c8f062badd885748428933dad8e8bdd64064d92e5ca" +version = "0.30.1" +source = "git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d#254d6b3420ce4e674f516f7a2bd440665e05484d" dependencies = [ - "ahash", + "ahash 0.8.11", "android-activity", "atomic-waker", - "bitflags 2.5.0", + "bitflags 2.6.0", + "block2", "bytemuck", "calloop", - "cfg_aliases 0.1.1", + "cfg_aliases 0.2.1", + "concurrent-queue", "core-foundation 0.9.4", "core-graphics 0.23.2", "cursor-icon", - "icrate", + "dpi 0.1.1 (git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d)", "js-sys", "libc", - "log", "memmap2 0.9.4", "ndk", - "ndk-sys", - "objc2 0.4.1", - "once_cell", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", "orbclient", "percent-encoding", + "pin-project", "raw-window-handle", - "redox_syscall 0.3.5", + "redox_syscall 0.4.1", "rustix", "sctk-adwaita", - "smithay-client-toolkit 0.18.1", + "smithay-client-toolkit", "smol_str", + "tracing", "unicode-segmentation", "wasm-bindgen", "wasm-bindgen-futures", @@ -11258,7 +11558,7 @@ dependencies = [ "wayland-protocols-plasma", "web-sys", "web-time", - "windows-sys 0.48.0", + "windows-sys 0.52.0", "x11-dl", "x11rb 0.13.0", "xkbcommon-dl", @@ -11424,14 +11724,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" [[package]] -name = "xkbcommon" -version = "0.7.0" +name = "xdg-home" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" dependencies = [ "libc", - "memmap2 0.8.0", - "xkeysym", + "windows-sys 0.59.0", ] [[package]] @@ -11440,7 +11739,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "dlib", "log", "once_cell", @@ -11452,9 +11751,6 @@ name = "xkeysym" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" -dependencies = [ - "bytemuck", -] [[package]] name = "xml-rs" @@ -11468,12 +11764,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" -[[package]] -name = "xxhash-rust" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03" - [[package]] name = "yansi" version = "1.0.1" @@ -11486,6 +11776,68 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.3.1", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate 3.1.0", + "proc-macro2 1.0.92", + "quote 1.0.36", + "syn 2.0.89", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zeno" version = "0.2.3" @@ -11507,9 +11859,9 @@ version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -11527,9 +11879,9 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ - "proc-macro2 1.0.80", + "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.59", + "syn 2.0.89", ] [[package]] @@ -11583,3 +11935,40 @@ checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" dependencies = [ "zune-core", ] + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate 3.1.0", + "proc-macro2 1.0.92", + "quote 1.0.36", + "syn 2.0.89", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.36", + "syn 2.0.89", +] diff --git a/Cargo.toml b/Cargo.toml index e8ef4af..4d9d4ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,12 +16,14 @@ members = [ "rust/scenario_runner", ] [workspace.dependencies] -#iced = { version = "0.12.4", features = ["tokio", "lazy", "advanced", "image", "multi-window"] } -iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet", features = ["tokio", "lazy", "advanced", "image", "multi-window"] } -#iced_aw = { version = "0.9.0", features = ["icons", "date_picker", "floating_element", "wrap", "number_input", "grid", "spinner"] } -iced_aw = { git = "https://github.com/project-gauntlet/iced_aw.git", branch = "gauntlet", default-features = false, features = ["icons", "date_picker", "floating_element", "wrap", "number_input", "grid", "spinner"] } +#iced = { version = "0.13.99", features = ["tokio", "lazy", "advanced", "image"] } +iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", features = ["tokio", "lazy", "advanced", "image"] } +#iced_aw = { version = "0.11.99", features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } +iced_aw = { git = "https://github.com/project-gauntlet/iced_aw.git", branch = "gauntlet-0.13", default-features = false, features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } #iced_table = "0.13.0" -iced_table = { git = "https://github.com/project-gauntlet/iced_table.git", branch = "gauntlet" } +iced_table = { git = "https://github.com/project-gauntlet/iced_table.git", branch = "gauntlet-0.13" } +#iced_fonts = { version = "0.1.1", features = ["bootstrap"] } +iced_fonts = { git = "https://github.com/project-gauntlet/iced_fonts.git", branch = "gauntlet-0.13", features = ["bootstrap"] } [dependencies] cli = { path = "rust/cli" } diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index b5a779a..9bbb3aa 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -7,6 +7,7 @@ tokio = "1.28.1" anyhow = { version = "1", features = ["backtrace"] } thiserror = "1" iced_aw.workspace = true +iced_fonts.workspace = true tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } common = { path = "../common" } @@ -27,7 +28,8 @@ tray-icon = { version = "0.15.1", default-features = false } [target.'cfg(target_os = "linux")'.dependencies] iced.workspace = true -iced.features = ["wayland"] +# TODO +#iced.features = ["wayland"] [target.'cfg(not(target_os = "linux"))'.dependencies] iced.workspace = true diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index ecbb9b7..2dd6f51 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -3,7 +3,7 @@ use crate::ui::widget::{ActionPanel, ComponentWidgetEvent}; use crate::ui::widget_container::PluginWidgetContainer; use crate::ui::AppMsg; use common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiWidgetId}; -use iced::Command; +use iced::Task; use std::collections::HashMap; pub struct ClientContext { @@ -112,15 +112,15 @@ impl ClientContext { } } - pub fn append_text(&self, text: &str) -> Command { + pub fn append_text(&self, text: &str) -> Task { self.view.append_text(text) } - pub fn backspace_text(&self) -> Command { + pub fn backspace_text(&self) -> Task { self.view.backspace_text() } - pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Command { + pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Task { self.view.focus_search_bar(widget_id) } @@ -132,19 +132,19 @@ impl ClientContext { self.view.get_action_ids() } - pub fn focus_up(&self) -> Command { + pub fn focus_up(&self) -> Task { self.view.focus_up() } - pub fn focus_down(&self) -> Command { + pub fn focus_down(&self) -> Task { self.view.focus_down() } - pub fn focus_left(&self) -> Command { + pub fn focus_left(&self) -> Task { self.view.focus_left() } - pub fn focus_right(&self) -> Command { + pub fn focus_right(&self) -> Task { self.view.focus_right() } } diff --git a/rust/client/src/ui/custom_widgets/loading_bar.rs b/rust/client/src/ui/custom_widgets/loading_bar.rs index 16af639..1afeb0d 100644 --- a/rust/client/src/ui/custom_widgets/loading_bar.rs +++ b/rust/client/src/ui/custom_widgets/loading_bar.rs @@ -10,7 +10,6 @@ use iced::advanced::Shell; use iced::advanced::Widget; use iced::event::Status; use iced::mouse::Cursor; -use iced::{window, Color}; use iced::Border; use iced::Element; use iced::Event; @@ -18,22 +17,23 @@ use iced::Length; use iced::Rectangle; use iced::Shadow; use iced::Size; +use iced::{window, Color}; use std::time::{Duration, Instant}; -pub struct LoadingBar +pub struct LoadingBar<'a, Theme> where - Theme: StyleSheet, + Theme: Catalog, { width: Length, segment_width: f32, height: Length, rate: Duration, - style: ::Style, + class: ::Class<'a>, } -impl Default for LoadingBar +impl<'a, Theme> Default for LoadingBar<'a, Theme> where - Theme: StyleSheet, + Theme: Catalog, { fn default() -> Self { Self { @@ -41,14 +41,14 @@ where segment_width: 200.0, height: Length::Fixed(1.0), rate: Duration::from_secs_f32(1.0), - style: ::Style::default(), + class: ::Class::default(), } } } -impl LoadingBar +impl<'a, Theme> LoadingBar<'a, Theme> where - Theme: StyleSheet, + Theme: Catalog, { #[must_use] pub fn new() -> Self { @@ -74,8 +74,8 @@ where } #[must_use] - pub fn style(mut self, style: ::Style) -> Self { - self.style = style; + pub fn class(mut self, class: ::Class<'a>) -> Self { + self.class = class; self } } @@ -89,10 +89,10 @@ fn is_visible(bounds: &Rectangle) -> bool { bounds.width > 0.0 && bounds.height > 0.0 } -impl Widget for LoadingBar +impl<'a, Message, Theme, Renderer> Widget for LoadingBar<'a, Theme> where Renderer: renderer::Renderer, - Theme: StyleSheet, + Theme: Catalog, { fn size(&self) -> Size { Size::new(self.width, self.height) @@ -124,7 +124,7 @@ where let position = bounds.position(); let size = bounds.size(); - let styling = theme.appearance(&self.style); + let styling = Catalog::style(theme, &self.class); renderer.fill_quad( renderer::Quad { @@ -184,7 +184,7 @@ where let bounds = layout.bounds(); - if let Event::Window(_id, window::Event::RedrawRequested(now)) = event { + if let Event::Window(window::Event::RedrawRequested(now)) = event { if is_visible(&bounds) { let state = state.state.downcast_mut::(); let duration = (now - state.last_update).as_secs_f32(); @@ -214,23 +214,26 @@ where } #[derive(Clone, Copy, Debug)] -pub struct Appearance { +pub struct Style { pub background_color: Color, pub loading_bar_color: Color, } -pub trait StyleSheet { - type Style: Default; - fn appearance(&self, style: &Self::Style) -> Appearance; +pub trait Catalog { + type Class<'a>: Default; + + fn default<'a>() -> Self::Class<'a>; + + fn style(&self, class: &Self::Class<'_>) -> Style; } -impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> +impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where Renderer: renderer::Renderer + 'a, - Theme: 'a + StyleSheet, + Theme: 'a + Catalog, { - fn from(spinner: LoadingBar) -> Self { + fn from(spinner: LoadingBar<'a, Theme>) -> Self { Self::new(spinner) } } diff --git a/rust/client/src/ui/hud/mod.rs b/rust/client/src/ui/hud/mod.rs index c45a853..b1c4887 100644 --- a/rust/client/src/ui/hud/mod.rs +++ b/rust/client/src/ui/hud/mod.rs @@ -1,7 +1,7 @@ use crate::ui::AppMsg; use iced::advanced::layout::Limits; use iced::window::{Level, Position, Settings}; -use iced::{window, Command, Size}; +use iced::{window, Size, Task}; use std::convert; use std::time::Duration; @@ -9,9 +9,10 @@ const HUD_WINDOW_WIDTH: f32 = 400.0; const HUD_WINDOW_HEIGHT: f32 = 40.0; pub fn show_hud_window( - #[cfg(target_os = "linux")] - wayland: bool, -) -> Command { + // TODO + // #[cfg(target_os = "linux")] + // wayland: bool, +) -> Task { let settings = Settings { size: Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT), @@ -21,84 +22,92 @@ pub fn show_hud_window( transparent: true, visible: true, level: Level::AlwaysOnTop, - #[cfg(target_os = "macos")] - platform_specific: iced::window::settings::PlatformSpecific { - activation_policy: window::settings::ActivationPolicy::Accessory, - activate_ignoring_other_apps: false, - ..Default::default() - }, + // #[cfg(target_os = "macos")] + // platform_specific: iced::window::settings::PlatformSpecific { + // activation_policy: window::settings::ActivationPolicy::Accessory, + // activate_ignoring_other_apps: false, + // ..Default::default() + // }, exit_on_close_request: false, ..Default::default() }; - #[cfg(target_os = "linux")] - if wayland { - let id = window::Id::unique(); + // #[cfg(target_os = "linux")] + // if wayland { + // let id = window::Id::unique(); + // + // let show_command = iced::wayland::commands::layer_surface::get_layer_surface(layer_shell_settings(id)); + // let close_command = in_2_seconds(AppMsg::CloseHudWindow { id }); + // + // Task::batch([ + // show_command, + // close_command + // ]) + // } else { + // let (id, show_command) = window::spawn(settings); + // let close_command = in_2_seconds(AppMsg::CloseHudWindow { id }); + // + // Task::batch([ + // show_command, + // close_command + // ]) + // } + // + // #[cfg(not(target_os = "linux"))] + // { + // let (id, show_command) = window::spawn(settings); + // let close_command = in_2_seconds(AppMsg::CloseHudWindow { id }); + // + // Task::batch([ + // show_command, + // close_command + // ]) + // } - let show_command = iced::wayland::commands::layer_surface::get_layer_surface(layer_shell_settings(id)); - let close_command = in_2_seconds(AppMsg::CloseHudWindow { id }); - - Command::batch([ - show_command, - close_command - ]) - } else { - let (id, show_command) = window::spawn(settings); - let close_command = in_2_seconds(AppMsg::CloseHudWindow { id }); - - Command::batch([ - show_command, - close_command - ]) - } - - #[cfg(not(target_os = "linux"))] - { - let (id, show_command) = window::spawn(settings); - let close_command = in_2_seconds(AppMsg::CloseHudWindow { id }); - - Command::batch([ - show_command, - close_command - ]) - } + window::open(settings) + .1 + .then(|id| in_2_seconds(AppMsg::CloseHudWindow { id })) } pub fn close_hud_window( - #[cfg(target_os = "linux")] - wayland: bool, + // #[cfg(target_os = "linux")] + // wayland: bool, id: window::Id -) -> Command { - #[cfg(target_os = "linux")] - if wayland { - iced::wayland::commands::layer_surface::destroy_layer_surface(id) - } else { - window::close(id) - } +) -> Task { + // TODO + // #[cfg(target_os = "linux")] + // if wayland { + // iced::wayland::commands::layer_surface::destroy_layer_surface(id) + // } else { + // window::close(id) + // } + // + // #[cfg(not(target_os = "linux"))] + // window::close(id) - #[cfg(not(target_os = "linux"))] window::close(id) } -#[cfg(target_os = "linux")] -fn layer_shell_settings(id: window::Id) -> iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { - iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { - id, - layer: iced::wayland::commands::layer_surface::Layer::Overlay, - keyboard_interactivity: iced::wayland::commands::layer_surface::KeyboardInteractivity::None, - pointer_interactivity: false, - anchor: iced::wayland::commands::layer_surface::Anchor::empty(), - output: Default::default(), - namespace: "Gauntlet HUD".to_string(), - margin: Default::default(), - exclusive_zone: 0, - size: Some((Some(HUD_WINDOW_WIDTH as u32), Some(HUD_WINDOW_HEIGHT as u32))), - size_limits: Limits::new(Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT), Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT)), - } -} +// TODO +// #[cfg(target_os = "linux")] +// fn layer_shell_settings(id: window::Id) -> iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { +// iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { +// id, +// layer: iced::wayland::commands::layer_surface::Layer::Overlay, +// keyboard_interactivity: iced::wayland::commands::layer_surface::KeyboardInteractivity::None, +// pointer_interactivity: false, +// anchor: iced::wayland::commands::layer_surface::Anchor::empty(), +// output: Default::default(), +// namespace: "Gauntlet HUD".to_string(), +// margin: Default::default(), +// exclusive_zone: 0, +// size: Some((Some(HUD_WINDOW_WIDTH as u32), Some(HUD_WINDOW_HEIGHT as u32))), +// size_limits: Limits::new(Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT), Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT)), +// } +// } -fn in_2_seconds(msg: AppMsg) -> Command { - Command::perform(async move { +fn in_2_seconds(msg: AppMsg) -> Task { + Task::perform(async move { tokio::time::sleep(Duration::from_secs(2)).await; msg diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 6131966..57bc428 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -5,25 +5,24 @@ use iced::advanced::graphics::core::SmolStr; use iced::advanced::layout::Limits; use iced::futures::channel::mpsc::Sender; use iced::futures::SinkExt; -use iced::keyboard::key::Named; +use iced::keyboard::key::{Named, Physical}; use iced::keyboard::{Key, Modifiers}; -use iced::multi_window::Application; use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; use iced::widget::text::Shaping; use iced::widget::text_input::focus; use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, Space}; use iced::window::settings::PlatformSpecific; use iced::window::{Level, Position, Screenshot}; -use iced::{event, executor, font, futures, keyboard, subscription, window, Alignment, Command, Event, Font, Length, Padding, Pixels, Settings, Size, Subscription}; -use iced_aw::core::icons; +use iced::{event, executor, font, futures, keyboard, stream, window, Alignment, Event, Font, Length, Padding, Pixels, Renderer, Settings, Size, Subscription, Task}; use std::collections::HashMap; use std::fs; use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex as StdMutex, Mutex, RwLock as StdRwLock}; +use iced::alignment::{Horizontal, Vertical}; +use iced_fonts::BOOTSTRAP_FONT_BYTES; use serde::Deserialize; use tokio::sync::{Mutex as TokioMutex, RwLock as TokioRwLock}; -use tonic::transport::Server; use client_context::ClientContext; use common::model::{BackendRequestData, BackendResponseData, EntrypointId, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; @@ -31,7 +30,7 @@ use common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFron use common::scenario_convert::{ui_render_location_from_scenario}; use common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; use common_ui::physical_key_model; -use utils::channel::{channel, RequestReceiver, RequestSender, Responder}; +use utils::channel::{RequestReceiver, RequestSender, Responder}; use crate::model::UiViewEvent; use crate::ui::search_list::search_list; @@ -67,8 +66,8 @@ pub struct AppModel { global_hotkey_manager: Arc>, current_hotkey: Arc>>, frontend_receiver: Arc>>, + main_window_id: window::Id, focused: bool, - theme: GauntletComplexTheme, wayland: bool, #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: tray_icon::TrayIcon, @@ -193,1554 +192,1539 @@ pub enum AppMsg { }, } -pub struct AppFlags { - frontend_receiver: RequestReceiver, - backend_sender: RequestSender, - wayland: bool, -} - -impl Default for AppFlags { - fn default() -> Self { - panic!("not needed") - } -} - const WINDOW_WIDTH: f32 = 750.0; const WINDOW_HEIGHT: f32 = 450.0; -#[cfg(target_os = "linux")] -fn layer_shell_settings() -> iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { - iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { - id: window::Id::MAIN, - layer: iced::wayland::commands::layer_surface::Layer::Overlay, - keyboard_interactivity: iced::wayland::commands::layer_surface::KeyboardInteractivity::Exclusive, - pointer_interactivity: true, - anchor: iced::wayland::commands::layer_surface::Anchor::empty(), - output: Default::default(), - namespace: "Gauntlet".to_string(), - margin: Default::default(), - exclusive_zone: 0, - size: Some((Some(WINDOW_WIDTH as u32), Some(WINDOW_HEIGHT as u32))), - size_limits: Limits::new(Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), Size::new(WINDOW_WIDTH, WINDOW_HEIGHT)), - } -} +// #[cfg(target_os = "linux")] +// fn layer_shell_settings() -> iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { +// iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { +// id: window::Id::MAIN, +// layer: iced::wayland::commands::layer_surface::Layer::Overlay, +// keyboard_interactivity: iced::wayland::commands::layer_surface::KeyboardInteractivity::Exclusive, +// pointer_interactivity: true, +// anchor: iced::wayland::commands::layer_surface::Anchor::empty(), +// output: Default::default(), +// namespace: "Gauntlet".to_string(), +// margin: Default::default(), +// exclusive_zone: 0, +// size: Some((Some(WINDOW_WIDTH as u32), Some(WINDOW_HEIGHT as u32))), +// size_limits: Limits::new(Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), Size::new(WINDOW_WIDTH, WINDOW_HEIGHT)), +// } +// } pub fn run( minimized: bool, frontend_receiver: RequestReceiver, backend_sender: RequestSender, ) { - let default_settings: Settings<()> = Settings::default(); + // #[cfg(target_os = "linux")] + // let wayland = std::env::var("WAYLAND_DISPLAY") + // .or_else(|_| std::env::var("WAYLAND_SOCKET")) + // .is_ok(); + // + // #[cfg(not(target_os = "linux"))] + // let wayland = false; - #[cfg(target_os = "linux")] - let wayland = std::env::var("WAYLAND_DISPLAY") - .or_else(|_| std::env::var("WAYLAND_SOCKET")) - .is_ok(); + let wayland = false; // TODO remove - #[cfg(not(target_os = "linux"))] - let wayland = false; + let theme = GauntletComplexTheme::new(); - let flags = AppFlags { - frontend_receiver, - backend_sender, - wayland - }; + iced::daemon::("Gauntlet Settings", update, view) + .subscription(subscription) + .theme(move |_, _| theme.clone()) + .run_with(move || new(frontend_receiver, backend_sender, wayland, minimized)) + .expect("Unable to start settings application"); - let settings = Settings { - id: None, - window: window::Settings { - size: Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), - position: Position::Centered, - resizable: false, - decorations: false, - transparent: true, - visible: !minimized, - #[cfg(target_os = "macos")] - platform_specific: PlatformSpecific { - activation_policy: window::settings::ActivationPolicy::Accessory, - activate_ignoring_other_apps: false, - ..Default::default() - }, - ..Default::default() - }, - #[cfg(target_os = "linux")] - initial_surface: iced::wayland::settings::InitialSurface::LayerSurface(layer_shell_settings()), - flags, - fonts: default_settings.fonts, - default_font: default_settings.default_font, - default_text_size: default_settings.default_text_size, - antialiasing: default_settings.antialiasing, - #[cfg(target_os = "linux")] - exit_on_close_request: false, - }; - - #[cfg(target_os = "linux")] - let result = if wayland { - AppModel::run_wayland(settings) - } else { - AppModel::run(settings) - }; - - #[cfg(not(target_os = "linux"))] - let result = AppModel::run(settings); - - result.expect("Unable to start application") + // #[cfg(target_os = "linux")] + // let result = if wayland { + // AppModel::run_wayland(settings) + // } else { + // AppModel::run(settings) + // }; + // + // #[cfg(not(target_os = "linux"))] + // let result = AppModel::run(settings); + // + // result.expect("Unable to start application") } -impl Application for AppModel { - type Executor = executor::Default; - type Message = AppMsg; - type Theme = GauntletComplexTheme; - type Flags = AppFlags; +fn new( + frontend_receiver: RequestReceiver, + backend_sender: RequestSender, + wayland: bool, + minimized: bool, +) -> (AppModel, Task) { + let backend_api = BackendForFrontendApi::new(backend_sender); - fn new(flags: Self::Flags) -> (Self, Command) { - let frontend_receiver = flags.frontend_receiver; - let backend_sender = flags.backend_sender; - let wayland = flags.wayland; + let global_hotkey_manager = GlobalHotKeyManager::new() + .expect("unable to create global hot key manager"); - let backend_api = BackendForFrontendApi::new(backend_sender); + let mut commands = vec![ + font::load(BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded), + ]; - let global_hotkey_manager = GlobalHotKeyManager::new() - .expect("unable to create global hot key manager"); + let settings = window::Settings { + size: Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), + position: Position::Centered, + resizable: false, + decorations: false, + transparent: true, + visible: !minimized, + // #[cfg(target_os = "macos")] + // platform_specific: PlatformSpecific { + // activation_policy: window::settings::ActivationPolicy::Accessory, + // activate_ignoring_other_apps: false, + // ..Default::default() + // }, + ..Default::default() + }; + // #[cfg(target_os = "linux")] + // initial_surface: iced::wayland::settings::InitialSurface::LayerSurface(layer_shell_settings()), + // #[cfg(target_os = "linux")] + // exit_on_close_request: false, + // - let mut commands = vec![ - font::load(icons::BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded), - ]; + let (main_window_id, open_task) = window::open(settings); - if !wayland { - commands.push( - window::gain_focus(window::Id::MAIN), - ); + commands.push( + open_task.map(|_| AppMsg::Noop), + ); - commands.push( - window::change_level(window::Id::MAIN, Level::AlwaysOnTop), - ) - } + if !wayland { + commands.push( + window::gain_focus(main_window_id), + ); - let (client_context, global_state) = if cfg!(feature = "scenario_runner") { - let gen_in = std::env::var("GAUNTLET_SCREENSHOT_GEN_IN") - .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_IN"); - - let gen_in = fs::read_to_string(gen_in) - .expect("Unable to read file at GAUNTLET_SCREENSHOT_GEN_IN"); - - let gen_out = std::env::var("GAUNTLET_SCREENSHOT_GEN_OUT") - .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_OUT"); - - let gen_name = std::env::var("GAUNTLET_SCREENSHOT_GEN_NAME") - .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_NAME"); - - let event: ScenarioFrontendEvent = serde_json::from_str(&gen_in) - .expect("GAUNTLET_SCREENSHOT_GEN_IN is not valid json"); - - commands.push( - Command::perform( - async { - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - }, - |_| AppMsg::Screenshot { save_path: gen_out }, - ) - ); - - match event { - ScenarioFrontendEvent::ReplaceView { entrypoint_id, render_location, top_level_view, container, images } => { - let plugin_id = PluginId::from_string("__SCREENSHOT_GEN___"); - let entrypoint_id = EntrypointId::from_string(entrypoint_id); - - let mut context = ClientContext::new(); - - let render_location = ui_render_location_from_scenario(render_location); - let container = RootWidget::deserialize(container).expect("should always be valid"); - let has_children = container.content.is_some(); - - // ignore commands because screenshots are non-interactive - let _ = context.replace_view( - render_location, - container, - images, - &plugin_id, - "Screenshot Plugin", - &entrypoint_id, - "Screenshot Entrypoint", - ); - - let context = Arc::new(StdRwLock::new(context)); - - commands.push(Command::perform(async { }, move |_| AppMsg::ReplaceView { top_level_view, has_children, render_location })); - - let state= match render_location { - UiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique(), context.clone()), - UiRenderLocation::View => GlobalState::new_plugin( - PluginViewData { - top_level_view, - plugin_id, - plugin_name: "Screenshot Gen".to_string(), - entrypoint_id, - entrypoint_name: gen_name, - action_shortcuts: Default::default(), - }, - context.clone() - ) - }; - - (context, state) - } - ScenarioFrontendEvent::ShowPreferenceRequiredView { entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { - let error_view = ErrorViewData::PreferenceRequired { - plugin_id: PluginId::from_string("__SCREENSHOT_GEN___"), - entrypoint_id: EntrypointId::from_string(entrypoint_id), - plugin_preferences_required, - entrypoint_preferences_required, - }; - - (Arc::new(StdRwLock::new(ClientContext::new())), GlobalState::new_error(error_view)) - } - ScenarioFrontendEvent::ShowPluginErrorView { entrypoint_id, render_location: _ } => { - let error_view = ErrorViewData::PluginError { - plugin_id: PluginId::from_string("__SCREENSHOT_GEN___"), - entrypoint_id: EntrypointId::from_string(entrypoint_id), - }; - - (Arc::new(StdRwLock::new(ClientContext::new())), GlobalState::new_error(error_view)) - } - } - } else { - let context = Arc::new(StdRwLock::new(ClientContext::new())); - (context.clone(), GlobalState::new(text_input::Id::unique(), context.clone())) - }; - - ( - AppModel { - // logic - backend_api, - global_hotkey_manager: Arc::new(StdRwLock::new(global_hotkey_manager)), - current_hotkey: Arc::new(StdMutex::new(None)), - frontend_receiver: Arc::new(TokioRwLock::new(frontend_receiver)), - focused: false, - theme: GauntletComplexTheme::new(), - wayland, - #[cfg(any(target_os = "macos", target_os = "windows"))] - tray_icon: sys_tray::create_tray(), - - // ephemeral state - prompt: "".to_string(), - - // state - global_state, - client_context, - search_results: vec![], - loading_bar_state: HashMap::new(), - hud_display: None, - }, - Command::batch(commands), + commands.push( + window::change_level(main_window_id, Level::AlwaysOnTop), ) } - fn title(&self, window: window::Id) -> String { - if window != window::Id::MAIN { - "Gauntlet".to_owned() - } else { - "Gauntlet HUD".to_owned() + let (client_context, global_state) = if cfg!(feature = "scenario_runner") { + let gen_in = std::env::var("GAUNTLET_SCREENSHOT_GEN_IN") + .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_IN"); + + let gen_in = fs::read_to_string(gen_in) + .expect("Unable to read file at GAUNTLET_SCREENSHOT_GEN_IN"); + + let gen_out = std::env::var("GAUNTLET_SCREENSHOT_GEN_OUT") + .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_OUT"); + + let gen_name = std::env::var("GAUNTLET_SCREENSHOT_GEN_NAME") + .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_NAME"); + + let event: ScenarioFrontendEvent = serde_json::from_str(&gen_in) + .expect("GAUNTLET_SCREENSHOT_GEN_IN is not valid json"); + + commands.push( + Task::perform( + async { + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + }, + move |_| AppMsg::Screenshot { save_path: gen_out.clone() }, + ) + ); + + match event { + ScenarioFrontendEvent::ReplaceView { entrypoint_id, render_location, top_level_view, container, images } => { + let plugin_id = PluginId::from_string("__SCREENSHOT_GEN___"); + let entrypoint_id = EntrypointId::from_string(entrypoint_id); + + let mut context = ClientContext::new(); + + let render_location = ui_render_location_from_scenario(render_location); + let container = RootWidget::deserialize(container).expect("should always be valid"); + let has_children = container.content.is_some(); + + // ignore commands because screenshots are non-interactive + let _ = context.replace_view( + render_location, + container, + images, + &plugin_id, + "Screenshot Plugin", + &entrypoint_id, + "Screenshot Entrypoint", + ); + + let context = Arc::new(StdRwLock::new(context)); + + commands.push(Task::perform(async { }, move |_| AppMsg::ReplaceView { top_level_view, has_children, render_location })); + + let state= match render_location { + UiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique(), context.clone()), + UiRenderLocation::View => GlobalState::new_plugin( + PluginViewData { + top_level_view, + plugin_id, + plugin_name: "Screenshot Gen".to_string(), + entrypoint_id, + entrypoint_name: gen_name, + action_shortcuts: Default::default(), + }, + context.clone() + ) + }; + + (context, state) + } + ScenarioFrontendEvent::ShowPreferenceRequiredView { entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { + let error_view = ErrorViewData::PreferenceRequired { + plugin_id: PluginId::from_string("__SCREENSHOT_GEN___"), + entrypoint_id: EntrypointId::from_string(entrypoint_id), + plugin_preferences_required, + entrypoint_preferences_required, + }; + + (Arc::new(StdRwLock::new(ClientContext::new())), GlobalState::new_error(error_view)) + } + ScenarioFrontendEvent::ShowPluginErrorView { entrypoint_id, render_location: _ } => { + let error_view = ErrorViewData::PluginError { + plugin_id: PluginId::from_string("__SCREENSHOT_GEN___"), + entrypoint_id: EntrypointId::from_string(entrypoint_id), + }; + + (Arc::new(StdRwLock::new(ClientContext::new())), GlobalState::new_error(error_view)) + } } - } + } else { + let context = Arc::new(StdRwLock::new(ClientContext::new())); + (context.clone(), GlobalState::new(text_input::Id::unique(), context.clone())) + }; - fn update(&mut self, message: Self::Message) -> Command { - match message { - AppMsg::OpenView { plugin_id, plugin_name, entrypoint_id, entrypoint_name } => { - match &mut self.global_state { - GlobalState::MainView { pending_plugin_view_data, .. } => { - *pending_plugin_view_data = Some(PluginViewData { - top_level_view: true, - plugin_id: plugin_id.clone(), - plugin_name, - entrypoint_id: entrypoint_id.clone(), - entrypoint_name, - action_shortcuts: HashMap::new(), - }); + ( + AppModel { + // logic + backend_api, + global_hotkey_manager: Arc::new(StdRwLock::new(global_hotkey_manager)), + current_hotkey: Arc::new(StdMutex::new(None)), + frontend_receiver: Arc::new(TokioRwLock::new(frontend_receiver)), + main_window_id, + focused: false, + wayland, + #[cfg(any(target_os = "macos", target_os = "windows"))] + tray_icon: sys_tray::create_tray(), - Command::batch([ - self.open_plugin_view(plugin_id, entrypoint_id), - Command::perform(async move { AppMsg::PendingPluginViewLoadingBar }, std::convert::identity) - ]) - } - GlobalState::ErrorView { .. } => { - Command::none() - } - GlobalState::PluginView { .. } => { - Command::none() - } + // ephemeral state + prompt: "".to_string(), + + // state + global_state, + client_context, + search_results: vec![], + loading_bar_state: HashMap::new(), + hud_display: None, + }, + Task::batch(commands), + ) +} + +// TODO +// fn title(state: &AppModel, window: window::Id) -> String { +// if window != window::Id::MAIN { +// "Gauntlet".to_owned() +// } else { +// "Gauntlet HUD".to_owned() +// } +// } + +fn update(state: &mut AppModel, message: AppMsg) -> Task { + match message { + AppMsg::OpenView { plugin_id, plugin_name, entrypoint_id, entrypoint_name } => { + match &mut state.global_state { + GlobalState::MainView { pending_plugin_view_data, .. } => { + *pending_plugin_view_data = Some(PluginViewData { + top_level_view: true, + plugin_id: plugin_id.clone(), + plugin_name, + entrypoint_id: entrypoint_id.clone(), + entrypoint_name, + action_shortcuts: HashMap::new(), + }); + + Task::batch([ + state.open_plugin_view(plugin_id, entrypoint_id), + Task::perform(async move { AppMsg::PendingPluginViewLoadingBar }, std::convert::identity) + ]) + } + GlobalState::ErrorView { .. } => { + Task::none() + } + GlobalState::PluginView { .. } => { + Task::none() } } - AppMsg::RunCommand { plugin_id, entrypoint_id } => { - Command::batch([ - self.hide_window(), - self.run_command(plugin_id, entrypoint_id), - ]) + } + AppMsg::RunCommand { plugin_id, entrypoint_id } => { + Task::batch([ + state.hide_window(), + state.run_command(plugin_id, entrypoint_id), + ]) + } + AppMsg::RunGeneratedCommandEvent { plugin_id, entrypoint_id, action_index } => { + Task::batch([ + state.hide_window(), + state.run_generated_command(plugin_id, entrypoint_id, action_index), + ]) + } + AppMsg::PromptChanged(mut new_prompt) => { + if cfg!(feature = "scenario_runner") { + Task::none() + } else { + match &mut state.global_state { + GlobalState::MainView { focused_search_result, sub_state, ..} => { + new_prompt.truncate(100); // search query uses regex so just to be safe truncate the prompt + + state.prompt = new_prompt.clone(); + + focused_search_result.reset(true); + + MainViewState::initial(sub_state); + } + GlobalState::ErrorView { .. } => {} + GlobalState::PluginView { .. } => {} + } + + state.search(new_prompt, true) } - AppMsg::RunGeneratedCommandEvent { plugin_id, entrypoint_id, action_index } => { - Command::batch([ - self.hide_window(), - self.run_generated_command(plugin_id, entrypoint_id, action_index), - ]) + } + AppMsg::UpdateSearchResults => { + match &state.global_state { + GlobalState::MainView { .. } => { + state.search(state.prompt.clone(), false) + } + _ => Task::none() } - AppMsg::PromptChanged(mut new_prompt) => { - if cfg!(feature = "scenario_runner") { - Command::none() - } else { - match &mut self.global_state { - GlobalState::MainView { focused_search_result, sub_state, ..} => { - new_prompt.truncate(100); // search query uses regex so just to be safe truncate the prompt + } + AppMsg::PromptSubmit => { + state.global_state.primary(&state.search_results) + }, + AppMsg::SetSearchResults(new_search_results) => { + state.search_results = new_search_results; - self.prompt = new_prompt.clone(); + Task::none() + } + AppMsg::ReplaceView { top_level_view, render_location, has_children } => { + match &mut state.global_state { + GlobalState::MainView { pending_plugin_view_data, focused_search_result, pending_plugin_view_loading_bar, .. } => { - focused_search_result.reset(true); - - MainViewState::initial(sub_state); - } - GlobalState::ErrorView { .. } => {} - GlobalState::PluginView { .. } => {} + if let LoadingBarState::Pending = pending_plugin_view_loading_bar { + *pending_plugin_view_loading_bar = LoadingBarState::Off; } - self.search(new_prompt, true) - } - } - AppMsg::UpdateSearchResults => { - match &self.global_state { - GlobalState::MainView { .. } => { - self.search(self.prompt.clone(), false) - } - _ => Command::none() - } - } - AppMsg::PromptSubmit => { - self.global_state.primary(&self.search_results) - }, - AppMsg::SetSearchResults(new_search_results) => { - self.search_results = new_search_results; - - Command::none() - } - AppMsg::ReplaceView { top_level_view, render_location, has_children } => { - match &mut self.global_state { - GlobalState::MainView { pending_plugin_view_data, focused_search_result, pending_plugin_view_loading_bar, .. } => { - - if let LoadingBarState::Pending = pending_plugin_view_loading_bar { - *pending_plugin_view_loading_bar = LoadingBarState::Off; - } - - if has_children { - if let UiRenderLocation::InlineView = render_location { - focused_search_result.unfocus(); - } - } - - let command = match pending_plugin_view_data { - None => Command::none(), - Some(pending_plugin_view_data) => { - let pending_plugin_view_data = pending_plugin_view_data.clone(); - GlobalState::plugin( - &mut self.global_state, - PluginViewData { - top_level_view, - ..pending_plugin_view_data - }, - self.client_context.clone() - ) - } - }; - + if has_children { if let UiRenderLocation::InlineView = render_location { - Command::batch([ - command, - self.inline_view_shortcuts() - ]) - } else { - command + focused_search_result.unfocus(); } } - GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { plugin_view_data, ..} => { - plugin_view_data.top_level_view = top_level_view; - Command::none() + let command = match pending_plugin_view_data { + None => Task::none(), + Some(pending_plugin_view_data) => { + let pending_plugin_view_data = pending_plugin_view_data.clone(); + GlobalState::plugin( + &mut state.global_state, + PluginViewData { + top_level_view, + ..pending_plugin_view_data + }, + state.client_context.clone() + ) + } + }; + + if let UiRenderLocation::InlineView = render_location { + Task::batch([ + command, + state.inline_view_shortcuts() + ]) + } else { + command } } - } - AppMsg::IcedEvent(Event::Keyboard(event)) => { - match event { - keyboard::Event::KeyPressed { key, modifiers, physical_key, text, .. } => { - tracing::debug!("Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", key, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); - match key { - Key::Named(Named::ArrowUp) => self.global_state.up(&self.search_results), - Key::Named(Named::ArrowDown) => self.global_state.down(&self.search_results), - Key::Named(Named::ArrowLeft) => self.global_state.left(&self.search_results), - Key::Named(Named::ArrowRight) => self.global_state.right(&self.search_results), - Key::Named(Named::Escape) => self.global_state.back(), - Key::Named(Named::Tab) if !modifiers.shift() => self.global_state.next(), - Key::Named(Named::Tab) if modifiers.shift() => self.global_state.previous(), - Key::Named(Named::Enter) => { - if modifiers.logo() || modifiers.alt() || modifiers.control() { - Command::none() // to avoid not wanted "enter" presses - } else { - if modifiers.shift() { - // for main view, also fired in cases where main text field is not focused - self.global_state.secondary(&self.search_results) - } else { - self.global_state.primary(&self.search_results) - } - } - }, - Key::Named(Named::Backspace) => { - match &mut self.global_state { - GlobalState::MainView { sub_state, search_field_id, .. } => { - match sub_state { - MainViewState::None => Self::backspace_prompt(&mut self.prompt, search_field_id.clone()), - MainViewState::SearchResultActionPanel { .. } => Command::none(), - MainViewState::InlineViewActionPanel { .. } => Command::none() - } - } - GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { sub_state, .. } => { - match sub_state { - PluginViewState::None => { - let mut client_context = self.client_context.write().expect("lock is poisoned"); + GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PluginView { plugin_view_data, ..} => { + plugin_view_data.top_level_view = top_level_view; - client_context.backspace_text() - } - PluginViewState::ActionPanel { .. } => Command::none() - } + Task::none() + } + } + } + AppMsg::IcedEvent(Event::Keyboard(event)) => { + match event { + keyboard::Event::KeyPressed { key, modifiers, physical_key, text, .. } => { + let Physical::Code(physical_key) = physical_key else { + return Task::none() + }; + + tracing::debug!("Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", key, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); + match key { + Key::Named(Named::ArrowUp) => state.global_state.up(&state.search_results), + Key::Named(Named::ArrowDown) => state.global_state.down(&state.search_results), + Key::Named(Named::ArrowLeft) => state.global_state.left(&state.search_results), + Key::Named(Named::ArrowRight) => state.global_state.right(&state.search_results), + Key::Named(Named::Escape) => state.global_state.back(), + Key::Named(Named::Tab) if !modifiers.shift() => state.global_state.next(), + Key::Named(Named::Tab) if modifiers.shift() => state.global_state.previous(), + Key::Named(Named::Enter) => { + if modifiers.logo() || modifiers.alt() || modifiers.control() { + Task::none() // to avoid not wanted "enter" presses + } else { + if modifiers.shift() { + // for main view, also fired in cases where main text field is not focused + state.global_state.secondary(&state.search_results) + } else { + state.global_state.primary(&state.search_results) + } + } + }, + Key::Named(Named::Backspace) => { + match &mut state.global_state { + GlobalState::MainView { sub_state, search_field_id, .. } => { + match sub_state { + MainViewState::None => AppModel::backspace_prompt(&mut state.prompt, search_field_id.clone()), + MainViewState::SearchResultActionPanel { .. } => Task::none(), + MainViewState::InlineViewActionPanel { .. } => Task::none() } } - }, - _ => { - match &mut self.global_state { - GlobalState::MainView { sub_state, search_field_id, focused_search_result, .. } => { - match sub_state { - MainViewState::None => { - match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) - } - Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - if modifier_shift || modifier_control || modifier_alt || modifier_meta { - if let Some(search_item) = focused_search_result.get(&self.search_results) { - if search_item.entrypoint_actions.len() > 0 { - self.handle_main_view_keyboard_event( - search_item.plugin_id.clone(), - search_item.entrypoint_id.clone(), - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta - ) - } else { - Command::none() - } - } else { - self.handle_inline_plugin_view_keyboard_event( + GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PluginView { sub_state, .. } => { + match sub_state { + PluginViewState::None => { + let mut client_context = state.client_context.write().expect("lock is poisoned"); + + client_context.backspace_text() + } + PluginViewState::ActionPanel { .. } => Task::none() + } + } + } + }, + _ => { + match &mut state.global_state { + GlobalState::MainView { sub_state, search_field_id, focused_search_result, .. } => { + match sub_state { + MainViewState::None => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { + Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) + } + Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { + if modifier_shift || modifier_control || modifier_alt || modifier_meta { + if let Some(search_item) = focused_search_result.get(&state.search_results) { + if search_item.entrypoint_actions.len() > 0 { + state.handle_main_view_keyboard_event( + search_item.plugin_id.clone(), + search_item.entrypoint_id.clone(), physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta ) - } - } else { - Self::append_prompt(&mut self.prompt, text, search_field_id.clone(), modifiers) - } - } - _ => Self::append_prompt(&mut self.prompt, text, search_field_id.clone(), modifiers) - } - } - MainViewState::SearchResultActionPanel { .. } => { - match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) - } - Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - if modifier_shift || modifier_control || modifier_alt || modifier_meta { - if let Some(search_item) = focused_search_result.get(&self.search_results) { - if search_item.entrypoint_actions.len() > 0 { - self.handle_main_view_keyboard_event( - search_item.plugin_id.clone(), - search_item.entrypoint_id.clone(), - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta - ) - } else { - Command::none() - } } else { - Command::none() + Task::none() } } else { - Command::none() - } - } - _ => Command::none() - } - } - MainViewState::InlineViewActionPanel { .. } => { - match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) - } - Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - if modifier_shift || modifier_control || modifier_alt || modifier_meta { - self.handle_inline_plugin_view_keyboard_event( + state.handle_inline_plugin_view_keyboard_event( physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta ) - } else { - Command::none() } + } else { + AppModel::append_prompt(&mut state.prompt, text, search_field_id.clone(), modifiers) } - _ => Command::none() } + _ => AppModel::append_prompt(&mut state.prompt, text, search_field_id.clone(), modifiers) + } + } + MainViewState::SearchResultActionPanel { .. } => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { + Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) + } + Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { + if modifier_shift || modifier_control || modifier_alt || modifier_meta { + if let Some(search_item) = focused_search_result.get(&state.search_results) { + if search_item.entrypoint_actions.len() > 0 { + state.handle_main_view_keyboard_event( + search_item.plugin_id.clone(), + search_item.entrypoint_id.clone(), + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + ) + } else { + Task::none() + } + } else { + Task::none() + } + } else { + Task::none() + } + } + _ => Task::none() + } + } + MainViewState::InlineViewActionPanel { .. } => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { + Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) + } + Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { + if modifier_shift || modifier_control || modifier_alt || modifier_meta { + state.handle_inline_plugin_view_keyboard_event( + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + ) + } else { + Task::none() + } + } + _ => Task::none() } } } - GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { sub_state, .. } => { - match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) - } - Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - if modifier_shift || modifier_control || modifier_alt || modifier_meta { - self.handle_plugin_view_keyboard_event(physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) - } else { - match sub_state { - PluginViewState::None => { - match text { - None => Command::none(), - Some(text) => { - let mut client_context = self.client_context.write().expect("lock is poisoned"); + } + GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PluginView { sub_state, .. } => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { + Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) + } + Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { + if modifier_shift || modifier_control || modifier_alt || modifier_meta { + state.handle_plugin_view_keyboard_event(physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + } else { + match sub_state { + PluginViewState::None => { + match text { + None => Task::none(), + Some(text) => { + let mut client_context = state.client_context.write().expect("lock is poisoned"); - client_context.append_text(text.as_str()) - } + client_context.append_text(text.as_str()) } } - PluginViewState::ActionPanel { .. } => Command::none() } + PluginViewState::ActionPanel { .. } => Task::none() } } - _ => Command::none() } + _ => Task::none() } } } } } - _ => Command::none() + } + _ => Task::none() + } + } + AppMsg::IcedEvent(Event::Window(window::Event::Focused)) => { + state.on_focused() + } + AppMsg::IcedEvent(Event::Window(window::Event::Unfocused)) => { + state.on_unfocused() + } + // #[cfg(target_os = "linux")] + // AppMsg::IcedEvent( + // Event::PlatformSpecific( + // iced::wayland::core::event::PlatformSpecific::Wayland( + // iced::wayland::core::event::wayland::Event::Layer( + // iced::wayland::core::event::wayland::LayerEvent::Unfocused, + // _, + // _ + // ) + // ) + // ) + // ) => { + // // wayland layer shell doesn't have the same unfocused problem as the other platforms + // state.hide_window() + // } + AppMsg::IcedEvent(_) => Task::none(), + AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::Noop, .. } => Task::none(), + AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => state.global_state.back(), + AppMsg::WidgetEvent { widget_event, plugin_id, render_location } => { + state.handle_plugin_event(widget_event, plugin_id, render_location) + } + AppMsg::Noop => Task::none(), + AppMsg::FontLoaded(result) => { + result.expect("unable to load font"); + Task::none() + } + AppMsg::ShowWindow => state.show_window(), + AppMsg::HideWindow => state.hide_window(), + AppMsg::ShowPreferenceRequiredView { + plugin_id, + entrypoint_id, + plugin_preferences_required, + entrypoint_preferences_required + } => { + GlobalState::error( + &mut state.global_state, + ErrorViewData::PreferenceRequired { + plugin_id, + entrypoint_id, + plugin_preferences_required, + entrypoint_preferences_required, + }, + ) + } + AppMsg::ShowPluginErrorView { plugin_id, entrypoint_id, .. } => { + GlobalState::error( + &mut state.global_state, + ErrorViewData::PluginError { + plugin_id, + entrypoint_id, + }, + ) + } + AppMsg::ShowBackendError(err) => { + GlobalState::error( + &mut state.global_state, + match err { + BackendForFrontendApiError::TimeoutError => ErrorViewData::BackendTimeout, + } + ) + } + AppMsg::OpenSettingsPreferences { plugin_id, entrypoint_id, } => { + state.open_settings_window_preferences(plugin_id, entrypoint_id) + } + AppMsg::OnOpenView { action_shortcuts } => { + match &mut state.global_state { + GlobalState::MainView { pending_plugin_view_data, .. } => { + match pending_plugin_view_data { + None => {} + Some(pending_plugin_view_data) => { + pending_plugin_view_data.action_shortcuts = action_shortcuts; + } + }; + } + GlobalState::ErrorView { .. } => { }, + GlobalState::PluginView { plugin_view_data, ..} => { + plugin_view_data.action_shortcuts = action_shortcuts; } } - AppMsg::IcedEvent(Event::Window(_, window::Event::Focused)) => { - self.on_focused() + + Task::none() + } + AppMsg::RunSearchItemAction(search_result, action_index) => { + match search_result.entrypoint_type { + SearchResultEntrypointType::Command => { + match action_index { + None => { + Task::done(AppMsg::RunCommand { + entrypoint_id: search_result.entrypoint_id.clone(), + plugin_id: search_result.plugin_id.clone() + }) + } + Some(_) => Task::none() + } + }, + SearchResultEntrypointType::View => { + match action_index { + None => { + Task::done(AppMsg::OpenView { + plugin_id: search_result.plugin_id.clone(), + plugin_name: search_result.plugin_name.clone(), + entrypoint_id: search_result.entrypoint_id.clone(), + entrypoint_name: search_result.entrypoint_name.clone(), + }) + } + Some(_) => Task::none() + } + }, + SearchResultEntrypointType::GeneratedCommand => { + Task::done(AppMsg::RunGeneratedCommandEvent { + entrypoint_id: search_result.entrypoint_id.clone(), + plugin_id: search_result.plugin_id.clone(), + action_index, + }) + }, } - AppMsg::IcedEvent(Event::Window(_, window::Event::Unfocused)) => { - self.on_unfocused() - } - #[cfg(target_os = "linux")] - AppMsg::IcedEvent( - Event::PlatformSpecific( - iced::wayland::core::event::PlatformSpecific::Wayland( - iced::wayland::core::event::wayland::Event::Layer( - iced::wayland::core::event::wayland::LayerEvent::Unfocused, - _, - _ + } + AppMsg::Screenshot { save_path } => { + println!("Creating screenshot at: {}", save_path); + + fs::create_dir_all(Path::new(&save_path).parent().expect("no parent?")) + .expect("unable to create scenario out directories"); + + window::screenshot(state.main_window_id) + .map(move |screenshot| AppMsg::ScreenshotDone { + save_path: save_path.clone(), + screenshot, + }) + } + AppMsg::ScreenshotDone { save_path, screenshot } => { + println!("Saving screenshot at: {}", save_path); + + Task::perform( + async move { + tokio::task::spawn_blocking(move || { + let save_dir = Path::new(&save_path); + + let save_parent_dir = save_dir + .parent() + .expect("save_path has no parent"); + + fs::create_dir_all(save_parent_dir) + .expect("unable to create save_parent_dir"); + + image::save_buffer_with_format( + &save_path, + &screenshot.bytes, + screenshot.size.width, + screenshot.size.height, + image::ColorType::Rgba8, + image::ImageFormat::Png ) - ) - ) - ) => { - // wayland layer shell doesn't have the same unfocused problem as the other platforms - self.hide_window() - } - AppMsg::IcedEvent(_) => Command::none(), - AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::Noop, .. } => Command::none(), - AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => self.global_state.back(), - AppMsg::WidgetEvent { widget_event, plugin_id, render_location } => { - self.handle_plugin_event(widget_event, plugin_id, render_location) - } - AppMsg::Noop => Command::none(), - AppMsg::FontLoaded(result) => { - result.expect("unable to load font"); - Command::none() - } - AppMsg::ShowWindow => self.show_window(), - AppMsg::HideWindow => self.hide_window(), - AppMsg::ShowPreferenceRequiredView { - plugin_id, - entrypoint_id, - plugin_preferences_required, - entrypoint_preferences_required - } => { - GlobalState::error( - &mut self.global_state, - ErrorViewData::PreferenceRequired { - plugin_id, - entrypoint_id, - plugin_preferences_required, - entrypoint_preferences_required, - }, - ) - } - AppMsg::ShowPluginErrorView { plugin_id, entrypoint_id, .. } => { - GlobalState::error( - &mut self.global_state, - ErrorViewData::PluginError { - plugin_id, - entrypoint_id, - }, - ) - } - AppMsg::ShowBackendError(err) => { - GlobalState::error( - &mut self.global_state, - match err { - BackendForFrontendApiError::TimeoutError => ErrorViewData::BackendTimeout, - } - ) - } - AppMsg::OpenSettingsPreferences { plugin_id, entrypoint_id, } => { - self.open_settings_window_preferences(plugin_id, entrypoint_id) - } - AppMsg::OnOpenView { action_shortcuts } => { - match &mut self.global_state { - GlobalState::MainView { pending_plugin_view_data, .. } => { - match pending_plugin_view_data { - None => {} - Some(pending_plugin_view_data) => { - pending_plugin_view_data.action_shortcuts = action_shortcuts; - } - }; - } - GlobalState::ErrorView { .. } => { }, - GlobalState::PluginView { plugin_view_data, ..} => { - plugin_view_data.action_shortcuts = action_shortcuts; - } - } + }).await + .expect("Unable to save screenshot") + }, + |_| AppMsg::Close, + ) + } + AppMsg::Close => { + // TODO + // #[cfg(target_os = "linux")] + // if state.wayland { + // iced::wayland::commands::window::close_window(window::Id::MAIN) + // } else { + // window::close(window::Id::MAIN) + // } + // + // #[cfg(not(target_os = "linux"))] + // window::close(window::Id::MAIN) - Command::none() - } - AppMsg::RunSearchItemAction(search_result, action_index) => { - match search_result.entrypoint_type { - SearchResultEntrypointType::Command => { - match action_index { - None => { - let msg = AppMsg::RunCommand { - entrypoint_id: search_result.entrypoint_id.clone(), - plugin_id: search_result.plugin_id.clone() - }; - Command::perform(async {}, |_| msg) - } - Some(_) => Command::none() - } - }, - SearchResultEntrypointType::View => { - match action_index { - None => { - let msg = AppMsg::OpenView { - plugin_id: search_result.plugin_id.clone(), - plugin_name: search_result.plugin_name.clone(), - entrypoint_id: search_result.entrypoint_id.clone(), - entrypoint_name: search_result.entrypoint_name.clone(), - }; - Command::perform(async {}, |_| msg) - } - Some(_) => Command::none() - } - }, - SearchResultEntrypointType::GeneratedCommand => { - let msg = AppMsg::RunGeneratedCommandEvent { - entrypoint_id: search_result.entrypoint_id.clone(), - plugin_id: search_result.plugin_id.clone(), - action_index, - }; - - Command::perform(async {}, |_| msg) - }, - } - } - AppMsg::Screenshot { save_path } => { - println!("Creating screenshot at: {}", save_path); - - fs::create_dir_all(Path::new(&save_path).parent().expect("no parent?")) - .expect("unable to create scenario out directories"); - - window::screenshot( - window::Id::MAIN, - |screenshot| AppMsg::ScreenshotDone { - save_path, - screenshot, - } - ) - } - AppMsg::ScreenshotDone { save_path, screenshot } => { - println!("Saving screenshot at: {}", save_path); - - Command::perform( - async move { - tokio::task::spawn_blocking(move || { - let save_dir = Path::new(&save_path); - - let save_parent_dir = save_dir - .parent() - .expect("save_path has no parent"); - - fs::create_dir_all(save_parent_dir) - .expect("unable to create save_parent_dir"); - - image::save_buffer_with_format( - &save_path, - &screenshot.bytes, - screenshot.size.width, - screenshot.size.height, - image::ColorType::Rgba8, - image::ImageFormat::Png - ) - }).await - .expect("Unable to save screenshot") - }, - |_| AppMsg::Close, - ) - } - AppMsg::Close => { - #[cfg(target_os = "linux")] - if self.wayland { - iced::wayland::commands::window::close_window(window::Id::MAIN) - } else { - window::close(window::Id::MAIN) - } - - #[cfg(not(target_os = "linux"))] - window::close(window::Id::MAIN) - } - AppMsg::ToggleActionPanel { keyboard } => { - match &mut self.global_state { - GlobalState::MainView { sub_state, focused_search_result, .. } => { - match sub_state { - MainViewState::None => { - if let Some(search_item) = focused_search_result.get(&self.search_results) { - if search_item.entrypoint_actions.len() > 0 { - MainViewState::search_result_action_panel(sub_state, keyboard); - } - } else { - let client_context = self.client_context.read().expect("lock is poisoned"); - if let Some(_) = client_context.get_first_inline_view_container() { - MainViewState::inline_result_action_panel(sub_state, keyboard); - } + window::close(state.main_window_id) + } + AppMsg::ToggleActionPanel { keyboard } => { + match &mut state.global_state { + GlobalState::MainView { sub_state, focused_search_result, .. } => { + match sub_state { + MainViewState::None => { + if let Some(search_item) = focused_search_result.get(&state.search_results) { + if search_item.entrypoint_actions.len() > 0 { + MainViewState::search_result_action_panel(sub_state, keyboard); + } + } else { + let client_context = state.client_context.read().expect("lock is poisoned"); + if let Some(_) = client_context.get_first_inline_view_container() { + MainViewState::inline_result_action_panel(sub_state, keyboard); } } - MainViewState::SearchResultActionPanel { .. } => { - MainViewState::initial(sub_state); - } - MainViewState::InlineViewActionPanel { .. } => { - MainViewState::initial(sub_state); - } } - } - GlobalState::ErrorView { .. } => { }, - GlobalState::PluginView { sub_state, .. } => { - let client_context = self.client_context.read().expect("lock is poisoned"); - - client_context.toggle_action_panel(); - - match sub_state { - PluginViewState::None => { - PluginViewState::action_panel(sub_state, keyboard) - } - PluginViewState::ActionPanel { .. } => { - PluginViewState::initial(sub_state) - } + MainViewState::SearchResultActionPanel { .. } => { + MainViewState::initial(sub_state); + } + MainViewState::InlineViewActionPanel { .. } => { + MainViewState::initial(sub_state); } } } + GlobalState::ErrorView { .. } => { }, + GlobalState::PluginView { sub_state, .. } => { + let client_context = state.client_context.read().expect("lock is poisoned"); - Command::none() - } - AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus => { - Command::perform(async {}, move |_| AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index: 0 }) - } - AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result } => { - Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_result, None)) - } - AppMsg::OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id } => { - let run_action_command = if widget_id == 0 { - Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_result, None)) - } else { - Command::perform(async {}, move |_| AppMsg::RunSearchItemAction(search_result, Some(widget_id - 1))) - }; + client_context.toggle_action_panel(); - Command::batch([ - run_action_command, - Command::perform(async {}, |_| AppMsg::ResetMainViewState) - ]) + match sub_state { + PluginViewState::None => { + PluginViewState::action_panel(sub_state, keyboard) + } + PluginViewState::ActionPanel { .. } => { + PluginViewState::initial(sub_state) + } + } + } } - AppMsg::OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id } => { - let client_context = self.client_context.read().expect("lock is poisoned"); - match client_context.get_first_inline_view_container() { - Some(container) => { - let plugin_id = container.get_plugin_id(); + Task::none() + } + AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus => { + Task::done(AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index: 0 }) + } + AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result } => { + Task::done(AppMsg::RunSearchItemAction(search_result, None)) + } + AppMsg::OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id } => { + let run_action_command = if widget_id == 0 { + Task::done(AppMsg::RunSearchItemAction(search_result, None)) + } else { + Task::done(AppMsg::RunSearchItemAction(search_result, Some(widget_id - 1))) + }; + + Task::batch([ + run_action_command, + Task::done(AppMsg::ResetMainViewState) + ]) + } + AppMsg::OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id } => { + let client_context = state.client_context.read().expect("lock is poisoned"); + + match client_context.get_first_inline_view_container() { + Some(container) => { + let plugin_id = container.get_plugin_id(); + + let widget_event = ComponentWidgetEvent::RunAction { + widget_id, + }; + let render_location = UiRenderLocation::InlineView; + + Task::batch([ + Task::done(AppMsg::ToggleActionPanel { keyboard: true }), + Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + ]) + } + None => Task::none() + } + } + AppMsg::OnPrimaryActionPluginViewNoPanelKeyboardWithFocus { widget_id } => { + Task::perform(async {}, move |_| AppMsg::OnAnyActionPluginViewAnyPanel { widget_id }) + } + AppMsg::OnPrimaryActionPluginViewAnyPanelKeyboardWithFocus { widget_id } => { + let client_context = state.client_context.read().expect("lock is poisoned"); + + let plugin_id = client_context.get_view_plugin_id(); + + let widget_event = ComponentWidgetEvent::RunAction { + widget_id, + }; + let render_location = UiRenderLocation::View; + + Task::batch([ + Task::done(AppMsg::ToggleActionPanel { keyboard: true }), + Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + ]) + } + AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id: _ } => { + // widget_id here is always 0 + match &state.global_state { + GlobalState::MainView { focused_search_result, .. } => { + if let Some(search_result) = focused_search_result.get(&state.search_results) { + let search_result = search_result.clone(); + Task::done(AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result }) + } else { + Task::done(AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus) + } + } + GlobalState::PluginView { .. } => Task::none(), + GlobalState::ErrorView { .. } => Task::none() + } + } + AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus => { + Task::done(AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index: 1 }) + } + AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result } => { + Task::done(AppMsg::RunSearchItemAction(search_result, Some(0))) + } + AppMsg::OnSecondaryActionPluginViewNoPanelKeyboardWithFocus { widget_id } => { + Task::done(AppMsg::OnAnyActionPluginViewAnyPanel { widget_id }) + } + AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index } => { + let client_context = state.client_context.read().expect("lock is poisoned"); + + if let Some(container) = client_context.get_first_inline_view_container() { + let plugin_id = container.get_plugin_id(); + let action_ids = container.get_action_ids(); + + match action_ids.get(index) { + Some(widget_id) => { + let widget_id = *widget_id; let widget_event = ComponentWidgetEvent::RunAction { widget_id, }; let render_location = UiRenderLocation::InlineView; - Command::batch([ - Command::perform(async {}, move |_| AppMsg::ToggleActionPanel { keyboard: true }), - Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) - ]) + Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) } - None => Command::none() + None => Task::none() } + } else { + Task::none() } - AppMsg::OnPrimaryActionPluginViewNoPanelKeyboardWithFocus { widget_id } => { - Command::perform(async {}, move |_| AppMsg::OnAnyActionPluginViewAnyPanel { widget_id }) - } - AppMsg::OnPrimaryActionPluginViewAnyPanelKeyboardWithFocus { widget_id } => { - let client_context = self.client_context.read().expect("lock is poisoned"); - - let plugin_id = client_context.get_view_plugin_id(); - - let widget_event = ComponentWidgetEvent::RunAction { - widget_id, - }; - let render_location = UiRenderLocation::View; - - Command::batch([ - Command::perform(async {}, move |_| AppMsg::ToggleActionPanel { keyboard: true }), - Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) - ]) - } - AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id: _ } => { - // widget_id here is always 0 - match &self.global_state { - GlobalState::MainView { focused_search_result, .. } => { - if let Some(search_result) = focused_search_result.get(&self.search_results) { - let search_result = search_result.clone(); - Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result }) - } else { - Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus) - } - } - GlobalState::PluginView { .. } => Command::none(), - GlobalState::ErrorView { .. } => Command::none() - } - } - AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus => { - Command::perform(async {}, move |_| AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index: 1 }) - } - AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result } => { - Command::perform(async {}, |_| AppMsg::RunSearchItemAction(search_result, Some(0))) - } - AppMsg::OnSecondaryActionPluginViewNoPanelKeyboardWithFocus { widget_id } => { - Command::perform(async {}, move |_| AppMsg::OnAnyActionPluginViewAnyPanel { widget_id }) - } - AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index } => { - let client_context = self.client_context.read().expect("lock is poisoned"); - - if let Some(container) = client_context.get_first_inline_view_container() { - let plugin_id = container.get_plugin_id(); - let action_ids = container.get_action_ids(); - - match action_ids.get(index) { - Some(widget_id) => { - let widget_id = *widget_id; - - let widget_event = ComponentWidgetEvent::RunAction { - widget_id, - }; - let render_location = UiRenderLocation::InlineView; - - Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) - } - None => Command::none() - } - } else { - Command::none() - } - } - AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id } => { - match &mut self.global_state { - GlobalState::MainView { focused_search_result, sub_state, .. } => { - match sub_state { - MainViewState::None => Command::none(), - MainViewState::SearchResultActionPanel { .. } => { - if let Some(search_result) = focused_search_result.get(&self.search_results) { - let search_result = search_result.clone(); - Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id }) - } else { - Command::none() - } - } - MainViewState::InlineViewActionPanel { .. } => { - Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id }) + } + AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id } => { + match &mut state.global_state { + GlobalState::MainView { focused_search_result, sub_state, .. } => { + match sub_state { + MainViewState::None => Task::none(), + MainViewState::SearchResultActionPanel { .. } => { + if let Some(search_result) = focused_search_result.get(&state.search_results) { + let search_result = search_result.clone(); + Task::done(AppMsg::OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id }) + } else { + Task::none() } } + MainViewState::InlineViewActionPanel { .. } => { + Task::done(AppMsg::OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id }) + } } - GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { .. } => Command::none() } + GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PluginView { .. } => Task::none() } - AppMsg::OnAnyActionPluginViewAnyPanel { widget_id } => { - let client_context = self.client_context.read().expect("lock is poisoned"); + } + AppMsg::OnAnyActionPluginViewAnyPanel { widget_id } => { + let client_context = state.client_context.read().expect("lock is poisoned"); - let plugin_id = client_context.get_view_plugin_id(); + let plugin_id = client_context.get_view_plugin_id(); - let widget_event = ComponentWidgetEvent::RunAction { - widget_id, - }; + let widget_event = ComponentWidgetEvent::RunAction { + widget_id, + }; - let render_location = UiRenderLocation::View; + let render_location = UiRenderLocation::View; - Command::perform(async {}, move |_| AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) - } - AppMsg::OpenPluginView(plugin_id, entrypoint_id) => { - self.open_plugin_view(plugin_id, entrypoint_id) - } - AppMsg::ClosePluginView(plugin_id) => { - self.close_plugin_view(plugin_id) - } - AppMsg::InlineViewShortcuts { shortcuts } => { - let mut client_context = self.client_context.write().expect("lock is poisoned"); + Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + } + AppMsg::OpenPluginView(plugin_id, entrypoint_id) => { + state.open_plugin_view(plugin_id, entrypoint_id) + } + AppMsg::ClosePluginView(plugin_id) => { + state.close_plugin_view(plugin_id) + } + AppMsg::InlineViewShortcuts { shortcuts } => { + let mut client_context = state.client_context.write().expect("lock is poisoned"); - client_context.set_inline_view_shortcuts(shortcuts); + client_context.set_inline_view_shortcuts(shortcuts); - Command::none() - } - AppMsg::ShowHud { display } => { - self.hud_display = Some(display); + Task::none() + } + AppMsg::ShowHud { display } => { + state.hud_display = Some(display); - show_hud_window( - #[cfg(target_os = "linux")] - self.wayland, - ) - } - AppMsg::CloseHudWindow { id } => { - self.hud_display = None; + show_hud_window( + // TODO + // #[cfg(target_os = "linux")] + // state.wayland, + ) + } + AppMsg::CloseHudWindow { id } => { + state.hud_display = None; - close_hud_window( - #[cfg(target_os = "linux")] - self.wayland, - id - ) - } - AppMsg::ResetMainViewState => { - match &mut self.global_state { - GlobalState::MainView { sub_state, .. } => { - MainViewState::initial(sub_state); + close_hud_window( + // TODO + // #[cfg(target_os = "linux")] + // state.wayland, + id + ) + } + AppMsg::ResetMainViewState => { + match &mut state.global_state { + GlobalState::MainView { sub_state, .. } => { + MainViewState::initial(sub_state); - Command::none() - } - GlobalState::ErrorView { .. } => Command::none(), - GlobalState::PluginView { .. } => Command::none(), + Task::none() } + GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PluginView { .. } => Task::none(), } - AppMsg::SetGlobalShortcut { shortcut, responder } => { - tracing::info!("Registering new global shortcut: {:?}", shortcut); + } + AppMsg::SetGlobalShortcut { shortcut, responder } => { + tracing::info!("Registering new global shortcut: {:?}", shortcut); - let run = || { - let global_hotkey_manager = self.global_hotkey_manager - .read() - .expect("lock is poisoned"); + let run = || { + let global_hotkey_manager = state.global_hotkey_manager + .read() + .expect("lock is poisoned"); - let mut hotkey_guard = self.current_hotkey - .lock() - .expect("lock is poisoned"); - - if let Some(current_hotkey) = *hotkey_guard { - global_hotkey_manager.unregister(current_hotkey)?; - } - - if let Some(shortcut) = shortcut { - let hotkey = convert_physical_shortcut_to_hotkey(shortcut); - - *hotkey_guard = Some(hotkey); - - global_hotkey_manager.register(hotkey)?; - } - - Ok(()) - }; - - // responder is not clone and send, and we need to consume it - // so we wrap it in arc mutex option - let mut responder = responder + let mut hotkey_guard = state.current_hotkey .lock() - .expect("lock is poisoned") - .take() - .expect("there should always be a responder here"); + .expect("lock is poisoned"); - match run() { - Ok(()) => { - responder.respond(UiResponseData::Nothing); - } - Err(err) => { - responder.respond(UiResponseData::Err(err)); - } + if let Some(current_hotkey) = *hotkey_guard { + global_hotkey_manager.unregister(current_hotkey)?; } - Command::none() - } - AppMsg::UpdateLoadingBar { plugin_id, entrypoint_id, show } => { - if show { - self.loading_bar_state.insert((plugin_id, entrypoint_id), ()); - } else { - self.loading_bar_state.remove(&(plugin_id, entrypoint_id)); + if let Some(shortcut) = shortcut { + let hotkey = convert_physical_shortcut_to_hotkey(shortcut); + + *hotkey_guard = Some(hotkey); + + global_hotkey_manager.register(hotkey)?; } - Command::none() - } - AppMsg::PendingPluginViewLoadingBar => { - if let GlobalState::MainView { pending_plugin_view_loading_bar, .. } = &mut self.global_state { - *pending_plugin_view_loading_bar = LoadingBarState::Pending; + Ok(()) + }; + + // responder is not clone and send, and we need to consume it + // so we wrap it in arc mutex option + let mut responder = responder + .lock() + .expect("lock is poisoned") + .take() + .expect("there should always be a responder here"); + + match run() { + Ok(()) => { + responder.respond(UiResponseData::Nothing); } - - Command::perform(async move { - tokio::time::sleep(std::time::Duration::from_millis(300)).await; - - AppMsg::ShowPluginViewLoadingBar - }, std::convert::identity) - } - AppMsg::ShowPluginViewLoadingBar => { - if let GlobalState::MainView { pending_plugin_view_loading_bar, .. } = &mut self.global_state { - if let LoadingBarState::Pending = pending_plugin_view_loading_bar { - *pending_plugin_view_loading_bar = LoadingBarState::On; - } + Err(err) => { + responder.respond(UiResponseData::Err(err)); } - - Command::none() } - AppMsg::FocusPluginViewSearchBar { widget_id } => { - let mut client_context = self.client_context.write().expect("lock is poisoned"); - client_context.focus_search_bar(widget_id) + Task::none() + } + AppMsg::UpdateLoadingBar { plugin_id, entrypoint_id, show } => { + if show { + state.loading_bar_state.insert((plugin_id, entrypoint_id), ()); + } else { + state.loading_bar_state.remove(&(plugin_id, entrypoint_id)); + } + + Task::none() + } + AppMsg::PendingPluginViewLoadingBar => { + if let GlobalState::MainView { pending_plugin_view_loading_bar, .. } = &mut state.global_state { + *pending_plugin_view_loading_bar = LoadingBarState::Pending; + } + + Task::perform(async move { + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + + AppMsg::ShowPluginViewLoadingBar + }, std::convert::identity) + } + AppMsg::ShowPluginViewLoadingBar => { + if let GlobalState::MainView { pending_plugin_view_loading_bar, .. } = &mut state.global_state { + if let LoadingBarState::Pending = pending_plugin_view_loading_bar { + *pending_plugin_view_loading_bar = LoadingBarState::On; + } + } + + Task::none() + } + AppMsg::FocusPluginViewSearchBar { widget_id } => { + let mut client_context = state.client_context.write().expect("lock is poisoned"); + + client_context.focus_search_bar(widget_id) + } + } +} + +fn view(state: &AppModel, window: window::Id) -> Element<'_, AppMsg> { + if window != state.main_window_id { + return match &state.hud_display { + Some(hud_display) => { + let hud: Element<_> = text(hud_display.to_string()) + .shaping(Shaping::Advanced) + .into(); + + let hud = container(hud) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .height(Length::Fill) + .themed(ContainerStyle::HudInner); + + let hud = container(hud) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .height(Length::Fill) + .themed(ContainerStyle::Hud); + + let hud = container(hud) + .height(Length::Fill) + .width(Length::Fill) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .class(ContainerStyleInner::Transparent) + .into(); + + hud + } + None => { + horizontal_space() + .into() } } } - fn view(&self, window: window::Id) -> Element<'_, Self::Message> { - if window != window::Id::MAIN { - return match &self.hud_display { - Some(hud_display) => { - let hud: Element<_> = text(&hud_display) + + match &state.global_state { + GlobalState::ErrorView { error_view } => { + match error_view { + ErrorViewData::PreferenceRequired { plugin_id, entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { + + let (description_text, msg) = match (plugin_preferences_required, entrypoint_preferences_required) { + (true, true) => { + // TODO do not show "entrypoint" name to user + let description_text = "Before using, plugin and entrypoint preferences need to be specified"; + // note: + // we open plugin view and not entrypoint even though both need to be specified + let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: None }; + (description_text, msg) + } + (false, true) => { + // TODO do not show "entrypoint" name to user + let description_text = "Before using, entrypoint preferences need to be specified"; + let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: Some(entrypoint_id.clone()) }; + (description_text, msg) + } + (true, false) => { + let description_text = "Before using, plugin preferences need to be specified"; + let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: None }; + (description_text, msg) + } + (false, false) => unreachable!() + }; + + let description: Element<_> = text(description_text) .shaping(Shaping::Advanced) .into(); - let hud = container(hud) - .center_x() - .center_y() - .height(Length::Fill) - .themed(ContainerStyle::HudInner); - - let hud = container(hud) - .center_x() - .center_y() - .height(Length::Fill) - .themed(ContainerStyle::Hud); - - let hud = container(hud) - .height(Length::Fill) + let description = container(description) .width(Length::Fill) - .center_x() - .center_y() - .style(ContainerStyleInner::Transparent) + .align_x(Horizontal::Center) + .themed(ContainerStyle::PreferenceRequiredViewDescription); + + let button_label: Element<_> = text("Open Settings") .into(); - hud + let button: Element<_> = button(button_label) + .on_press(msg) + .into(); + + let button = container(button) + .width(Length::Fill) + .align_x(Horizontal::Center) + .into(); + + let content: Element<_> = column([ + description, + button + ]).into(); + + let content: Element<_> = container(content) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .width(Length::Fill) + .height(Length::Fill) + .themed(ContainerStyle::Main); + + content + } + ErrorViewData::PluginError { .. } => { + let description: Element<_> = text("Error occurred in plugin when trying to show the view") + .into(); + + let description = container(description) + .width(Length::Fill) + .align_x(Horizontal::Center) + .themed(ContainerStyle::PluginErrorViewTitle); + + let sub_description: Element<_> = text("Please report this to plugin author") + .into(); + + let sub_description = container(sub_description) + .width(Length::Fill) + .align_x(Horizontal::Center) + .themed(ContainerStyle::PluginErrorViewDescription); + + let button_label: Element<_> = text("Close") + .into(); + + let button: Element<_> = button(button_label) + .on_press(AppMsg::HideWindow) + .into(); + + let button = container(button) + .width(Length::Fill) + .align_x(Horizontal::Center) + .into(); + + let content: Element<_> = column([ + description, + sub_description, + button + ]).into(); + + let content: Element<_> = container(content) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .width(Length::Fill) + .height(Length::Fill) + .themed(ContainerStyle::Main); + + content + } + ErrorViewData::UnknownError { display } => { + let description: Element<_> = text("Unknown error occurred") + .into(); + + let description = container(description) + .width(Length::Fill) + .align_x(Horizontal::Center) + .themed(ContainerStyle::PluginErrorViewTitle); + + let sub_description: Element<_> = text("Please report") // TODO link + .into(); + + let sub_description = container(sub_description) + .width(Length::Fill) + .align_x(Horizontal::Center) + .themed(ContainerStyle::PluginErrorViewDescription); + + let error_description: Element<_> = text(display) + .shaping(Shaping::Advanced) + .into(); + + let error_description = container(error_description) + .width(Length::Fill) + .themed(ContainerStyle::PluginErrorViewDescription); + + let error_description = scrollable(error_description) + .width(Length::Fill) + .into(); + + let button_label: Element<_> = text("Close") + .into(); + + let button: Element<_> = button(button_label) + .on_press(AppMsg::HideWindow) + .into(); + + let button = container(button) + .width(Length::Fill) + .align_x(Horizontal::Center) + .into(); + + let content: Element<_> = column([ + description, + sub_description, + error_description, + button + ]).into(); + + let content: Element<_> = container(content) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .width(Length::Fill) + .height(Length::Fill) + .themed(ContainerStyle::Main); + + content + } + ErrorViewData::BackendTimeout => { + let description: Element<_> = text("Error occurred") + .into(); + + let description = container(description) + .width(Length::Fill) + .align_x(Horizontal::Center) + .themed(ContainerStyle::PluginErrorViewTitle); + + let sub_description: Element<_> = text("Backend was unable to process message in a timely manner") + .into(); + + let sub_description = container(sub_description) + .width(Length::Fill) + .align_x(Horizontal::Center) + .themed(ContainerStyle::PluginErrorViewDescription); + + let button_label: Element<_> = text("Close") + .into(); + + let button: Element<_> = button(button_label) + .on_press(AppMsg::HideWindow) + .into(); + + let button = container(button) + .width(Length::Fill) + .align_x(Horizontal::Center) + .into(); + + let content: Element<_> = column([ + description, + sub_description, + button + ]).into(); + + let content: Element<_> = container(content) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .width(Length::Fill) + .height(Length::Fill) + .themed(ContainerStyle::Main); + + content + } + } + } + GlobalState::MainView { focused_search_result, sub_state, search_field_id, pending_plugin_view_loading_bar, .. } => { + let input: Element<_> = text_input("Search...", &state.prompt) + .on_input(AppMsg::PromptChanged) + .on_submit(AppMsg::PromptSubmit) + .ignore_with_modifiers(true) + .id(search_field_id.clone()) + .width(Length::Fill) + .themed(TextInputStyle::MainSearch); + + let search_list = search_list( + &state.search_results, + &focused_search_result, + |search_result| AppMsg::RunSearchItemAction(search_result, None), + ); + + let search_list = container(search_list) + .width(Length::Fill) + .themed(ContainerStyle::MainListInner); + + let list: Element<_> = scrollable(search_list) + .id(focused_search_result.scrollable_id.clone()) + .width(Length::Fill) + .into(); + + let list = container(list) + .width(Length::Fill) + .height(Length::Fill) + .themed(ContainerStyle::MainList); + + let input = container(input) + .width(Length::Fill) + .themed(ContainerStyle::MainSearchBar); + + let separator = if matches!(pending_plugin_view_loading_bar, LoadingBarState::On) || !state.loading_bar_state.is_empty() { + LoadingBar::new() + .into() + } else { + horizontal_rule(1) + .into() + }; + + let client_context = state.client_context.read().expect("lock is poisoned"); + + let inline_view = match client_context.get_all_inline_view_containers().first() { + Some((plugin_id, container)) => { + let plugin_id = plugin_id.clone(); + container.render_inline_root_widget() + .map(move |widget_event| { + AppMsg::WidgetEvent { + plugin_id: plugin_id.clone(), + render_location: UiRenderLocation::InlineView, + widget_event, + } + }) } None => { horizontal_space() .into() } - } - } + }; + let content: Element<_> = column(vec![ + inline_view, + list, + ]).into(); - match &self.global_state { - GlobalState::ErrorView { error_view } => { - match error_view { - ErrorViewData::PreferenceRequired { plugin_id, entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { + let (primary_action, action_panel) = if let Some(search_item) = focused_search_result.get(&state.search_results) { + let label = match search_item.entrypoint_type { + SearchResultEntrypointType::Command => "Run Command", + SearchResultEntrypointType::View => "Open View", + SearchResultEntrypointType::GeneratedCommand => "Run Command", + }.to_string(); - let (description_text, msg) = match (plugin_preferences_required, entrypoint_preferences_required) { - (true, true) => { - // TODO do not show "entrypoint" name to user - let description_text = "Before using, plugin and entrypoint preferences need to be specified"; - // note: - // we open plugin view and not entrypoint even though both need to be specified - let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: None }; - (description_text, msg) - } - (false, true) => { - // TODO do not show "entrypoint" name to user - let description_text = "Before using, entrypoint preferences need to be specified"; - let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: Some(entrypoint_id.clone()) }; - (description_text, msg) - } - (true, false) => { - let description_text = "Before using, plugin preferences need to be specified"; - let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: None }; - (description_text, msg) - } - (false, false) => unreachable!() + let default_shortcut = PhysicalShortcut { + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }; + + let mut actions: Vec<_> = search_item.entrypoint_actions + .iter() + .enumerate() + .map(|(index, action)| { + let physical_shortcut = if index == 0 { + Some(PhysicalShortcut { // secondary action + physical_key: PhysicalKey::Enter, + modifier_shift: true, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }) + } else { + action.shortcut.clone() }; - let description: Element<_> = text(description_text) - .shaping(Shaping::Advanced) - .into(); + ActionPanelItem::Action { + label: action.label.clone(), + widget_id: index + 1, + physical_shortcut, + } + }) + .collect(); - let description = container(description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PreferenceRequiredViewDescription); + let primary_action_widget_id = 0; - let button_label: Element<_> = text("Open Settings") - .into(); - - let button: Element<_> = button(button_label) - .on_press(msg) - .into(); - - let button = container(button) - .width(Length::Fill) - .center_x() - .into(); - - let content: Element<_> = column([ - description, - button - ]).into(); - - let content: Element<_> = container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .themed(ContainerStyle::Main); - - content - } - ErrorViewData::PluginError { .. } => { - let description: Element<_> = text("Error occurred in plugin when trying to show the view") - .into(); - - let description = container(description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PluginErrorViewTitle); - - let sub_description: Element<_> = text("Please report this to plugin author") - .into(); - - let sub_description = container(sub_description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PluginErrorViewDescription); - - let button_label: Element<_> = text("Close") - .into(); - - let button: Element<_> = button(button_label) - .on_press(AppMsg::HideWindow) - .into(); - - let button = container(button) - .width(Length::Fill) - .center_x() - .into(); - - let content: Element<_> = column([ - description, - sub_description, - button - ]).into(); - - let content: Element<_> = container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .themed(ContainerStyle::Main); - - content - } - ErrorViewData::UnknownError { display } => { - let description: Element<_> = text("Unknown error occurred") - .into(); - - let description = container(description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PluginErrorViewTitle); - - let sub_description: Element<_> = text("Please report") // TODO link - .into(); - - let sub_description = container(sub_description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PluginErrorViewDescription); - - let error_description: Element<_> = text(display) - .shaping(Shaping::Advanced) - .into(); - - let error_description = container(error_description) - .width(Length::Fill) - .themed(ContainerStyle::PluginErrorViewDescription); - - let error_description = scrollable(error_description) - .width(Length::Fill) - .into(); - - let button_label: Element<_> = text("Close") - .into(); - - let button: Element<_> = button(button_label) - .on_press(AppMsg::HideWindow) - .into(); - - let button = container(button) - .width(Length::Fill) - .center_x() - .into(); - - let content: Element<_> = column([ - description, - sub_description, - error_description, - button - ]).into(); - - let content: Element<_> = container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .themed(ContainerStyle::Main); - - content - } - ErrorViewData::BackendTimeout => { - let description: Element<_> = text("Error occurred") - .into(); - - let description = container(description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PluginErrorViewTitle); - - let sub_description: Element<_> = text("Backend was unable to process message in a timely manner") - .into(); - - let sub_description = container(sub_description) - .width(Length::Fill) - .center_x() - .themed(ContainerStyle::PluginErrorViewDescription); - - let button_label: Element<_> = text("Close") - .into(); - - let button: Element<_> = button(button_label) - .on_press(AppMsg::HideWindow) - .into(); - - let button = container(button) - .width(Length::Fill) - .center_x() - .into(); - - let content: Element<_> = column([ - description, - sub_description, - button - ]).into(); - - let content: Element<_> = container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .themed(ContainerStyle::Main); - - content - } - } - } - GlobalState::MainView { focused_search_result, sub_state, search_field_id, pending_plugin_view_loading_bar, .. } => { - let input: Element<_> = text_input("Search...", &self.prompt) - .on_input(AppMsg::PromptChanged) - .on_submit(AppMsg::PromptSubmit) - .ignore_with_modifiers(true) - .id(search_field_id.clone()) - .width(Length::Fill) - .themed(TextInputStyle::MainSearch); - - let search_list = search_list( - &self.search_results, - &focused_search_result, - |search_result| AppMsg::RunSearchItemAction(search_result, None), - ); - - let search_list = container(search_list) - .width(Length::Fill) - .themed(ContainerStyle::MainListInner); - - let list: Element<_> = scrollable(search_list) - .id(focused_search_result.scrollable_id.clone()) - .width(Length::Fill) - .into(); - - let list = container(list) - .width(Length::Fill) - .height(Length::Fill) - .themed(ContainerStyle::MainList); - - let input = container(input) - .width(Length::Fill) - .themed(ContainerStyle::MainSearchBar); - - let separator = if matches!(pending_plugin_view_loading_bar, LoadingBarState::On) || !self.loading_bar_state.is_empty() { - LoadingBar::new() - .into() + if actions.len() == 0 { + (Some((label, primary_action_widget_id, default_shortcut)), None) } else { - horizontal_rule(1) - .into() - }; - - let client_context = self.client_context.read().expect("lock is poisoned"); - - let inline_view = match client_context.get_all_inline_view_containers().first() { - Some((plugin_id, container)) => { - let plugin_id = plugin_id.clone(); - container.render_inline_root_widget() - .map(move |widget_event| { - AppMsg::WidgetEvent { - plugin_id: plugin_id.clone(), - render_location: UiRenderLocation::InlineView, - widget_event, - } - }) - } - None => { - horizontal_space() - .into() - } - }; - - let content: Element<_> = column(vec![ - inline_view, - list, - ]).into(); - - let (primary_action, action_panel) = if let Some(search_item) = focused_search_result.get(&self.search_results) { - let label = match search_item.entrypoint_type { - SearchResultEntrypointType::Command => "Run Command", - SearchResultEntrypointType::View => "Open View", - SearchResultEntrypointType::GeneratedCommand => "Run Command", - }.to_string(); - - let default_shortcut = PhysicalShortcut { - physical_key: PhysicalKey::Enter, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: false, + let primary_action = ActionPanelItem::Action { + label: label.clone(), + widget_id: primary_action_widget_id, + physical_shortcut: Some(default_shortcut.clone()), }; - let mut actions: Vec<_> = search_item.entrypoint_actions - .iter() - .enumerate() - .map(|(index, action)| { - let physical_shortcut = if index == 0 { - Some(PhysicalShortcut { // secondary action + actions.insert(0, primary_action); + + let action_panel = ActionPanel { + title: Some(search_item.entrypoint_name.clone()), + items: actions, + }; + + (Some((label, primary_action_widget_id, default_shortcut)), Some(action_panel)) + } + } else { + let client_context = state.client_context.read().expect("lock is poisoned"); + + match client_context.get_first_inline_view_action_panel() { + None => (None, None), + Some(action_panel) => { + match action_panel.find_first() { + None => (None, None), + Some((label, widget_id)) => { + let shortcut = PhysicalShortcut { physical_key: PhysicalKey::Enter, - modifier_shift: true, + modifier_shift: false, modifier_control: false, modifier_alt: false, - modifier_meta: false, - }) - } else { - action.shortcut.clone() - }; + modifier_meta: false + }; - ActionPanelItem::Action { - label: action.label.clone(), - widget_id: index + 1, - physical_shortcut, - } - }) - .collect(); - - let primary_action_widget_id = 0; - - if actions.len() == 0 { - (Some((label, primary_action_widget_id, default_shortcut)), None) - } else { - let primary_action = ActionPanelItem::Action { - label: label.clone(), - widget_id: primary_action_widget_id, - physical_shortcut: Some(default_shortcut.clone()), - }; - - actions.insert(0, primary_action); - - let action_panel = ActionPanel { - title: Some(search_item.entrypoint_name.clone()), - items: actions, - }; - - (Some((label, primary_action_widget_id, default_shortcut)), Some(action_panel)) - } - } else { - let client_context = self.client_context.read().expect("lock is poisoned"); - - match client_context.get_first_inline_view_action_panel() { - None => (None, None), - Some(action_panel) => { - match action_panel.find_first() { - None => (None, None), - Some((label, widget_id)) => { - let shortcut = PhysicalShortcut { - physical_key: PhysicalKey::Enter, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: false - }; - - (Some((label, widget_id, shortcut)), Some(action_panel)) - } + (Some((label, widget_id, shortcut)), Some(action_panel)) } } } - }; + } + }; - let toast_text = if !self.loading_bar_state.is_empty() { - Some("Indexing...") - } else { - None - }; + let toast_text = if !state.loading_bar_state.is_empty() { + Some("Indexing...") + } else { + None + }; - let root = match sub_state { - MainViewState::None => { - render_root( - false, - input, - separator, - toast_text, - content, - primary_action, - action_panel, - None::<&ScrollHandle>, - "", - || AppMsg::ToggleActionPanel { keyboard: false }, - |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, - |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, - || AppMsg::Noop, - ) - } - MainViewState::SearchResultActionPanel { focused_action_item, .. } => { - render_root( - true, - input, - separator, - toast_text, - content, - primary_action, - action_panel, - Some(focused_action_item), - "", - || AppMsg::ToggleActionPanel { keyboard: false }, - |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, - |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, - || AppMsg::Noop, - ) - } - MainViewState::InlineViewActionPanel { focused_action_item, .. } => { - render_root( - true, - input, - separator, - toast_text, - content, - primary_action, - action_panel, - Some(focused_action_item), - "", - || AppMsg::ToggleActionPanel { keyboard: false }, - |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, - |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, - || AppMsg::Noop, - ) - } - }; + let root = match sub_state { + MainViewState::None => { + render_root( + false, + input, + separator, + toast_text, + content, + primary_action, + action_panel, + None::<&ScrollHandle>, + "", + || AppMsg::ToggleActionPanel { keyboard: false }, + |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, + |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, + || AppMsg::Noop, + ) + } + MainViewState::SearchResultActionPanel { focused_action_item, .. } => { + render_root( + true, + input, + separator, + toast_text, + content, + primary_action, + action_panel, + Some(focused_action_item), + "", + || AppMsg::ToggleActionPanel { keyboard: false }, + |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, + |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, + || AppMsg::Noop, + ) + } + MainViewState::InlineViewActionPanel { focused_action_item, .. } => { + render_root( + true, + input, + separator, + toast_text, + content, + primary_action, + action_panel, + Some(focused_action_item), + "", + || AppMsg::ToggleActionPanel { keyboard: false }, + |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, + |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, + || AppMsg::Noop, + ) + } + }; - let root: Element<_> = container(root) - .width(Length::Fill) - .height(Length::Fill) - .themed(ContainerStyle::Main); + let root: Element<_> = container(root) + .width(Length::Fill) + .height(Length::Fill) + .themed(ContainerStyle::Main); - root - } - GlobalState::PluginView { plugin_view_data, sub_state, .. } => { - let PluginViewData { plugin_id, action_shortcuts, .. } = plugin_view_data; + root + } + GlobalState::PluginView { plugin_view_data, sub_state, .. } => { + let PluginViewData { plugin_id, action_shortcuts, .. } = plugin_view_data; - let client_context = self.client_context.read().expect("lock is poisoned"); - let view_container = client_context.get_view_container(); + let client_context = state.client_context.read().expect("lock is poisoned"); + let view_container = client_context.get_view_container(); - let container_element = view_container - .render_root_widget(sub_state, action_shortcuts) - .map(|widget_event| AppMsg::WidgetEvent { - plugin_id: plugin_id.clone(), - render_location: UiRenderLocation::View, - widget_event, - }); + let container_element = view_container + .render_root_widget(sub_state, action_shortcuts) + .map(|widget_event| AppMsg::WidgetEvent { + plugin_id: plugin_id.clone(), + render_location: UiRenderLocation::View, + widget_event, + }); - let element: Element<_> = container(container_element) - .width(Length::Fill) - .height(Length::Fill) - .themed(ContainerStyle::Root); + let element: Element<_> = container(container_element) + .width(Length::Fill) + .height(Length::Fill) + .themed(ContainerStyle::Root); - // let element = element.explain(color!(0xFF0000)); + // let element = element.explain(color!(0xFF0000)); - element - } + element } } +} - fn theme(&self, _window: window::Id) -> Self::Theme { - self.theme.clone() - } +fn subscription(state: &AppModel) -> Subscription { + let client_context = state.client_context.clone(); + let frontend_receiver = state.frontend_receiver.clone(); - fn subscription(&self) -> Subscription { - let client_context = self.client_context.clone(); - let frontend_receiver = self.frontend_receiver.clone(); + struct RequestLoop; + struct GlobalShortcutListener; - struct RequestLoop; - struct GlobalShortcutListener; + let events_subscription = event::listen_with(|event, status, window_id| match status { + event::Status::Ignored => Some(AppMsg::IcedEvent(event)), + event::Status::Captured => match &event { + Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Named(Named::Escape), .. }) => Some(AppMsg::IcedEvent(event)), + _ => None + } + }); - let events_subscription = event::listen_with(|event, status| match status { - event::Status::Ignored => Some(AppMsg::IcedEvent(event)), - event::Status::Captured => match &event { - Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Named(Named::Escape), .. }) => Some(AppMsg::IcedEvent(event)), - _ => None - } - }); - - Subscription::batch([ - subscription::channel( - std::any::TypeId::of::(), + Subscription::batch([ + Subscription::run_with_id( + std::any::TypeId::of::(), + stream::channel( 10, |sender| async move { register_listener(sender.clone()); @@ -1749,10 +1733,12 @@ impl Application for AppModel { unreachable!() }, - ), - events_subscription, - subscription::channel( - std::any::TypeId::of::(), + ) + ), + events_subscription, + Subscription::run_with_id( + std::any::TypeId::of::(), + stream::channel( 100, |sender| async move { request_loop(client_context, frontend_receiver, sender).await; @@ -1760,48 +1746,53 @@ impl Application for AppModel { panic!("request_rx was unexpectedly closed") }, ) - ]) - } + ) + ]) } + impl AppModel { - fn on_focused(&mut self) -> Command { + fn on_focused(&mut self) -> Task { self.focused = true; - Command::none() + Task::none() } - fn on_unfocused(&mut self) -> Command { + fn on_unfocused(&mut self) -> Task { // for some reason (on both macOS and linux x11) duplicate Unfocused fires right before Focus event if self.focused { self.focused = false; self.hide_window() } else { - Command::none() + Task::none() } } - fn hide_window(&mut self) -> Command { + fn hide_window(&mut self) -> Task { let mut commands = vec![]; - #[cfg(target_os = "linux")] - if self.wayland { - use iced::wayland::commands::layer_surface::KeyboardInteractivity; + // #[cfg(target_os = "linux")] + // if self.wayland { + // use iced::wayland::commands::layer_surface::KeyboardInteractivity; + // + // commands.push( + // iced::wayland::commands::layer_surface::destroy_layer_surface(window::Id::MAIN), + // ); + // commands.push( + // iced::wayland::commands::layer_surface::set_keyboard_interactivity(window::Id::MAIN, KeyboardInteractivity::None), + // ); + // } else { + // commands.push( + // window::change_mode(window::Id::MAIN, window::Mode::Hidden) + // ); + // }; + // + // #[cfg(not(target_os = "linux"))] + // commands.push( + // window::change_mode(window::Id::MAIN, window::Mode::Hidden) + // ); - commands.push( - iced::wayland::commands::layer_surface::destroy_layer_surface(window::Id::MAIN), - ); - commands.push( - iced::wayland::commands::layer_surface::set_keyboard_interactivity(window::Id::MAIN, KeyboardInteractivity::None), - ); - } else { - commands.push( - window::change_mode(window::Id::MAIN, window::Mode::Hidden) - ); - }; - - #[cfg(not(target_os = "linux"))] commands.push( - window::change_mode(window::Id::MAIN, window::Mode::Hidden) + window::change_mode(self.main_window_id, window::Mode::Hidden) ); match &self.global_state { @@ -1812,41 +1803,45 @@ impl AppModel { GlobalState::ErrorView { .. } => {} } - Command::batch(commands) + Task::batch(commands) } - fn show_window(&mut self) -> Command { + fn show_window(&mut self) -> Task { let mut commands = vec![]; - #[cfg(target_os = "linux")] - if self.wayland { - use iced::wayland::commands::layer_surface::KeyboardInteractivity; + // #[cfg(target_os = "linux")] + // if self.wayland { + // use iced::wayland::commands::layer_surface::KeyboardInteractivity; + // + // commands.push( + // iced::wayland::commands::layer_surface::get_layer_surface(layer_shell_settings()), + // ); + // commands.push( + // iced::wayland::commands::layer_surface::set_keyboard_interactivity(window::Id::MAIN, KeyboardInteractivity::Exclusive), + // ); + // } else { + // commands.push( + // window::change_mode(window::Id::MAIN, window::Mode::Windowed) + // ); + // }; + // + // #[cfg(not(target_os = "linux"))] + // commands.push( + // window::change_mode(window::Id::MAIN, window::Mode::Windowed) + // ); - commands.push( - iced::wayland::commands::layer_surface::get_layer_surface(layer_shell_settings()), - ); - commands.push( - iced::wayland::commands::layer_surface::set_keyboard_interactivity(window::Id::MAIN, KeyboardInteractivity::Exclusive), - ); - } else { - commands.push( - window::change_mode(window::Id::MAIN, window::Mode::Windowed) - ); - }; - - #[cfg(not(target_os = "linux"))] commands.push( - window::change_mode(window::Id::MAIN, window::Mode::Windowed) + window::change_mode(self.main_window_id, window::Mode::Windowed) ); commands.push( self.reset_window_state() ); - Command::batch(commands) + Task::batch(commands) } - fn reset_window_state(&mut self) -> Command { + fn reset_window_state(&mut self) -> Task { self.prompt = "".to_string(); let mut client_context = self.client_context.write().expect("lock is poisoned"); @@ -1859,17 +1854,17 @@ impl AppModel { if !self.wayland { commands.push( - window::gain_focus(window::Id::MAIN), + window::gain_focus(self.main_window_id), ); } - Command::batch(commands) + Task::batch(commands) } - fn open_plugin_view(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Command { + fn open_plugin_view(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Task { let mut backend_client = self.backend_api.clone(); - Command::perform(async move { + Task::perform(async move { let result = backend_client.request_view_render(plugin_id, entrypoint_id) .await?; @@ -1877,10 +1872,10 @@ impl AppModel { }, |result| handle_backend_error(result, |action_shortcuts| AppMsg::OnOpenView { action_shortcuts })) } - fn close_plugin_view(&self, plugin_id: PluginId) -> Command { + fn close_plugin_view(&self, plugin_id: PluginId) -> Task { let mut backend_client = self.backend_api.clone(); - Command::perform(async move { + Task::perform(async move { backend_client.request_view_close(plugin_id) .await?; @@ -1888,10 +1883,10 @@ impl AppModel { }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } - fn run_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Command { + fn run_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Task { let mut backend_client = self.backend_api.clone(); - Command::perform(async move { + Task::perform(async move { backend_client.request_run_command(plugin_id, entrypoint_id) .await?; @@ -1899,10 +1894,10 @@ impl AppModel { }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } - fn run_generated_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: Option) -> Command { + fn run_generated_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: Option) -> Task { let mut backend_client = self.backend_api.clone(); - Command::perform(async move { + Task::perform(async move { backend_client.request_run_generated_command(plugin_id, entrypoint_id, action_index) .await?; @@ -1910,11 +1905,11 @@ impl AppModel { }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } - fn handle_plugin_event(&self, widget_event: ComponentWidgetEvent, plugin_id: PluginId, render_location: UiRenderLocation) -> Command { + fn handle_plugin_event(&self, widget_event: ComponentWidgetEvent, plugin_id: PluginId, render_location: UiRenderLocation) -> Task { let mut backend_client = self.backend_api.clone(); let client_context = self.client_context.clone(); - Command::perform(async move { + Task::perform(async move { let event = { let client_context = client_context.read().expect("lock is poisoned"); client_context.handle_event(render_location, &plugin_id, widget_event.clone()) @@ -1947,10 +1942,10 @@ impl AppModel { }, |result| handle_backend_error(result, |msg| msg)) } - fn handle_main_view_keyboard_event(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Command { + fn handle_main_view_keyboard_event(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Task { let mut backend_client = self.backend_api.clone(); - Command::perform( + Task::perform( async move { backend_client.send_keyboard_event(plugin_id, entrypoint_id, KeyboardEventOrigin::MainView, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) .await?; @@ -1961,7 +1956,7 @@ impl AppModel { ) } - fn handle_plugin_view_keyboard_event(&self, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Command { + fn handle_plugin_view_keyboard_event(&self, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Task { let mut backend_client = self.backend_api.clone(); let (plugin_id, entrypoint_id) = { @@ -1969,7 +1964,7 @@ impl AppModel { (client_context.get_view_plugin_id(), client_context.get_view_entrypoint_id()) }; - Command::perform( + Task::perform( async move { backend_client.send_keyboard_event(plugin_id, entrypoint_id, KeyboardEventOrigin::PluginView, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) .await?; @@ -1980,20 +1975,20 @@ impl AppModel { ) } - fn handle_inline_plugin_view_keyboard_event(&self, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Command { + fn handle_inline_plugin_view_keyboard_event(&self, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Task { let mut backend_client = self.backend_api.clone(); let (plugin_id, entrypoint_id) = { let client_context = self.client_context.read().expect("lock is poisoned"); match client_context.get_first_inline_view_container() { None => { - return Command::none() + return Task::none() }, Some(container) => (container.get_plugin_id(), container.get_entrypoint_id()) } }; - Command::perform( + Task::perform( async move { backend_client.send_keyboard_event(plugin_id, entrypoint_id, KeyboardEventOrigin::PluginView, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) .await?; @@ -2004,10 +1999,10 @@ impl AppModel { ) } - fn search(&self, new_prompt: String, render_inline_view: bool) -> Command { + fn search(&self, new_prompt: String, render_inline_view: bool) -> Task { let mut backend_api = self.backend_api.clone(); - Command::perform(async move { + Task::perform(async move { let search_results = backend_api.search(new_prompt, render_inline_view) .await?; @@ -2015,10 +2010,10 @@ impl AppModel { }, |result| handle_backend_error(result, |search_results| AppMsg::SetSearchResults(search_results))) } - fn open_settings_window_preferences(&self, plugin_id: PluginId, entrypoint_id: Option) -> Command { + fn open_settings_window_preferences(&self, plugin_id: PluginId, entrypoint_id: Option) -> Task { let mut backend_api = self.backend_api.clone(); - Command::perform(async move { + Task::perform(async move { backend_api.open_settings_window_preferences(plugin_id, entrypoint_id) .await?; @@ -2026,10 +2021,10 @@ impl AppModel { }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } - fn inline_view_shortcuts(&self) -> Command { + fn inline_view_shortcuts(&self) -> Task { let mut backend_api = self.backend_api.clone(); - Command::perform(async move { + Task::perform(async move { backend_api.inline_view_shortcuts().await }, |result| handle_backend_error(result, |shortcuts| AppMsg::InlineViewShortcuts { shortcuts })) } @@ -2038,9 +2033,9 @@ impl AppModel { // these are needed to force focus the text_input in main search view when // the window is opened but text_input not focused impl AppModel { - fn append_prompt(prompt: &mut String, value: Option, search_field_id: text_input::Id, modifiers: Modifiers) -> Command { + fn append_prompt(prompt: &mut String, value: Option, search_field_id: text_input::Id, modifiers: Modifiers) -> Task { if modifiers.control() || modifiers.alt() || modifiers.logo() { - Command::none() + Task::none() } else { match value { Some(value) => { @@ -2048,15 +2043,15 @@ impl AppModel { *prompt = format!("{}{}", prompt, value); focus(search_field_id.clone()) } else { - Command::none() + Task::none() } } - None => Command::none() + None => Task::none() } } } - fn backspace_prompt(prompt: &mut String, search_field_id: text_input::Id) -> Command { + fn backspace_prompt(prompt: &mut String, search_field_id: text_input::Id) -> Task { let mut chars = prompt.chars(); chars.next_back(); *prompt = chars.as_str().to_owned(); diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs index 6ef6c29..b72fe82 100644 --- a/rust/client/src/ui/scroll_handle.rs +++ b/rust/client/src/ui/scroll_handle.rs @@ -1,5 +1,5 @@ use std::marker::PhantomData; -use iced::Command; +use iced::Task; use iced::widget::scrollable; use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; use crate::ui::AppMsg; @@ -45,7 +45,7 @@ impl ScrollHandle { } } - pub fn focus_next(&mut self, total_item_amount: usize) -> Option> { + pub fn focus_next(&mut self, total_item_amount: usize) -> Option> { match self.focus_next_in(total_item_amount, 1) { None => None, Some(index) => Some(self.scroll_to(index)) @@ -84,7 +84,7 @@ impl ScrollHandle { } } - pub fn focus_previous(&mut self) -> Option> { + pub fn focus_previous(&mut self) -> Option> { match self.focus_previous_in(1) { None => None, Some(index) => Some(self.scroll_to(index)) @@ -113,8 +113,8 @@ impl ScrollHandle { } } - pub fn scroll_to(&self, row_index: usize) -> Command { - let mut pos_y = row_index as f32 * self.item_height - (self.offset as f32 * self.item_height); + pub fn scroll_to(&self, row_index: usize) -> Task { + let pos_y = row_index as f32 * self.item_height - (self.offset as f32 * self.item_height); scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) } diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index e304bea..3fa01a4 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -104,7 +104,7 @@ impl<'a, Message> Component for SearchList<'a, Me button_content.push(sub_text); let button_content: Element<_> = row(button_content) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .into(); let style = match self.focused_search_result { diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index ed8ba83..79c1e2a 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -9,7 +9,7 @@ use crate::ui::AppMsg; use common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult}; use iced::widget::text_input; use iced::widget::text_input::focus; -use iced::Command; +use iced::Task; use std::collections::HashMap; use std::sync::{Arc, RwLock as StdRwLock}; @@ -101,73 +101,73 @@ impl GlobalState { } } - pub fn initial(prev_global_state: &mut GlobalState, client_context: Arc>) -> Command { + pub fn initial(prev_global_state: &mut GlobalState, client_context: Arc>) -> Task { let search_field_id = text_input::Id::unique(); *prev_global_state = GlobalState::new(search_field_id.clone(), client_context); - Command::batch([ + Task::batch([ focus(search_field_id), - Command::perform(async {}, |_| AppMsg::UpdateSearchResults), + Task::perform(async {}, |_| AppMsg::UpdateSearchResults), ]) } - pub fn error(prev_global_state: &mut GlobalState, error_view_data: ErrorViewData) -> Command { + pub fn error(prev_global_state: &mut GlobalState, error_view_data: ErrorViewData) -> Task { *prev_global_state = GlobalState::new_error(error_view_data); - Command::none() + Task::none() } - pub fn plugin(prev_global_state: &mut GlobalState, plugin_view_data: PluginViewData, client_context: Arc>) -> Command { + pub fn plugin(prev_global_state: &mut GlobalState, plugin_view_data: PluginViewData, client_context: Arc>) -> Task { *prev_global_state = GlobalState::new_plugin(plugin_view_data, client_context); - Command::none() + Task::none() } } pub trait Focus { - fn primary(&mut self, focus_list: &[T]) -> Command; - fn secondary(&mut self, focus_list: &[T]) -> Command; - fn back(&mut self) -> Command; - fn next(&mut self) -> Command; - fn previous(&mut self) -> Command; - fn up(&mut self, focus_list: &[T]) -> Command; - fn down(&mut self, focus_list: &[T]) -> Command; - fn left(&mut self, focus_list: &[T]) -> Command; - fn right(&mut self, focus_list: &[T]) -> Command; + fn primary(&mut self, focus_list: &[T]) -> Task; + fn secondary(&mut self, focus_list: &[T]) -> Task; + fn back(&mut self) -> Task; + fn next(&mut self) -> Task; + fn previous(&mut self) -> Task; + fn up(&mut self, focus_list: &[T]) -> Task; + fn down(&mut self, focus_list: &[T]) -> Task; + fn left(&mut self, focus_list: &[T]) -> Task; + fn right(&mut self, focus_list: &[T]) -> Task; } impl Focus for GlobalState { - fn primary(&mut self, focus_list: &[SearchResult]) -> Command { + fn primary(&mut self, focus_list: &[SearchResult]) -> Task { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { MainViewState::None => { if let Some(search_result) = focused_search_result.get(focus_list) { let search_result = search_result.clone(); - Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result }) + Task::done(AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result }) } else { - Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus) + Task::done(AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus) } } MainViewState::SearchResultActionPanel { focused_action_item, .. } => { match focused_action_item.index { - None => Command::none(), + None => Task::none(), Some(widget_id) => { if let Some(search_result) = focused_search_result.get(&focus_list) { let search_result = search_result.clone(); - Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id }) + Task::done(AppMsg::OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id }) } else { - Command::none() + Task::none() } } } } MainViewState::InlineViewActionPanel { focused_action_item } => { match focused_action_item.index { - None => Command::none(), + None => Task::none(), Some(widget_id) => { - Command::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id }) + Task::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id }) } } } @@ -182,40 +182,40 @@ impl Focus for GlobalState { PluginViewState::None => { if let Some(widget_id) = action_ids.get(0) { let widget_id = *widget_id; - Command::perform(async {}, move |_| AppMsg::OnPrimaryActionPluginViewNoPanelKeyboardWithFocus { widget_id }) + Task::perform(async {}, move |_| AppMsg::OnPrimaryActionPluginViewNoPanelKeyboardWithFocus { widget_id }) } else { - Command::none() + Task::none() } }, PluginViewState::ActionPanel { focused_action_item, .. } => { if let Some(widget_id) = focused_action_item.get(&action_ids) { let widget_id = *widget_id; - Command::perform(async {}, move |_| AppMsg::OnPrimaryActionPluginViewAnyPanelKeyboardWithFocus { widget_id }) + Task::perform(async {}, move |_| AppMsg::OnPrimaryActionPluginViewAnyPanelKeyboardWithFocus { widget_id }) } else { - Command::none() + Task::none() } } } } - GlobalState::ErrorView { .. } => Command::none() + GlobalState::ErrorView { .. } => Task::none() } } - fn secondary(&mut self, focus_list: &[SearchResult]) -> Command { + fn secondary(&mut self, focus_list: &[SearchResult]) -> Task { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { MainViewState::None => { if let Some(search_result) = focused_search_result.get(focus_list) { let search_result = search_result.clone(); - Command::perform(async {}, move |_| AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result }) + Task::done(AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result }) } else { - Command::perform(async {}, move |_| AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus) + Task::done(AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus) } } MainViewState::SearchResultActionPanel { .. } | MainViewState::InlineViewActionPanel { .. } => { // secondary does nothing when action panel is opened - Command::none() + Task::none() } } } @@ -228,35 +228,35 @@ impl Focus for GlobalState { PluginViewState::None => { if let Some(widget_id) = action_ids.get(1) { let widget_id = *widget_id; - Command::perform(async {}, move |_| AppMsg::OnSecondaryActionPluginViewNoPanelKeyboardWithFocus { widget_id }) + Task::perform(async {}, move |_| AppMsg::OnSecondaryActionPluginViewNoPanelKeyboardWithFocus { widget_id }) } else { - Command::none() + Task::none() } }, PluginViewState::ActionPanel { .. } => { // secondary does nothing when action panel is opened - Command::none() + Task::none() } } } - GlobalState::ErrorView { .. } => Command::none() + GlobalState::ErrorView { .. } => Task::none() } } - fn back(&mut self) -> Command { + fn back(&mut self) -> Task { match self { GlobalState::MainView { sub_state, .. } => { match sub_state { MainViewState::None => { - Command::perform(async {}, |_| AppMsg::HideWindow) + Task::perform(async {}, |_| AppMsg::HideWindow) } MainViewState::SearchResultActionPanel { .. } => { MainViewState::initial(sub_state); - Command::none() + Task::none() } MainViewState::InlineViewActionPanel { .. } => { MainViewState::initial(sub_state); - Command::none() + Task::none() } } } @@ -278,59 +278,59 @@ impl Focus for GlobalState { let client_context = client_context.clone(); - Command::batch([ - Command::perform(async {}, |_| AppMsg::ClosePluginView(plugin_id)), + Task::batch([ + Task::done(AppMsg::ClosePluginView(plugin_id)), GlobalState::initial(self, client_context) ]) } else { let plugin_id = plugin_id.clone(); let entrypoint_id = entrypoint_id.clone(); - Command::perform(async {}, |_| AppMsg::OpenPluginView(plugin_id, entrypoint_id)) + Task::done(AppMsg::OpenPluginView(plugin_id, entrypoint_id)) } } PluginViewState::ActionPanel { .. } => { - Command::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) + Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) } } } GlobalState::ErrorView { .. } => { - Command::perform(async {}, |_| AppMsg::HideWindow) + Task::perform(async {}, |_| AppMsg::HideWindow) } } } - fn next(&mut self) -> Command { + fn next(&mut self) -> Task { match self { - GlobalState::MainView { .. } => Command::none(), - GlobalState::PluginView { .. } => Command::none(), - GlobalState::ErrorView { .. } => Command::none(), + GlobalState::MainView { .. } => Task::none(), + GlobalState::PluginView { .. } => Task::none(), + GlobalState::ErrorView { .. } => Task::none(), } } - fn previous(&mut self) -> Command { + fn previous(&mut self) -> Task { match self { - GlobalState::MainView { .. } => Command::none(), - GlobalState::PluginView { .. } => Command::none(), - GlobalState::ErrorView { .. } => Command::none(), + GlobalState::MainView { .. } => Task::none(), + GlobalState::PluginView { .. } => Task::none(), + GlobalState::ErrorView { .. } => Task::none(), } } - fn up(&mut self, _focus_list: &[SearchResult]) -> Command { + fn up(&mut self, _focus_list: &[SearchResult]) -> Task { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { MainViewState::None => { focused_search_result.focus_previous() - .unwrap_or_else(|| Command::none()) + .unwrap_or_else(|| Task::none()) } MainViewState::SearchResultActionPanel { focused_action_item } => { focused_action_item.focus_previous() - .unwrap_or_else(|| Command::none()) + .unwrap_or_else(|| Task::none()) } MainViewState::InlineViewActionPanel { focused_action_item } => { focused_action_item.focus_previous() - .unwrap_or_else(|| Command::none()) + .unwrap_or_else(|| Task::none()) } } } - GlobalState::ErrorView { .. } => Command::none(), + GlobalState::ErrorView { .. } => Task::none(), GlobalState::PluginView { sub_state, client_context, .. } => { match sub_state { PluginViewState::None => { @@ -340,34 +340,34 @@ impl Focus for GlobalState { }, PluginViewState::ActionPanel { focused_action_item } => { focused_action_item.focus_previous() - .unwrap_or_else(|| Command::none()) + .unwrap_or_else(|| Task::none()) } } }, } } - fn down(&mut self, focus_list: &[SearchResult]) -> Command { + fn down(&mut self, focus_list: &[SearchResult]) -> Task { match self { GlobalState::MainView { focused_search_result, sub_state, client_context, .. } => { match sub_state { MainViewState::None => { if focus_list.len() != 0 { focused_search_result.focus_next(focus_list.len()) - .unwrap_or_else(|| Command::none()) + .unwrap_or_else(|| Task::none()) } else { - Command::none() + Task::none() } } MainViewState::SearchResultActionPanel { focused_action_item } => { if let Some(search_item) = focused_search_result.get(focus_list) { if search_item.entrypoint_actions.len() != 0 { focused_action_item.focus_next(search_item.entrypoint_actions.len() + 1) - .unwrap_or_else(|| Command::none()) + .unwrap_or_else(|| Task::none()) } else { - Command::none() + Task::none() } } else { - Command::none() + Task::none() } } MainViewState::InlineViewActionPanel { focused_action_item } => { @@ -377,17 +377,17 @@ impl Focus for GlobalState { Some(action_panel) => { if action_panel.action_count() != 0 { focused_action_item.focus_next(action_panel.action_count()) - .unwrap_or_else(|| Command::none()) + .unwrap_or_else(|| Task::none()) } else { - Command::none() + Task::none() } } - None => Command::none() + None => Task::none() } } } } - GlobalState::ErrorView { .. } => Command::none(), + GlobalState::ErrorView { .. } => Task::none(), GlobalState::PluginView { sub_state, client_context, .. } => { match sub_state { PluginViewState::None => { @@ -402,16 +402,16 @@ impl Focus for GlobalState { if action_ids.len() != 0 { focused_action_item.focus_next(action_ids.len()) - .unwrap_or_else(|| Command::none()) + .unwrap_or_else(|| Task::none()) } else { - Command::none() + Task::none() } } } } } } - fn left(&mut self, _focus_list: &[SearchResult]) -> Command { + fn left(&mut self, _focus_list: &[SearchResult]) -> Task { match self { GlobalState::PluginView { client_context, sub_state, .. } => { match sub_state { @@ -420,14 +420,14 @@ impl Focus for GlobalState { client_context.focus_left() } - PluginViewState::ActionPanel { .. } => Command::none() + PluginViewState::ActionPanel { .. } => Task::none() } }, - GlobalState::MainView { .. } => Command::none(), - GlobalState::ErrorView { .. } => Command::none(), + GlobalState::MainView { .. } => Task::none(), + GlobalState::ErrorView { .. } => Task::none(), } } - fn right(&mut self, _focus_list: &[SearchResult]) -> Command { + fn right(&mut self, _focus_list: &[SearchResult]) -> Task { match self { GlobalState::PluginView { client_context, sub_state, .. } => { match sub_state { @@ -436,11 +436,11 @@ impl Focus for GlobalState { client_context.focus_right() } - PluginViewState::ActionPanel { .. } => Command::none() + PluginViewState::ActionPanel { .. } => Task::none() } }, - GlobalState::MainView { .. } => Command::none(), - GlobalState::ErrorView { .. } => Command::none(), + GlobalState::MainView { .. } => Task::none(), + GlobalState::ErrorView { .. } => Task::none(), } } } diff --git a/rust/client/src/ui/theme/button.rs b/rust/client/src/ui/theme/button.rs index 9eee65d..7b90afc 100644 --- a/rust/client/src/ui/theme/button.rs +++ b/rust/client/src/ui/theme/button.rs @@ -1,14 +1,13 @@ -use button::Appearance; +use button::Style; use iced::{Border, Padding, Renderer}; use iced::widget::{button, Button}; +use iced::widget::button::Status; use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, NOT_INTENDED_TO_BE_USED, padding_all, ThemableWidget, TRANSPARENT}; -#[derive(Default, Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy)] pub enum ButtonStyle { - // #[default] ShouldNotBeUsed, - #[default] // TODO Not supposed to be default but unable to customize datepicker buttons right now DatePicker, Action, @@ -26,12 +25,6 @@ pub enum ButtonStyle { MetadataTagItem, } -enum ButtonState { - Active, - Hover, - Pressed, -} - impl ButtonStyle { fn padding(&self) -> Padding { let theme = get_theme(); @@ -88,7 +81,7 @@ impl ButtonStyle { } } - fn appearance(&self, theme: &GauntletComplexTheme, state: ButtonState) -> Appearance { + fn appearance(&self, theme: &GauntletComplexTheme, state: Status) -> Style { let (background_color, background_color_hover, background_color_pressed, text_color, text_color_hover, border_radius, border_width, border_color) = match &self { ButtonStyle::RootBottomPanelPrimaryActionButton | ButtonStyle::RootBottomPanelActionToggleButton => { let theme = &theme.root_bottom_panel_action_toggle_button; @@ -147,7 +140,7 @@ impl ButtonStyle { } }; - let active = Appearance { + let active = Style { background: background_color.map(|color| color.to_iced().into()), text_color: text_color.to_iced(), border: Border { @@ -159,38 +152,42 @@ impl ButtonStyle { }; match state { - ButtonState::Active => active, - ButtonState::Pressed => { - Appearance { + Status::Active => active, + Status::Pressed => { + Style { background: background_color_pressed.map(|color| color.to_iced().into()), text_color: text_color_hover.to_iced(), ..active } } - ButtonState::Hover => { - Appearance { + Status::Hovered => { + Style { background: background_color_hover.map(|color| color.to_iced().into()), text_color: text_color_hover.to_iced(), ..active } } + Status::Disabled => { + Style { + background: Some(NOT_INTENDED_TO_BE_USED.to_iced().into()), + ..active + } + } } } } -impl button::StyleSheet for GauntletComplexTheme { - type Style = ButtonStyle; +impl button::Catalog for GauntletComplexTheme { + type Class<'a> = ButtonStyle; - fn active(&self, style: &Self::Style) -> Appearance { - style.appearance(self, ButtonState::Active) + fn default<'a>() -> Self::Class<'a> { + // TODO Not supposed to be default but unable to customize datepicker buttons right now + // ButtonStyle::ShouldNotBeUsed + ButtonStyle::DatePicker } - fn hovered(&self, style: &Self::Style) -> Appearance { - style.appearance(self, ButtonState::Hover) - } - - fn pressed(&self, style: &Self::Style) -> Appearance { - style.appearance(self, ButtonState::Pressed) + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class.appearance(self, status) } } @@ -198,6 +195,6 @@ impl<'a, Message: 'a + Clone> ThemableWidget<'a, Message> for Button<'a, Message type Kind = ButtonStyle; fn themed(self, kind: ButtonStyle) -> Element<'a, Message> { - self.style(kind).padding(kind.padding()).into() + self.class(kind).padding(kind.padding()).into() } } \ No newline at end of file diff --git a/rust/client/src/ui/theme/checkbox.rs b/rust/client/src/ui/theme/checkbox.rs index 8c9f032..3b77b91 100644 --- a/rust/client/src/ui/theme/checkbox.rs +++ b/rust/client/src/ui/theme/checkbox.rs @@ -1,71 +1,80 @@ -use checkbox::Appearance; use iced::{Border, Renderer}; use iced::widget::{checkbox, Checkbox}; - +use iced::widget::checkbox::{Status, Style}; use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, NOT_INTENDED_TO_BE_USED, ThemableWidget}; -#[derive(Default)] pub enum CheckboxStyle { - #[default] Default, } -impl checkbox::StyleSheet for GauntletComplexTheme { - type Style = CheckboxStyle; +impl checkbox::Catalog for GauntletComplexTheme { + type Class<'a> = CheckboxStyle; - fn active(&self, _: &Self::Style, is_checked: bool) -> Appearance { - let theme = &self.form_input_checkbox; - - let background = if is_checked { - theme.background_color_checked.to_iced().into() - } else { - theme.background_color_unchecked.to_iced().into() - }; - - Appearance { - background, - icon_color: theme.icon_color.to_iced(), - border: Border { - radius: theme.border_radius.into(), - width: theme.border_width, - color: theme.border_color.to_iced().into(), - }, - text_color: None, - } + fn default<'a>() -> Self::Class<'a> { + CheckboxStyle::Default } - fn hovered(&self, _: &Self::Style, is_checked: bool) -> Appearance { - let theme = &self.form_input_checkbox; - - let background = if is_checked { - theme.background_color_checked_hovered.to_iced().into() - } else { - theme.background_color_unchecked_hovered.to_iced().into() - }; - - Appearance { - background, - icon_color: theme.icon_color.to_iced(), - border: Border { - radius: theme.border_radius.into(), - width: theme.border_width, - color: theme.border_color.to_iced().into(), - }, - text_color: None, + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + match status { + Status::Active { is_checked } => active(self, is_checked), + Status::Hovered { is_checked } => hovered(self, is_checked), + Status::Disabled { is_checked } => disabled(is_checked) } } +} - fn disabled(&self, _: &Self::Style, _is_checked: bool) -> Appearance { - Appearance { - background: NOT_INTENDED_TO_BE_USED.to_iced().into(), - icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: NOT_INTENDED_TO_BE_USED.to_iced(), - }, - text_color: None, - } +fn active(theme: &GauntletComplexTheme, is_checked: bool) -> Style { + let theme = &theme.form_input_checkbox; + + let background = if is_checked { + theme.background_color_checked.to_iced().into() + } else { + theme.background_color_unchecked.to_iced().into() + }; + + Style { + background, + icon_color: theme.icon_color.to_iced(), + border: Border { + radius: theme.border_radius.into(), + width: theme.border_width, + color: theme.border_color.to_iced().into(), + }, + text_color: None, + } +} + +fn hovered(theme: &GauntletComplexTheme, is_checked: bool) -> Style { + let theme = &theme.form_input_checkbox; + + let background = if is_checked { + theme.background_color_checked_hovered.to_iced().into() + } else { + theme.background_color_unchecked_hovered.to_iced().into() + }; + + Style { + background, + icon_color: theme.icon_color.to_iced(), + border: Border { + radius: theme.border_radius.into(), + width: theme.border_width, + color: theme.border_color.to_iced().into(), + }, + text_color: None, + } +} + +fn disabled(_is_checked: bool) -> Style { + Style { + background: NOT_INTENDED_TO_BE_USED.to_iced().into(), + icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), + border: Border { + radius: 2.0.into(), + width: 1.0, + color: NOT_INTENDED_TO_BE_USED.to_iced(), + }, + text_color: None, } } @@ -77,7 +86,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Checkbox<'a, Message, Gaun match style { CheckboxStyle::Default => { - self.style(style) + self.class(style) // .spacing() // TODO // .size() } diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 170193b..e1ea86c 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -1,8 +1,6 @@ -use container::Appearance; -use iced::{Border, Color, Length, Padding, Renderer}; -use iced::border::Radius; +use iced::{Border, Color, Length, Renderer}; use iced::widget::{Container, container}; - +use iced::widget::container::Style; use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; pub enum ContainerStyle { @@ -62,9 +60,7 @@ pub enum ContainerStyle { RootBottomPanelPrimaryActionButton, } -#[derive(Default)] pub enum ContainerStyleInner { - #[default] Transparent, Tooltip, @@ -81,18 +77,22 @@ pub enum ContainerStyleInner { } -impl container::StyleSheet for GauntletComplexTheme { - type Style = ContainerStyleInner; +impl container::Catalog for GauntletComplexTheme { + type Class<'a> = ContainerStyleInner; - fn appearance(&self, style: &Self::Style) -> Appearance { - match style { + fn default<'a>() -> Self::Class<'a> { + ContainerStyleInner::Transparent + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + match class { ContainerStyleInner::Transparent => Default::default(), ContainerStyleInner::ActionPanel => { let root_theme = &self.root; let panel_theme = &self.action_panel; let background_color = &panel_theme.background_color; - Appearance { + Style { text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -109,7 +109,7 @@ impl container::StyleSheet for GauntletComplexTheme { let background_color = &theme.background_color; let border_color = &theme.border_color; - Appearance { + Style { text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -125,7 +125,7 @@ impl container::StyleSheet for GauntletComplexTheme { let background_color = &theme.background_color; let border_color = &theme.border_color; - Appearance { + Style { text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -140,7 +140,7 @@ impl container::StyleSheet for GauntletComplexTheme { let theme = &self.root; let background_color = &theme.background_color; - Appearance { + Style { text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -155,7 +155,7 @@ impl container::StyleSheet for GauntletComplexTheme { let theme = &self.root; let background_color = &theme.background_color; - Appearance { + Style { text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -171,7 +171,7 @@ impl container::StyleSheet for GauntletComplexTheme { let tooltip_theme = &self.tooltip; let background_color = &tooltip_theme.background_color; - Appearance { + Style { text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -187,47 +187,47 @@ impl container::StyleSheet for GauntletComplexTheme { // TODO this border radius doesn't work on image, for some reason - Appearance { + Style { border: Border { radius: theme.border_radius.into(), width: 0.0, color: Color::TRANSPARENT, }, - ..Appearance::default() + ..Style::default() } } ContainerStyleInner::RootBottomPanel => { let root_theme = &self.root; let panel_theme = &self.root_bottom_panel; - Appearance { + Style { background: Some(panel_theme.background_color.to_iced().into()), border: Border { - radius: Radius::from([0.0, 0.0, root_theme.border_radius, root_theme.border_radius]), + radius: common_ui::radius(0.0, 0.0, root_theme.border_radius, root_theme.border_radius), width: root_theme.border_width, color: root_theme.border_color.to_iced(), }, - ..Appearance::default() + ..Style::default() } } ContainerStyleInner::InlineInner => { let theme = &self.inline_inner; - Appearance { + Style { background: Some(theme.background_color.to_iced().into()), border: Border { radius: theme.border_radius.into(), width: theme.border_width, color: theme.border_color.to_iced(), }, - ..Appearance::default() + ..Style::default() } } ContainerStyleInner::Hud => { let theme = &self.hud; let background_color = &theme.background_color; - Appearance { + Style { text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -256,15 +256,15 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau self.padding(theme.action_panel_title.padding.to_iced()) } ContainerStyle::ActionShortcutModifier => { - self.style(ContainerStyleInner::ActionShortcutModifier) + self.class(ContainerStyleInner::ActionShortcutModifier) .padding(theme.action_shortcut_modifier.padding.to_iced()) } ContainerStyle::ActionShortcutModifiersInit => { let horizontal_spacing = theme.action_shortcut_modifier.spacing; - self.padding(Padding::from([0.0, horizontal_spacing, 0.0, 0.0])) + self.padding(common_ui::padding(0.0, horizontal_spacing, 0.0, 0.0)) } ContainerStyle::ActionPanel => { - self.style(ContainerStyleInner::ActionPanel) + self.class(ContainerStyleInner::ActionPanel) .padding(theme.action_panel.padding.to_iced()) .height(Length::Fixed(250.0)) .width(Length::Fixed(350.0)) @@ -282,7 +282,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau self.padding(theme.metadata_item_value.padding.to_iced()) } ContainerStyle::RootBottomPanel => { - self.style(ContainerStyleInner::RootBottomPanel) + self.class(ContainerStyleInner::RootBottomPanel) .padding(theme.root_bottom_panel.padding.to_iced()) } ContainerStyle::RootTopPanel => { @@ -307,11 +307,11 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau self.padding(theme.content_code_block.padding.to_iced()) } ContainerStyle::ContentCodeBlockText => { - self.style(ContainerStyleInner::ContentCodeBlockText) + self.class(ContainerStyleInner::ContentCodeBlockText) .padding(theme.content_code_block_text.padding.to_iced()) } ContainerStyle::ContentImage => { - self.style(ContainerStyleInner::ContentImage) + self.class(ContainerStyleInner::ContentImage) .padding(theme.content_image.padding.to_iced()) } ContainerStyle::DetailContentInner => { @@ -340,7 +340,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau .height(120) .max_height(120) .padding(theme.inline_inner.padding.to_iced()) - .style(ContainerStyleInner::InlineInner) + .class(ContainerStyleInner::InlineInner) } ContainerStyle::InlineName => { self.padding(theme.inline_name.padding.to_iced()) @@ -349,7 +349,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau self.padding(theme.empty_view_image.padding.to_iced()) } ContainerStyle::Main => { - self.style(ContainerStyleInner::Main) + self.class(ContainerStyleInner::Main) } ContainerStyle::MainListItemText => { self.padding(theme.main_list_item_text.padding.to_iced()) @@ -370,7 +370,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau self.padding(theme.main_search_bar.padding.to_iced()) } ContainerStyle::Root => { - self.style(ContainerStyleInner::Root) + self.class(ContainerStyleInner::Root) } ContainerStyle::PluginErrorViewTitle => { self.padding(theme.plugin_error_view_title.padding.to_iced()) @@ -406,14 +406,14 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau self.padding(theme.root_bottom_panel_primary_action_text.padding.to_iced()) } ContainerStyle::RootBottomPanelPrimaryActionButton => { - self.padding(Padding::from([0.0, theme.root_bottom_panel.spacing, 0.0, 0.0])) + self.padding(common_ui::padding(0.0, theme.root_bottom_panel.spacing, 0.0, 0.0)) } ContainerStyle::TextAccessory => { self.padding(theme.text_accessory.padding.to_iced()) } ContainerStyle::TextAccessoryIcon => { let horizontal_spacing = theme.text_accessory.spacing; - self.padding(Padding::from([0.0, horizontal_spacing, 0.0, 0.0])) + self.padding(common_ui::padding(0.0, horizontal_spacing, 0.0, 0.0)) } ContainerStyle::IconAccessory => { self.padding(theme.icon_accessory.padding.to_iced()) @@ -422,7 +422,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau self.padding(theme.hud_content.padding.to_iced()) } ContainerStyle::Hud => { - self.style(ContainerStyleInner::Hud) + self.class(ContainerStyleInner::Hud) } }.into() } diff --git a/rust/client/src/ui/theme/date_picker.rs b/rust/client/src/ui/theme/date_picker.rs index 993a7f7..398cd88 100644 --- a/rust/client/src/ui/theme/date_picker.rs +++ b/rust/client/src/ui/theme/date_picker.rs @@ -1,8 +1,9 @@ +use crate::ui::theme::{Element, GauntletComplexTheme, ThemableWidget}; use iced::Color; -use iced_aw::{date_picker, DatePicker}; -use iced_aw::date_picker::Appearance; - -use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; +use iced_aw::date_picker::Style; +use iced_aw::style::date_picker::Catalog; +use iced_aw::style::Status; +use iced_aw::DatePicker; #[derive(Clone, Default)] pub enum DatePickerStyle { @@ -10,59 +11,73 @@ pub enum DatePickerStyle { Default, } -impl date_picker::StyleSheet for GauntletComplexTheme { - type Style = DatePickerStyle; +impl Catalog for GauntletComplexTheme { + type Class<'a> = DatePickerStyle; - fn active(&self, _: &Self::Style) -> Appearance { - let theme = get_theme(); - let root_theme = &theme.root; - let theme = &theme.form_input_date_picker; - - Appearance { - background: theme.background_color.to_iced().into(), - border_radius: root_theme.border_radius, - border_width: root_theme.border_width, - border_color: root_theme.border_color.to_iced(), - text_color: theme.text_color.to_iced(), - text_attenuated_color: theme.text_attenuated_color.to_iced(), - day_background: theme.day_background_color.to_iced().into(), - } + fn default<'a>() -> Self::Class<'a> { + DatePickerStyle::Default } - fn selected(&self, style: &Self::Style) -> Appearance { - let theme = get_theme(); - let theme = &theme.form_input_date_picker; - - Appearance { - day_background: theme.day_background_color_selected.to_iced().into(), - text_color: theme.text_color_selected.to_iced(), - ..self.active(style) - } - } - - fn hovered(&self, style: &Self::Style) -> Appearance { - let theme = get_theme(); - let theme = &theme.form_input_date_picker; - - Appearance { - day_background: theme.day_background_color_hovered.to_iced().into(), - text_color: theme.text_color_hovered.to_iced(), - ..self.active(style) - } - } - - fn focused(&self, style: &Self::Style) -> Appearance { - Appearance { - border_color: Color::from_rgb(0.5, 0.5, 0.5), // TODO move to theme? - ..self.active(style) + fn style(&self, _class: &Self::Class<'_>, status: Status) -> Style { + match status { + Status::Active => active(self), + Status::Hovered => hovered(self), + Status::Pressed => hovered(self), // TODO proper styling + Status::Disabled => hovered(self), // TODO proper styling + Status::Focused => focused(self), + Status::Selected => selected(self) } } } + +fn active(theme: &GauntletComplexTheme) -> Style { + let root_theme = &theme.root; + let theme = &theme.form_input_date_picker; + + Style { + background: theme.background_color.to_iced().into(), + border_radius: root_theme.border_radius, + border_width: root_theme.border_width, + border_color: root_theme.border_color.to_iced(), + text_color: theme.text_color.to_iced(), + text_attenuated_color: theme.text_attenuated_color.to_iced(), + day_background: theme.day_background_color.to_iced().into(), + } +} + +fn selected(theme: &GauntletComplexTheme) -> Style { + let form_theme = &theme.form_input_date_picker; + + Style { + day_background: form_theme.day_background_color_selected.to_iced().into(), + text_color: form_theme.text_color_selected.to_iced(), + ..active(theme) + } +} + +fn hovered(theme: &GauntletComplexTheme) -> Style { + let form_theme = &theme.form_input_date_picker; + + Style { + day_background: form_theme.day_background_color_hovered.to_iced().into(), + text_color: form_theme.text_color_hovered.to_iced(), + ..active(theme) + } +} + +fn focused(theme: &GauntletComplexTheme) -> Style { + Style { + border_color: Color::from_rgb(0.5, 0.5, 0.5), // TODO move to theme? + ..active(theme) + } +} + + impl<'a, Message: 'a + Clone + 'static> ThemableWidget<'a, Message> for DatePicker<'a, Message, GauntletComplexTheme> { type Kind = DatePickerStyle; fn themed(self, kind: DatePickerStyle) -> Element<'a, Message> { - self.style(kind).into() + self.class(kind).into() } } \ No newline at end of file diff --git a/rust/client/src/ui/theme/loading_bar.rs b/rust/client/src/ui/theme/loading_bar.rs index 8098600..7696322 100644 --- a/rust/client/src/ui/theme/loading_bar.rs +++ b/rust/client/src/ui/theme/loading_bar.rs @@ -1,5 +1,5 @@ use crate::ui::custom_widgets::loading_bar; -use crate::ui::custom_widgets::loading_bar::{Appearance, LoadingBar}; +use crate::ui::custom_widgets::loading_bar::{LoadingBar, Style}; use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::GauntletComplexTheme; @@ -9,18 +9,22 @@ pub enum LoadingBarStyle { Default, } -impl loading_bar::StyleSheet for GauntletComplexTheme { - type Style = LoadingBarStyle; +impl loading_bar::Catalog for GauntletComplexTheme { + type Class<'a> = LoadingBarStyle; - fn appearance(&self, _style: &Self::Style) -> Appearance { - Appearance { + fn default<'a>() -> Self::Class<'a> { + LoadingBarStyle::Default + } + + fn style(&self, _class: &Self::Class<'_>) -> Style { + Style { background_color: self.loading_bar.background_color.to_iced(), loading_bar_color: self.loading_bar.loading_bar_color.to_iced(), } } } -impl<'a, Message: 'a> ThemableWidget<'a, Message> for LoadingBar { +impl<'a, Message: 'a> ThemableWidget<'a, Message> for LoadingBar<'a, GauntletComplexTheme> { type Kind = LoadingBarStyle; fn themed(self, _kind: LoadingBarStyle) -> Element<'a, Message> { diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 84413de..d2cbfb1 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -1,6 +1,7 @@ use std::io::ErrorKind; use std::path::PathBuf; use iced::{application, Color, Padding}; +use iced::application::DefaultStyle; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use common::dirs::Dirs; @@ -995,10 +996,8 @@ pub trait ThemableWidget<'a, Message> { fn themed(self, name: Self::Kind) -> Element<'a, Message>; } -impl application::StyleSheet for GauntletComplexTheme { - type Style = (); - - fn appearance(&self, _: &Self::Style) -> application::Appearance { +impl DefaultStyle for GauntletComplexTheme { + fn default_style(&self) -> application::Appearance { let theme = get_theme(); application::Appearance { diff --git a/rust/client/src/ui/theme/pick_list.rs b/rust/client/src/ui/theme/pick_list.rs index 5e00c3f..7ad0da3 100644 --- a/rust/client/src/ui/theme/pick_list.rs +++ b/rust/client/src/ui/theme/pick_list.rs @@ -2,7 +2,7 @@ use std::borrow::Borrow; use iced::{Border, overlay}; use iced::widget::{pick_list, PickList}; - +use iced::widget::pick_list::Status; use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, NOT_INTENDED_TO_BE_USED, ThemableWidget}; #[derive(Clone, Default)] @@ -17,58 +17,52 @@ pub enum MenuStyle { Default, } -impl pick_list::StyleSheet for GauntletComplexTheme { - type Style = PickListStyle; +impl pick_list::Catalog for GauntletComplexTheme { + type Class<'a> = PickListStyle; - fn active(&self, _: &Self::Style) -> pick_list::Appearance { - pick_list_appearance(PickListState::Active) + fn default<'a>() -> ::Class<'a> { + PickListStyle::Default } - fn hovered(&self, _: &Self::Style) -> pick_list::Appearance { - pick_list_appearance(PickListState::Hovered) - } -} - -enum PickListState { - Active, - Hovered -} - -fn pick_list_appearance(state: PickListState) -> pick_list::Appearance { - let theme = get_theme(); - let theme = &theme.form_input_select; - - let background_color = match state { - PickListState::Active => theme.background_color.to_iced(), - PickListState::Hovered => theme.background_color_hovered.to_iced(), - }; - - let text_color = match state { - PickListState::Active => theme.text_color.to_iced(), - PickListState::Hovered => theme.text_color_hovered.to_iced(), - }; - - pick_list::Appearance { - text_color, - background: background_color.into(), - placeholder_color: NOT_INTENDED_TO_BE_USED.to_iced(), - handle_color: text_color, - border: Border { - radius: theme.border_radius.into(), - width: theme.border_width, - color: theme.border_color.to_iced().into(), - }, - } -} - -impl overlay::menu::StyleSheet for GauntletComplexTheme { - type Style = MenuStyle; - - fn appearance(&self, _: &Self::Style) -> overlay::menu::Appearance { + fn style(&self, _class: &::Class<'_>, status: Status) -> pick_list::Style { let theme = get_theme(); - let theme = &theme.form_input_select_menu; // TODO consider using root style + let theme = &theme.form_input_select; - overlay::menu::Appearance { + let background_color = match status { + Status::Active | Status::Opened => theme.background_color.to_iced(), + Status::Hovered => theme.background_color_hovered.to_iced(), + }; + + let text_color = match status { + Status::Active | Status::Opened => theme.text_color.to_iced(), + Status::Hovered => theme.text_color_hovered.to_iced(), + }; + + pick_list::Style { + text_color, + background: background_color.into(), + placeholder_color: NOT_INTENDED_TO_BE_USED.to_iced(), + handle_color: text_color, + border: Border { + radius: theme.border_radius.into(), + width: theme.border_width, + color: theme.border_color.to_iced().into(), + }, + } + } +} + +impl overlay::menu::Catalog for GauntletComplexTheme { + type Class<'a> = MenuStyle; + + fn default<'a>() -> ::Class<'a> { + MenuStyle::Default + } + + fn style(&self, _class: &::Class<'_>) -> overlay::menu::Style { + let theme = &self.form_input_select_menu; // TODO consider using root style + + overlay::menu::Style { text_color: theme.text_color.to_iced(), background: theme.background_color.to_iced().into(), border: Border { @@ -82,14 +76,6 @@ impl overlay::menu::StyleSheet for GauntletComplexTheme { } } -impl From for MenuStyle { - fn from(pick_list: PickListStyle) -> Self { - match pick_list { - PickListStyle::Default => Self::Default, - } - } -} - impl<'a, Message: 'a + Clone + 'static, T, L, V> ThemableWidget<'a, Message> for PickList<'a, T, L, V, Message, GauntletComplexTheme> where T: ToString + PartialEq + Clone + 'a, @@ -99,7 +85,7 @@ where type Kind = PickListStyle; fn themed(self, kind: PickListStyle) -> Element<'a, Message> { - self.style(kind) + self.class(kind) // .padding() // TODO .into() } diff --git a/rust/client/src/ui/theme/row.rs b/rust/client/src/ui/theme/row.rs index 07acca1..7bc0d7f 100644 --- a/rust/client/src/ui/theme/row.rs +++ b/rust/client/src/ui/theme/row.rs @@ -37,12 +37,12 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletC } RowStyle::ListFirstSectionTitle => { let padding = theme.list_section_title.padding.to_iced(); - self.padding(Padding::from([padding.bottom, padding.right, padding.bottom, padding.left])) + self.padding(common_ui::padding(padding.bottom, padding.right, padding.bottom, padding.left)) .spacing(theme.list_section_title.spacing) } RowStyle::GridFirstSectionTitle => { let padding = theme.grid_section_title.padding.to_iced(); - self.padding(Padding::from([0.0, padding.right, padding.bottom, padding.left])) + self.padding(common_ui::padding(0.0, padding.right, padding.bottom, padding.left)) .spacing(theme.grid_section_title.spacing) } RowStyle::GridItemTitle => { diff --git a/rust/client/src/ui/theme/rule.rs b/rust/client/src/ui/theme/rule.rs index bd033d2..61095e1 100644 --- a/rust/client/src/ui/theme/rule.rs +++ b/rust/client/src/ui/theme/rule.rs @@ -1,42 +1,42 @@ +use iced::widget::rule::Style; use iced::widget::{rule, Rule}; -use rule::Appearance; -use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; +use crate::ui::theme::{Element, GauntletComplexTheme, ThemableWidget}; -#[derive(Default)] pub enum RuleStyle { - #[default] Default, ActionPanel, PrimaryActionSeparator, } -impl rule::StyleSheet for GauntletComplexTheme { - type Style = RuleStyle; +impl rule::Catalog for GauntletComplexTheme { + type Class<'a> = RuleStyle; - fn appearance(&self, style: &Self::Style) -> Appearance { - let theme = get_theme(); + fn default<'a>() -> Self::Class<'a> { + RuleStyle::Default + } - match style { + fn style(&self, class: &Self::Class<'_>) -> Style { + match class { RuleStyle::Default => { - Appearance { - color: theme.separator.color.to_iced(), + Style { + color: self.separator.color.to_iced(), width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Full, } } RuleStyle::ActionPanel => { - Appearance { - color: theme.separator.color.to_iced(), + Style { + color: self.separator.color.to_iced(), width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Percent(96.0), } } RuleStyle::PrimaryActionSeparator => { - Appearance { - color: theme.separator.color.to_iced(), + Style { + color: self.separator.color.to_iced(), width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Percent(70.0), @@ -46,10 +46,10 @@ impl rule::StyleSheet for GauntletComplexTheme { } } -impl<'a, Message: 'a> ThemableWidget<'a, Message> for Rule { +impl<'a, Message: 'a> ThemableWidget<'a, Message> for Rule<'a, GauntletComplexTheme> { type Kind = RuleStyle; fn themed(self, kind: RuleStyle) -> Element<'a, Message> { - self.style(kind).into() + self.class(kind).into() } } diff --git a/rust/client/src/ui/theme/scrollable.rs b/rust/client/src/ui/theme/scrollable.rs index 5ba8a79..d55a622 100644 --- a/rust/client/src/ui/theme/scrollable.rs +++ b/rust/client/src/ui/theme/scrollable.rs @@ -1,56 +1,94 @@ use iced::{Border, Color}; -use iced::widget::scrollable; -use scrollable::Appearance; +use iced::widget::{container, scrollable}; +use iced::widget::scrollable::{Status, Style}; use crate::ui::theme::{GauntletComplexTheme, get_theme}; -impl scrollable::StyleSheet for GauntletComplexTheme { - type Style = (); +impl scrollable::Catalog for GauntletComplexTheme { + type Class<'a> = (); - fn active(&self, _: &Self::Style) -> Appearance { - appearance(ScrollbarState::Active) + fn default<'a>() -> Self::Class<'a> { + () } - fn hovered(&self, _: &Self::Style, is_mouse_over_scrollbar: bool) -> Appearance { - if is_mouse_over_scrollbar { - appearance(ScrollbarState::Hovered) - } else { - self.active(&()) - } - } -} + fn style(&self, _class: &Self::Class<'_>, status: Status) -> Style { + let theme = get_theme(); + let theme = &theme.scrollbar; -enum ScrollbarState { - Active, - Hovered -} - -fn appearance(state: ScrollbarState) -> Appearance { - let theme = get_theme(); - let theme = &theme.scrollbar; - - let scroller_color = match state { - ScrollbarState::Active => Color::TRANSPARENT, - ScrollbarState::Hovered => theme.color.to_iced(), - }; - - Appearance { - container: Default::default(), - scrollbar: scrollable::Scrollbar { + let scrollbar = scrollable::Rail { background: None, - border: Border { - color: Color::TRANSPARENT, - ..Border::default() - }, + border: Border::default(), scroller: scrollable::Scroller { - color: scroller_color, + color: Color::TRANSPARENT, border: Border { color: theme.border_color.to_iced(), width: theme.border_width.into(), radius: theme.border_radius.into(), }, }, - }, - gap: None, + }; + + match status { + Status::Active => Style { + container: container::Style::default(), + vertical_rail: scrollbar, + horizontal_rail: scrollbar, + gap: None, + }, + Status::Hovered { + is_horizontal_scrollbar_hovered, + is_vertical_scrollbar_hovered, + } => { + let hovered_scrollbar = scrollable::Rail { + scroller: scrollable::Scroller { + color: theme.color.to_iced(), + ..scrollbar.scroller + }, + ..scrollbar + }; + + Style { + container: container::Style::default(), + vertical_rail: if is_vertical_scrollbar_hovered { + hovered_scrollbar + } else { + scrollbar + }, + horizontal_rail: if is_horizontal_scrollbar_hovered { + hovered_scrollbar + } else { + scrollbar + }, + gap: None, + } + } + Status::Dragged { + is_horizontal_scrollbar_dragged, + is_vertical_scrollbar_dragged, + } => { + let dragged_scrollbar = scrollable::Rail { + scroller: scrollable::Scroller { + color: theme.color.to_iced(), + ..scrollbar.scroller + }, + ..scrollbar + }; + + Style { + container: container::Style::default(), + vertical_rail: if is_vertical_scrollbar_dragged { + dragged_scrollbar + } else { + scrollbar + }, + horizontal_rail: if is_horizontal_scrollbar_dragged { + dragged_scrollbar + } else { + scrollbar + }, + gap: None, + } + } + } } -} \ No newline at end of file +} diff --git a/rust/client/src/ui/theme/text.rs b/rust/client/src/ui/theme/text.rs index 02c8295..3760984 100644 --- a/rust/client/src/ui/theme/text.rs +++ b/rust/client/src/ui/theme/text.rs @@ -1,7 +1,6 @@ use iced::Renderer; use iced::widget::{Text, text}; -use text::Appearance; - +use iced::widget::text::Style; use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; #[derive(Clone, Default)] @@ -35,75 +34,79 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Text<'a, GauntletComplexTh TextStyle::MetadataItemLabel => { let theme = get_theme(); - self.style(kind) + self.class(kind) .size(theme.metadata_item_label.text_size) .into() } TextStyle::InlineName => { self.size(15) - .style(kind) + .class(kind) .into() } _ => { - self.style(kind) + self.class(kind) .into() } } } } -impl text::StyleSheet for GauntletComplexTheme { - type Style = TextStyle; +impl text::Catalog for GauntletComplexTheme { + type Class<'a> = TextStyle; - fn appearance(&self, style: Self::Style) -> Appearance { - match style { + fn default<'a>() -> Self::Class<'a> { + TextStyle::Default + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + match class { TextStyle::Default => Default::default(), - TextStyle::EmptyViewSubtitle => Appearance { + TextStyle::EmptyViewSubtitle => Style { color: Some(self.empty_view_subtitle.text_color.to_iced()), }, - TextStyle::ListItemSubtitle => Appearance { + TextStyle::ListItemSubtitle => Style { color: Some(self.list_item_subtitle.text_color.to_iced()), }, - TextStyle::ListSectionTitle => Appearance { + TextStyle::ListSectionTitle => Style { color: Some(self.list_section_title.text_color.to_iced()), }, - TextStyle::ListSectionSubtitle => Appearance { + TextStyle::ListSectionSubtitle => Style { color: Some(self.list_section_subtitle.text_color.to_iced()), }, - TextStyle::GridSectionTitle => Appearance { + TextStyle::GridSectionTitle => Style { color: Some(self.grid_section_title.text_color.to_iced()), }, - TextStyle::GridSectionSubtitle => Appearance{ + TextStyle::GridSectionSubtitle => Style{ color: Some(self.grid_section_subtitle.text_color.to_iced()), }, - TextStyle::MainListItemSubtext => Appearance { + TextStyle::MainListItemSubtext => Style { color: Some(self.main_list_item_sub_text.text_color.to_iced()), }, - TextStyle::MetadataItemLabel => Appearance { + TextStyle::MetadataItemLabel => Style { color: Some(self.metadata_item_label.text_color.to_iced()), }, - TextStyle::TextAccessory => Appearance { + TextStyle::TextAccessory => Style { color: Some(self.text_accessory.text_color.to_iced()), }, - TextStyle::IconAccessory => Appearance { + TextStyle::IconAccessory => Style { color: Some(self.icon_accessory.icon_color.to_iced()), }, - TextStyle::GridItemTitle => Appearance { + TextStyle::GridItemTitle => Style { color: Some(self.grid_item_title.text_color.to_iced()), }, - TextStyle::GridItemSubTitle => Appearance { + TextStyle::GridItemSubTitle => Style { color: Some(self.grid_item_subtitle.text_color.to_iced()), }, - TextStyle::InlineName => Appearance { + TextStyle::InlineName => Style { color: Some(self.inline_name.text_color.to_iced()), }, - TextStyle::InlineSeparator => Appearance { + TextStyle::InlineSeparator => Style { color: Some(self.inline_separator.text_color.to_iced()), }, - TextStyle::RootBottomPanelPrimaryActionText => Appearance { + TextStyle::RootBottomPanelPrimaryActionText => Style { color: Some(self.root_bottom_panel_primary_action_text.text_color.to_iced()), }, - TextStyle::RootBottomPanelActionToggleText => Appearance { + TextStyle::RootBottomPanelActionToggleText => Style { color: Some(self.root_bottom_panel_action_toggle_text.text_color.to_iced()), } } diff --git a/rust/client/src/ui/theme/text_input.rs b/rust/client/src/ui/theme/text_input.rs index 80f9d80..ae0f6e4 100644 --- a/rust/client/src/ui/theme/text_input.rs +++ b/rust/client/src/ui/theme/text_input.rs @@ -1,12 +1,10 @@ +use iced::widget::text_input::{Status, Style}; use iced::widget::{text_input, TextInput}; use iced::{Border, Color, Renderer}; -use text_input::Appearance; use crate::ui::theme::{Element, GauntletComplexTheme, ThemableWidget, NOT_INTENDED_TO_BE_USED}; -#[derive(Default)] pub enum TextInputStyle { - #[default] ShouldNotBeUsed, MainSearch, @@ -14,112 +12,129 @@ pub enum TextInputStyle { FormInput, } -// noinspection RsSortImplTraitMembers -impl text_input::StyleSheet for GauntletComplexTheme { - type Style = TextInputStyle; +impl text_input::Catalog for GauntletComplexTheme { + type Class<'a> = TextInputStyle; - fn active(&self, style: &Self::Style) -> Appearance { - match style { - TextInputStyle::ShouldNotBeUsed => { - Appearance { - background: NOT_INTENDED_TO_BE_USED.to_iced().into(), - border: Border { - color: NOT_INTENDED_TO_BE_USED.to_iced().into(), - ..Border::default() - }, - icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), - } - }, - TextInputStyle::FormInput => { - let theme = &self.form_input_text_field; + fn default<'a>() -> Self::Class<'a> { + TextInputStyle::ShouldNotBeUsed + } - Appearance { - background: theme.background_color.to_iced().into(), - border: Border { - radius: theme.border_radius.into(), - width: theme.border_width, - color: theme.border_color.to_iced().into(), - }, - icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), - } - }, - TextInputStyle::MainSearch | TextInputStyle::PluginSearchBar => { - Appearance { - background: Color::TRANSPARENT.into(), - border: Border { - color: Color::TRANSPARENT, - ..Border::default() - }, - icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), - } - }, + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + match status { + Status::Active => active(self, class), + Status::Hovered => focused(self, class), // TODO proper style + Status::Focused => focused(self, class), + Status::Disabled => disabled(), } } +} - fn focused(&self, style: &Self::Style) -> Appearance { - match style { - TextInputStyle::ShouldNotBeUsed => { - Appearance { - background: NOT_INTENDED_TO_BE_USED.to_iced().into(), - border: Border { - color: NOT_INTENDED_TO_BE_USED.to_iced().into(), - ..Border::default() - }, - icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), - } - }, - TextInputStyle::FormInput => { - let theme = &self.form_input_text_field; +fn active(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { + match style { + TextInputStyle::ShouldNotBeUsed => { + Style { + background: NOT_INTENDED_TO_BE_USED.to_iced().into(), + border: Border { + color: NOT_INTENDED_TO_BE_USED.to_iced().into(), + ..Border::default() + }, + icon: NOT_INTENDED_TO_BE_USED.to_iced(), + placeholder: NOT_INTENDED_TO_BE_USED.to_iced(), + value: NOT_INTENDED_TO_BE_USED.to_iced(), + selection: NOT_INTENDED_TO_BE_USED.to_iced(), + } + }, + TextInputStyle::FormInput => { + let theme = &theme.form_input_text_field; - Appearance { - background: theme.background_color_hovered.to_iced().into(), - border: Border { - radius: theme.border_radius.into(), - width: theme.border_width, - color: theme.border_color_hovered.to_iced().into(), - }, - icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), - } - }, - TextInputStyle::MainSearch | TextInputStyle::PluginSearchBar => { - Appearance { - background: Color::TRANSPARENT.into(), - border: Border { - color: Color::TRANSPARENT, - ..Border::default() - }, - icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), - } - }, - } + Style { + background: theme.background_color.to_iced().into(), + border: Border { + radius: theme.border_radius.into(), + width: theme.border_width, + color: theme.border_color.to_iced().into(), + }, + icon: NOT_INTENDED_TO_BE_USED.to_iced(), + placeholder: theme.text_color_placeholder.to_iced(), + value: theme.text_color.to_iced(), + selection: theme.selection_color.to_iced(), + } + }, + TextInputStyle::MainSearch | TextInputStyle::PluginSearchBar => { + Style { + background: Color::TRANSPARENT.into(), + border: Border { + color: Color::TRANSPARENT, + ..Border::default() + }, + icon: NOT_INTENDED_TO_BE_USED.to_iced(), + placeholder: theme.form_input_text_field.text_color_placeholder.to_iced(), // TODO fix + value: theme.form_input_text_field.text_color.to_iced(), // TODO fix + selection: theme.form_input_text_field.selection_color.to_iced(), // TODO fix + } + }, } +} - fn disabled(&self, _: &Self::Style) -> Appearance { - Appearance { - background: NOT_INTENDED_TO_BE_USED.to_iced().into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: Color::TRANSPARENT, - }, - icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), - } +fn focused(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { + match style { + TextInputStyle::ShouldNotBeUsed => { + Style { + background: NOT_INTENDED_TO_BE_USED.to_iced().into(), + border: Border { + color: NOT_INTENDED_TO_BE_USED.to_iced().into(), + ..Border::default() + }, + icon: NOT_INTENDED_TO_BE_USED.to_iced(), + placeholder: NOT_INTENDED_TO_BE_USED.to_iced(), + value: NOT_INTENDED_TO_BE_USED.to_iced(), + selection: NOT_INTENDED_TO_BE_USED.to_iced(), + } + }, + TextInputStyle::FormInput => { + let theme = &theme.form_input_text_field; + + Style { + background: theme.background_color_hovered.to_iced().into(), + border: Border { + radius: theme.border_radius.into(), + width: theme.border_width, + color: theme.border_color_hovered.to_iced().into(), + }, + icon: NOT_INTENDED_TO_BE_USED.to_iced(), + placeholder: theme.text_color_placeholder.to_iced(), + value: theme.text_color.to_iced(), + selection: theme.selection_color.to_iced(), + } + }, + TextInputStyle::MainSearch | TextInputStyle::PluginSearchBar => { + Style { + background: Color::TRANSPARENT.into(), + border: Border { + color: Color::TRANSPARENT, + ..Border::default() + }, + icon: NOT_INTENDED_TO_BE_USED.to_iced(), + placeholder: theme.form_input_text_field.text_color_placeholder.to_iced(), // TODO fix + value: theme.form_input_text_field.text_color.to_iced(), // TODO fix + selection: theme.form_input_text_field.selection_color.to_iced(), // TODO fix + } + }, } +} - fn placeholder_color(&self, _: &Self::Style) -> Color { - self.form_input_text_field.text_color_placeholder.to_iced() - } - - fn value_color(&self, _: &Self::Style) -> Color { - self.form_input_text_field.text_color.to_iced() - } - - fn disabled_color(&self, style: &Self::Style) -> Color { - self.placeholder_color(style) - } - - fn selection_color(&self, _: &Self::Style) -> Color { - self.form_input_text_field.selection_color.to_iced() +fn disabled() -> Style { + Style { + background: NOT_INTENDED_TO_BE_USED.to_iced().into(), + border: Border { + radius: 2.0.into(), + width: 1.0, + color: Color::TRANSPARENT, + }, + icon: NOT_INTENDED_TO_BE_USED.to_iced(), + placeholder: NOT_INTENDED_TO_BE_USED.to_iced(), + value: NOT_INTENDED_TO_BE_USED.to_iced(), + selection: NOT_INTENDED_TO_BE_USED.to_iced(), } } @@ -129,12 +144,12 @@ impl<'a, Message: 'a + Clone> ThemableWidget<'a, Message> for TextInput<'a, Mess fn themed(self, kind: TextInputStyle) -> Element<'a, Message> { match kind { TextInputStyle::PluginSearchBar => { - self.style(kind) + self.class(kind) .padding(0) .into() } _ => { - self.style(kind) + self.class(kind) // .padding() // TODO .into() } diff --git a/rust/client/src/ui/theme/tooltip.rs b/rust/client/src/ui/theme/tooltip.rs index 3747d61..58f7f00 100644 --- a/rust/client/src/ui/theme/tooltip.rs +++ b/rust/client/src/ui/theme/tooltip.rs @@ -16,7 +16,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Tooltip<'a, Message, Gaunt match kind { TooltipStyle::Tooltip => { - self.style(ContainerStyleInner::Tooltip) + self.class(ContainerStyleInner::Tooltip) .padding(theme.tooltip.padding) } }.into() diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index fd5365d..e5a93b0 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1,21 +1,3 @@ -use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Icons, Image, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiWidgetId}; -use common_ui::shortcut_to_text; -use iced::alignment::{Horizontal, Vertical}; -use iced::font::Weight; -use iced::widget::image::Handle; -use iced::widget::text::Shaping; -use iced::widget::tooltip::Position; -use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, mouse_area, pick_list, row, scrollable, text, text_input, tooltip, vertical_rule, Space}; -use iced::{Alignment, Command, Font, Length}; -use iced_aw::core::icons; -use iced_aw::date_picker::Date; -use iced_aw::floating_element::Offset; -use iced_aw::helpers::{date_picker, grid, grid_row, wrap_horizontal}; -use iced_aw::{floating_element, GridRow}; -use itertools::Itertools; -use std::cell::Cell; -use std::collections::HashMap; -use std::fmt::{Debug, Display}; use crate::model::UiViewEvent; use crate::ui::custom_widgets::loading_bar::LoadingBar; use crate::ui::grid_navigation::{grid_down_offset, grid_up_offset, GridSectionData}; @@ -33,6 +15,23 @@ use crate::ui::theme::text_input::TextInputStyle; use crate::ui::theme::tooltip::TooltipStyle; use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::AppMsg; +use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Icons, Image, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiWidgetId}; +use common_ui::shortcut_to_text; +use iced::alignment::{Horizontal, Vertical}; +use iced::font::Weight; +use iced::widget::image::Handle; +use iced::widget::text::Shaping; +use iced::widget::tooltip::Position; +use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, mouse_area, pick_list, row, scrollable, stack, text, text_input, tooltip, value, vertical_rule, Space}; +use iced::{Alignment, Font, Length, Task}; +use iced_aw::date_picker::Date; +use iced_aw::helpers::{date_picker, grid, grid_row}; +use iced_aw::GridRow; +use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; +use itertools::Itertools; +use std::cell::Cell; +use std::collections::HashMap; +use std::fmt::{Debug, Display}; #[derive(Debug)] @@ -439,20 +438,20 @@ impl<'b> ComponentWidgets<'b> { amount_per_section } - pub fn append_text(&mut self, text: &str) -> Command { + pub fn append_text(&mut self, text: &str) -> Task { let Some(root_widget) = &self.root_widget else { - return Command::none(); + return Task::none(); }; let Some(content) = &root_widget.content else { - return Command::none(); + return Task::none(); }; let widget_id = match content { RootWidgetMembers::List(widget) => { match &widget.content.search_bar { None => { - return Command::none() + return Task::none() } Some(widget) => widget.__id__ } @@ -460,12 +459,12 @@ impl<'b> ComponentWidgets<'b> { RootWidgetMembers::Grid(widget) => { match &widget.content.search_bar { None => { - return Command::none() + return Task::none() } Some(widget) => widget.__id__ } } - _ => return Command::none() + _ => return Task::none() }; let TextFieldState { text_input_id, state_value } = ComponentWidgets::text_field_state_mut_on_state(&mut self.state, widget_id); @@ -475,24 +474,24 @@ impl<'b> ComponentWidgets<'b> { text_input::focus(text_input_id.clone()) } else { - Command::none() + Task::none() } } - pub fn backspace_text(&mut self) -> Command { + pub fn backspace_text(&mut self) -> Task { let Some(root_widget) = &self.root_widget else { - return Command::none(); + return Task::none(); }; let Some(content) = &root_widget.content else { - return Command::none(); + return Task::none(); }; let widget_id = match content { RootWidgetMembers::List(widget) => { match &widget.content.search_bar { None => { - return Command::none() + return Task::none() } Some(widget) => widget.__id__ } @@ -500,12 +499,12 @@ impl<'b> ComponentWidgets<'b> { RootWidgetMembers::Grid(widget) => { match &widget.content.search_bar { None => { - return Command::none() + return Task::none() } Some(widget) => widget.__id__ } } - _ => return Command::none() + _ => return Task::none() }; let TextFieldState { text_input_id, state_value } = ComponentWidgets::text_field_state_mut_on_state(&mut self.state, widget_id); @@ -551,45 +550,45 @@ impl<'b> ComponentWidgets<'b> { } } - pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Command { + pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Task { let TextFieldState { text_input_id, .. } = self.text_field_state(widget_id); text_input::focus(text_input_id.clone()) } - pub fn focus_up(&mut self) -> Command { + pub fn focus_up(&mut self) -> Task { let Some(root_widget) = &self.root_widget else { - return Command::none(); + return Task::none(); }; let Some(content) = &root_widget.content else { - return Command::none(); + return Task::none(); }; match content { - RootWidgetMembers::Detail(_) => Command::none(), - RootWidgetMembers::Form(_) => Command::none(), - RootWidgetMembers::Inline(_) => Command::none(), + RootWidgetMembers::Detail(_) => Task::none(), + RootWidgetMembers::Form(_) => Task::none(), + RootWidgetMembers::Inline(_) => Task::none(), RootWidgetMembers::List(widget) => { let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, widget.__id__); focused_item.focus_previous() - .unwrap_or_else(|| Command::none()) + .unwrap_or_else(|| Task::none()) } RootWidgetMembers::Grid(grid_widget) => { let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); let Some(current_index) = &focused_item.index else { - return Command::none(); + return Task::none(); }; let amount_per_section_total = Self::grid_section_sizes(grid_widget); match grid_up_offset(*current_index, amount_per_section_total) { - None => Command::none(), + None => Task::none(), Some(data) => { match focused_item.focus_previous_in(data.offset) { - None => Command::none(), + None => Task::none(), Some(_) => focused_item.scroll_to(data.row_index) } } @@ -598,19 +597,19 @@ impl<'b> ComponentWidgets<'b> { } } - pub fn focus_down(&mut self) -> Command { + pub fn focus_down(&mut self) -> Task { let Some(root_widget) = &self.root_widget else { - return Command::none(); + return Task::none(); }; let Some(content) = &root_widget.content else { - return Command::none(); + return Task::none(); }; match content { - RootWidgetMembers::Detail(_) => Command::none(), - RootWidgetMembers::Form(_) => Command::none(), - RootWidgetMembers::Inline(_) => Command::none(), + RootWidgetMembers::Detail(_) => Task::none(), + RootWidgetMembers::Form(_) => Task::none(), + RootWidgetMembers::Inline(_) => Task::none(), RootWidgetMembers::List(widget) => { let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, widget.__id__); @@ -634,7 +633,7 @@ impl<'b> ComponentWidgets<'b> { .count(); focused_item.focus_next(total) - .unwrap_or_else(|| Command::none()) + .unwrap_or_else(|| Task::none()) } RootWidgetMembers::Grid(grid_widget) => { let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); @@ -648,7 +647,7 @@ impl<'b> ComponentWidgets<'b> { let Some(current_index) = &focused_item.index else { let unfocus = match &grid_widget.content.search_bar { - None => Command::none(), + None => Task::none(), Some(_) => { // there doesn't seem to be an unfocus command but focusing non-existing input will unfocus all text_input::focus(text_input::Id::unique()) @@ -657,17 +656,17 @@ impl<'b> ComponentWidgets<'b> { let _ = focused_item.focus_next(total); - return Command::batch([ + return Task::batch([ unfocus, focused_item.scroll_to(0) ]) }; match grid_down_offset(*current_index, amount_per_section_total) { - None => Command::none(), + None => Task::none(), Some(data) => { match focused_item.focus_next_in(total, data.offset) { - None => Command::none(), + None => Task::none(), Some(_) => focused_item.scroll_to(data.row_index) } } @@ -676,20 +675,20 @@ impl<'b> ComponentWidgets<'b> { } } - pub fn focus_left(&mut self) -> Command { + pub fn focus_left(&mut self) -> Task { let Some(root_widget) = &self.root_widget else { - return Command::none(); + return Task::none(); }; let Some(content) = &root_widget.content else { - return Command::none(); + return Task::none(); }; match content { - RootWidgetMembers::Detail(_) => Command::none(), - RootWidgetMembers::Form(_) => Command::none(), - RootWidgetMembers::Inline(_) => Command::none(), - RootWidgetMembers::List(_) => Command::none(), + RootWidgetMembers::Detail(_) => Task::none(), + RootWidgetMembers::Form(_) => Task::none(), + RootWidgetMembers::Inline(_) => Task::none(), + RootWidgetMembers::List(_) => Task::none(), RootWidgetMembers::Grid(widget) => { let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, widget.__id__); @@ -697,25 +696,25 @@ impl<'b> ComponentWidgets<'b> { // focused_item.scroll_to(0) // TODO - Command::none() + Task::none() } } } - pub fn focus_right(&mut self) -> Command { + pub fn focus_right(&mut self) -> Task { let Some(root_widget) = &self.root_widget else { - return Command::none(); + return Task::none(); }; let Some(content) = &root_widget.content else { - return Command::none(); + return Task::none(); }; match content { - RootWidgetMembers::Detail(_) => Command::none(), - RootWidgetMembers::Form(_) => Command::none(), - RootWidgetMembers::Inline(_) => Command::none(), - RootWidgetMembers::List(_) => Command::none(), + RootWidgetMembers::Detail(_) => Task::none(), + RootWidgetMembers::Form(_) => Task::none(), + RootWidgetMembers::Inline(_) => Task::none(), + RootWidgetMembers::List(_) => Task::none(), RootWidgetMembers::Grid(grid_widget) => { let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); @@ -741,7 +740,7 @@ impl<'b> ComponentWidgets<'b> { let _ = focused_item.focus_next(total); // focused_item.scroll_to(0) - Command::none() + Task::none() } } } @@ -883,7 +882,7 @@ impl<'b> ComponentWidgets<'b> { } fn render_metadata_tag_list_widget<'a>(&self, widget: &MetadataTagListWidget) -> Element<'a, ComponentWidgetEvent> { - let content = widget.content.ordered_members + let content: Vec> = widget.content.ordered_members .iter() .map(|members| { match members { @@ -892,7 +891,8 @@ impl<'b> ComponentWidgets<'b> { }) .collect(); - let value = wrap_horizontal(content) + let value = row(content) + .wrap() .into(); render_metadata_item(&widget.label, value) @@ -902,8 +902,8 @@ impl<'b> ComponentWidgets<'b> { fn render_metadata_link_widget<'a>(&self, widget: &MetadataLinkWidget) -> Element<'a, ComponentWidgetEvent> { let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); - let icon: Element<_> = text(icons::Bootstrap::BoxArrowUpRight) - .font(icons::BOOTSTRAP_FONT) + let icon: Element<_> = value(Bootstrap::BoxArrowUpRight) + .font(BOOTSTRAP_FONT) .size(16) .into(); @@ -911,7 +911,7 @@ impl<'b> ComponentWidgets<'b> { .themed(ContainerStyle::MetadataLinkIcon); let content: Element<_> = row([content, icon]) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .into(); let link: Element<_> = button(content) @@ -921,7 +921,7 @@ impl<'b> ComponentWidgets<'b> { let content: Element<_> = if widget.href.is_empty() { link } else { - let href: Element<_> = text(&widget.href) + let href: Element<_> = text(widget.href.to_string()) .shaping(Shaping::Advanced) .into(); @@ -941,8 +941,8 @@ impl<'b> ComponentWidgets<'b> { } fn render_metadata_icon_widget<'a>(&self, widget: &MetadataIconWidget) -> Element<'a, ComponentWidgetEvent> { - let value = text(icon_to_bootstrap(&widget.icon)) - .font(icons::BOOTSTRAP_FONT) + let value = value(icon_to_bootstrap(&widget.icon)) + .font(BOOTSTRAP_FONT) .size(26) .into(); @@ -992,7 +992,7 @@ impl<'b> ComponentWidgets<'b> { .width(Length::Fill); if centered { - content = content.center_x() + content = content.align_x(Horizontal::Center) } content.themed(ContainerStyle::ContentParagraph) @@ -1006,7 +1006,7 @@ impl<'b> ComponentWidgets<'b> { .width(Length::Fill); if centered { - content = content.center_x() + content = content.align_x(Horizontal::Center) } content.themed(ContainerStyle::ContentImage) @@ -1082,8 +1082,8 @@ impl<'b> ComponentWidgets<'b> { container(content) .width(Length::Fill) .height(Length::Fill) - .center_x() - .center_y() + .align_x(Horizontal::Center) + .align_y(Vertical::Center) .into() } else { content @@ -1268,9 +1268,9 @@ impl<'b> ComponentWidgets<'b> { .into() } Some(label) => { - let label: Element<_> = text(label) + let label: Element<_> = text(label.to_string()) .shaping(Shaping::Advanced) - .horizontal_alignment(Horizontal::Right) + .align_x(Horizontal::Right) .width(Length::Fill) .into(); @@ -1294,7 +1294,7 @@ impl<'b> ComponentWidgets<'b> { ]; let row: Element<_> = row(content) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .themed(RowStyle::FormInput); row @@ -1347,11 +1347,11 @@ impl<'b> ComponentWidgets<'b> { .into(); let top_rule = container(top_rule) - .center_x() + .align_x(Horizontal::Center) .into(); - let icon = text(icon_to_bootstrap(icon)) - .font(icons::BOOTSTRAP_FONT) + let icon = value(icon_to_bootstrap(icon)) + .font(BOOTSTRAP_FONT) .size(45) .themed(TextStyle::InlineSeparator); @@ -1359,11 +1359,11 @@ impl<'b> ComponentWidgets<'b> { .into(); let bot_rule = container(bot_rule) - .center_x() + .align_x(Horizontal::Center) .into(); column([top_rule, icon, bot_rule]) - .align_items(Alignment::Center) + .align_x(Alignment::Center) .into() } } @@ -1415,14 +1415,14 @@ impl<'b> ComponentWidgets<'b> { .as_ref() .map(|image| self.render_image(widget.__id__, image, Some(TextStyle::EmptyViewSubtitle))); - let title: Element<_> = text(&widget.title) + let title: Element<_> = text(widget.title.to_string()) .shaping(Shaping::Advanced) .into(); let subtitle: Element<_> = match &widget.description { None => horizontal_space().into(), Some(subtitle) => { - text(subtitle) + text(subtitle.to_string()) .shaping(Shaping::Advanced) .themed(TextStyle::EmptyViewSubtitle) } @@ -1437,14 +1437,14 @@ impl<'b> ComponentWidgets<'b> { } let content: Element<_> = column(content) - .align_items(Alignment::Center) + .align_x(Alignment::Center) .into(); container(content) .width(Length::Fill) .height(Length::Fill) - .center_x() - .center_y() + .align_x(Horizontal::Center) + .align_y(Vertical::Center) .into() } @@ -1464,14 +1464,14 @@ impl<'b> ComponentWidgets<'b> { let icon = self.render_image(widget.__id__, &widget.icon, Some(TextStyle::IconAccessory)); let content = container(icon) - .center_x() - .center_y() + .align_x(Horizontal::Center) + .align_y(Vertical::Center) .themed(ContainerStyle::IconAccessory); match widget.tooltip.as_ref() { None => content, Some(tooltip_text) => { - let tooltip_text: Element<_> = text(tooltip_text) + let tooltip_text: Element<_> = text(tooltip_text.to_string()) .shaping(Shaping::Advanced) .into(); @@ -1486,7 +1486,7 @@ impl<'b> ComponentWidgets<'b> { .as_ref() .map(|icon| self.render_image(widget.__id__, icon, Some(TextStyle::TextAccessory))); - let text_content: Element<_> = text(&widget.text) + let text_content: Element<_> = text(widget.text.to_string()) .shaping(Shaping::Advanced) .themed(TextStyle::TextAccessory); @@ -1502,18 +1502,18 @@ impl<'b> ComponentWidgets<'b> { content.push(text_content); let content: Element<_> = row(content) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .into(); let content = container(content) - .center_x() - .center_y() + .align_x(Horizontal::Center) + .align_y(Vertical::Center) .themed(ContainerStyle::TextAccessory); match widget.tooltip.as_ref() { None => content, Some(tooltip_text) => { - let tooltip_text: Element<_> = text(tooltip_text) + let tooltip_text: Element<_> = text(tooltip_text.to_string()) .shaping(Shaping::Advanced) .into(); @@ -1672,7 +1672,7 @@ impl<'b> ComponentWidgets<'b> { .as_ref() .map(|icon| self.render_image(widget.__id__, icon, None)); - let title: Element<_> = text(&widget.title) + let title: Element<_> = text(widget.title.to_string()) .shaping(Shaping::Advanced) .into(); let title: Element<_> = container(title) @@ -1688,7 +1688,7 @@ impl<'b> ComponentWidgets<'b> { } if let Some(subtitle) = &widget.subtitle { - let subtitle: Element<_> = text(subtitle) + let subtitle: Element<_> = text(subtitle.to_string()) .shaping(Shaping::Advanced) .themed(TextStyle::ListItemSubtitle); let subtitle: Element<_> = container(subtitle) @@ -1719,7 +1719,7 @@ impl<'b> ComponentWidgets<'b> { } let content: Element<_> = row(content) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .into(); let style = match item_focus_index { @@ -1878,7 +1878,7 @@ impl<'b> ComponentWidgets<'b> { if let Some(title) = &widget.title { // TODO text truncation when iced supports it - let title = text(title) + let title = text(title.to_string()) .shaping(Shaping::Advanced) .themed(TextStyle::GridItemTitle); @@ -1886,7 +1886,7 @@ impl<'b> ComponentWidgets<'b> { } if let Some(subtitle) = &widget.subtitle { - let subtitle = text(subtitle) + let subtitle = text(subtitle.to_string()) .shaping(Shaping::Advanced) .themed(TextStyle::GridItemSubTitle); @@ -1961,8 +1961,8 @@ impl<'b> ComponentWidgets<'b> { } fn render_top_panel<'a>(&self, search_bar: &Option) -> Element<'a, ComponentWidgetEvent> { - let icon = text(icons::Bootstrap::ArrowLeft) - .font(icons::BOOTSTRAP_FONT); + let icon = value(Bootstrap::ArrowLeft) + .font(BOOTSTRAP_FONT); let back_button: Element<_> = button(icon) .on_press(ComponentWidgetEvent::PreviousView) @@ -1974,7 +1974,7 @@ impl<'b> ComponentWidgets<'b> { .unwrap_or_else(|| Space::with_width(Length::FillPortion(3)).into()); let top_panel: Element<_> = row(vec![back_button, search_bar_element]) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .themed(RowStyle::RootTopPanel); let top_panel: Element<_> = container(top_panel) @@ -2067,7 +2067,7 @@ impl<'b> ComponentWidgets<'b> { Image::ImageSource(_) => { match self.images.get(&widget_id) { Some(bytes) => { - image(Handle::from_memory(bytes.clone())) + image(Handle::from_bytes(bytes.clone())) .into() } None => { @@ -2079,13 +2079,13 @@ impl<'b> ComponentWidgets<'b> { Image::Icons(icon) => { match icon_style { None => { - text(icon_to_bootstrap(icon)) - .font(icons::BOOTSTRAP_FONT) + value(icon_to_bootstrap(icon)) + .font(BOOTSTRAP_FONT) .into() } Some(icon_style) => { - text(icon_to_bootstrap(icon)) - .font(icons::BOOTSTRAP_FONT) + value(icon_to_bootstrap(icon)) + .font(BOOTSTRAP_FONT) .themed(icon_style) } } @@ -2109,7 +2109,7 @@ impl Display for SelectItem { fn render_metadata_item<'a>(label: &str, value: Element<'a, ComponentWidgetEvent>) -> Element<'a, ComponentWidgetEvent> { - let label: Element<_> = text(label) + let label: Element<_> = text(label.to_string()) .shaping(Shaping::Advanced) .themed(TextStyle::MetadataItemLabel); @@ -2132,7 +2132,7 @@ fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option< let mut title_content = vec![]; if let Some(title) = title { - let title: Element<_> = text(title) + let title: Element<_> = text(title.to_string()) .shaping(Shaping::Advanced) .size(15) .themed(theme_kind_title_text); @@ -2141,7 +2141,7 @@ fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option< } if let Some(subtitle) = subtitle { - let subtitle: Element<_> = text(subtitle) + let subtitle: Element<_> = text(subtitle.to_string()) .shaping(Shaping::Advanced) .size(15) .themed(theme_kind_subtitle_text); @@ -2340,7 +2340,7 @@ fn render_action_panel_items<'a, T: 'a + Clone>( .into(); row([text, space, shortcut_element]) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .into() } else { text(label) @@ -2422,7 +2422,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( on_action_click: impl Fn(UiWidgetId) -> T, noop_msg: impl Fn() -> T, ) -> Element<'a, T> { - let entrypoint_name: Element<_> = text(entrypoint_name) + let entrypoint_name: Element<_> = text(entrypoint_name.to_string()) .shaping(Shaping::Advanced) .into(); @@ -2473,7 +2473,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( let mut bottom_panel_content = vec![entrypoint_name]; if let Some(toast_text) = toast_text { - let toast_text = text(toast_text) + let toast_text = text(toast_text.to_string()) .into(); bottom_panel_content.push(toast_text); @@ -2488,7 +2488,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( bottom_panel_content.push(primary_action); let rule: Element<_> = vertical_rule(1) - .style(RuleStyle::PrimaryActionSeparator) + .class(RuleStyle::PrimaryActionSeparator) .into(); let rule: Element<_> = container(rule) @@ -2510,7 +2510,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( bottom_panel_content.push(action_panel_toggle); let bottom_panel: Element<_> = row(bottom_panel_content) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .themed(RowStyle::RootBottomPanel); (!show_action_panel, Some(action_panel), bottom_panel) @@ -2522,7 +2522,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( let mut bottom_panel_content = vec![]; if let Some(toast_text) = toast_text { - let toast_text = text(toast_text) + let toast_text = text(toast_text.to_string()) .into(); bottom_panel_content.push(toast_text); @@ -2537,7 +2537,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( } let bottom_panel: Element<_> = row(bottom_panel_content) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .themed(RowStyle::RootBottomPanel); (true, None, bottom_panel) @@ -2560,14 +2560,23 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( .on_press(if hide_action_panel { noop_msg() } else { on_panel_toggle_click() }) .into(); - let action_panel_element = match (action_panel, action_panel_scroll_handle) { - (Some(action_panel), Some(action_panel_scroll_handle)) => render_action_panel(action_panel, on_action_click, action_panel_scroll_handle), - _ => Space::with_height(1).into(), + let mut content = vec![content]; + + if let (Some(action_panel), Some(action_panel_scroll_handle)) = (action_panel, action_panel_scroll_handle) { + if !hide_action_panel { + let action_panel = render_action_panel(action_panel, on_action_click, action_panel_scroll_handle); + + let action_panel: Element<_>= container(action_panel) + .padding(common_ui::padding(0.0, 8.0, 48.0, 0.0)) + .align_right(Length::Fill) + .align_bottom(Length::Fill) + .into(); + + content.push(action_panel); + } }; - floating_element(content, action_panel_element) - .offset(Offset::from([8.0, 48.0])) // TODO calculate based on theme - .hide(hide_action_panel) + stack(content) .into() } @@ -2851,227 +2860,227 @@ pub fn parse_date(value: &str) -> Option<(i32, u32, u32)> { } } -fn icon_to_bootstrap(icon: &Icons) -> icons::Bootstrap { +fn icon_to_bootstrap(icon: &Icons) -> Bootstrap { match icon { - Icons::Airplane => icons::Bootstrap::Airplane, - Icons::Alarm => icons::Bootstrap::Alarm, - Icons::AlignCentre => icons::Bootstrap::AlignCenter, - Icons::AlignLeft => icons::Bootstrap::AlignStart, - Icons::AlignRight => icons::Bootstrap::AlignEnd, - // Icons::Anchor => icons::Bootstrap::, - Icons::ArrowClockwise => icons::Bootstrap::ArrowClockwise, - Icons::ArrowCounterClockwise => icons::Bootstrap::ArrowCounterclockwise, - Icons::ArrowDown => icons::Bootstrap::ArrowDown, - Icons::ArrowLeft => icons::Bootstrap::ArrowLeft, - Icons::ArrowRight => icons::Bootstrap::ArrowRight, - Icons::ArrowUp => icons::Bootstrap::ArrowUp, - Icons::ArrowLeftRight => icons::Bootstrap::ArrowLeftRight, - Icons::ArrowsContract => icons::Bootstrap::ArrowsAngleContract, - Icons::ArrowsExpand => icons::Bootstrap::ArrowsAngleExpand, - Icons::AtSymbol => icons::Bootstrap::At, - // Icons::BandAid => icons::Bootstrap::Bandaid, - Icons::Cash => icons::Bootstrap::Cash, - // Icons::BarChart => icons::Bootstrap::BarChart, - // Icons::BarCode => icons::Bootstrap::, - Icons::Battery => icons::Bootstrap::Battery, - Icons::BatteryCharging => icons::Bootstrap::BatteryCharging, - // Icons::BatteryDisabled => icons::Bootstrap::, - Icons::Bell => icons::Bootstrap::Bell, - Icons::BellDisabled => icons::Bootstrap::BellSlash, - // Icons::Bike => icons::Bootstrap::Bicycle, - // Icons::Binoculars => icons::Bootstrap::Binoculars, - // Icons::Bird => icons::Bootstrap::, - Icons::Bluetooth => icons::Bootstrap::Bluetooth, - // Icons::Boat => icons::Bootstrap::, - Icons::Bold => icons::Bootstrap::TypeBold, - // Icons::Bolt => icons::Bootstrap::, - // Icons::BoltDisabled => icons::Bootstrap::, - Icons::Book => icons::Bootstrap::Book, - Icons::Bookmark => icons::Bootstrap::Bookmark, - Icons::Box => icons::Bootstrap::Box, - // Icons::Brush => icons::Bootstrap::Brush, - Icons::Bug => icons::Bootstrap::Bug, - Icons::Building => icons::Bootstrap::Building, - Icons::BulletPoints => icons::Bootstrap::ListUl, - Icons::Calculator => icons::Bootstrap::Calculator, - Icons::Calendar => icons::Bootstrap::Calendar, - Icons::Camera => icons::Bootstrap::Camera, - Icons::Car => icons::Bootstrap::CarFront, - Icons::Cart => icons::Bootstrap::Cart, - // Icons::Cd => icons::Bootstrap::, - // Icons::Center => icons::Bootstrap::, - Icons::Checkmark => icons::Bootstrap::Checktwo, - // Icons::ChessPiece => icons::Bootstrap::, - Icons::ChevronDown => icons::Bootstrap::ChevronDown, - Icons::ChevronLeft => icons::Bootstrap::ChevronLeft, - Icons::ChevronRight => icons::Bootstrap::ChevronRight, - Icons::ChevronUp => icons::Bootstrap::ChevronUp, - Icons::ChevronExpand => icons::Bootstrap::ChevronExpand, - Icons::Circle => icons::Bootstrap::Circle, - // Icons::CircleProgress100 => icons::Bootstrap::, - // Icons::CircleProgress25 => icons::Bootstrap::, - // Icons::CircleProgress50 => icons::Bootstrap::, - // Icons::CircleProgress75 => icons::Bootstrap::, - // Icons::ClearFormatting => icons::Bootstrap::, - Icons::Clipboard => icons::Bootstrap::Clipboard, - Icons::Clock => icons::Bootstrap::Clock, - Icons::Cloud => icons::Bootstrap::Cloud, - Icons::CloudLightning => icons::Bootstrap::CloudLightning, - Icons::CloudRain => icons::Bootstrap::CloudRain, - Icons::CloudSnow => icons::Bootstrap::CloudSnow, - Icons::CloudSun => icons::Bootstrap::CloudSun, - Icons::Code => icons::Bootstrap::Code, - Icons::Gear => icons::Bootstrap::Gear, - Icons::Coin => icons::Bootstrap::Coin, - Icons::Command => icons::Bootstrap::Command, - Icons::Compass => icons::Bootstrap::Compass, - // Icons::ComputerChip => icons::Bootstrap::, - // Icons::Contrast => icons::Bootstrap::, - Icons::CreditCard => icons::Bootstrap::CreditCard, - Icons::Crop => icons::Bootstrap::Crop, - // Icons::Crown => icons::Bootstrap::, - Icons::Document => icons::Bootstrap::FileEarmark, - Icons::DocumentAdd => icons::Bootstrap::FileEarmarkPlus, - Icons::DocumentDelete => icons::Bootstrap::FileEarmarkX, - Icons::Dot => icons::Bootstrap::Dot, - Icons::Download => icons::Bootstrap::Download, - // Icons::Duplicate => icons::Bootstrap::, - Icons::Eject => icons::Bootstrap::Eject, - Icons::ThreeDots => icons::Bootstrap::ThreeDots, - Icons::Envelope => icons::Bootstrap::Envelope, - Icons::Eraser => icons::Bootstrap::Eraser, - Icons::ExclamationMark => icons::Bootstrap::ExclamationLg, - Icons::Eye => icons::Bootstrap::Eye, - Icons::EyeDisabled => icons::Bootstrap::EyeSlash, - Icons::EyeDropper => icons::Bootstrap::Eyedropper, - Icons::Female => icons::Bootstrap::GenderFemale, - Icons::Film => icons::Bootstrap::Film, - Icons::Filter => icons::Bootstrap::Filter, - Icons::Fingerprint => icons::Bootstrap::Fingerprint, - Icons::Flag => icons::Bootstrap::Flag, - Icons::Folder => icons::Bootstrap::Folder, - Icons::FolderAdd => icons::Bootstrap::FolderPlus, - Icons::FolderDelete => icons::Bootstrap::FolderMinus, - Icons::Forward => icons::Bootstrap::Forward, - Icons::GameController => icons::Bootstrap::Controller, - Icons::Virus => icons::Bootstrap::Virus, - Icons::Gift => icons::Bootstrap::Gift, - Icons::Glasses => icons::Bootstrap::Eyeglasses, - Icons::Globe => icons::Bootstrap::Globe, - Icons::Hammer => icons::Bootstrap::Hammer, - Icons::HardDrive => icons::Bootstrap::DeviceHdd, - Icons::Headphones => icons::Bootstrap::Headphones, - Icons::Heart => icons::Bootstrap::Heart, - // Icons::HeartDisabled => icons::Bootstrap::, - Icons::Heartbeat => icons::Bootstrap::Activity, - Icons::Hourglass => icons::Bootstrap::Hourglass, - Icons::House => icons::Bootstrap::House, - Icons::Image => icons::Bootstrap::Image, - Icons::Info => icons::Bootstrap::InfoLg, - Icons::Italics => icons::Bootstrap::TypeItalic, - Icons::Key => icons::Bootstrap::Key, - Icons::Keyboard => icons::Bootstrap::Keyboard, - Icons::Layers => icons::Bootstrap::Layers, - // Icons::Leaf => icons::Bootstrap::, - Icons::LightBulb => icons::Bootstrap::Lightbulb, - Icons::LightBulbDisabled => icons::Bootstrap::LightbulbOff, - Icons::Link => icons::Bootstrap::LinkFourfivedeg, - Icons::List => icons::Bootstrap::List, - Icons::Lock => icons::Bootstrap::Lock, - // Icons::LockDisabled => icons::Bootstrap::, - Icons::LockUnlocked => icons::Bootstrap::Unlock, - // Icons::Logout => icons::Bootstrap::, - // Icons::Lowercase => icons::Bootstrap::, - // Icons::MagnifyingGlass => icons::Bootstrap::, - Icons::Male => icons::Bootstrap::GenderMale, - Icons::Map => icons::Bootstrap::Map, - Icons::Maximize => icons::Bootstrap::Fullscreen, - Icons::Megaphone => icons::Bootstrap::Megaphone, - Icons::MemoryModule => icons::Bootstrap::Memory, - Icons::MemoryStick => icons::Bootstrap::UsbDrive, - Icons::Message => icons::Bootstrap::Chat, - Icons::Microphone => icons::Bootstrap::Mic, - Icons::MicrophoneDisabled => icons::Bootstrap::MicMute, - Icons::Minimize => icons::Bootstrap::FullscreenExit, - Icons::Minus => icons::Bootstrap::Dash, - Icons::Mobile => icons::Bootstrap::Phone, - // Icons::Monitor => icons::Bootstrap::, - Icons::Moon => icons::Bootstrap::Moon, - // Icons::Mountain => icons::Bootstrap::, - Icons::Mouse => icons::Bootstrap::Mouse, - Icons::Multiply => icons::Bootstrap::X, - Icons::Music => icons::Bootstrap::MusicNoteBeamed, - Icons::Network => icons::Bootstrap::BroadcastPin, - Icons::Paperclip => icons::Bootstrap::Paperclip, - Icons::Paragraph => icons::Bootstrap::TextParagraph, - Icons::Pause => icons::Bootstrap::Pause, - Icons::Pencil => icons::Bootstrap::Pencil, - Icons::Person => icons::Bootstrap::Person, - Icons::PersonAdd => icons::Bootstrap::PersonAdd, - Icons::PersonRemove => icons::Bootstrap::PersonDash, - Icons::Phone => icons::Bootstrap::Telephone, - // Icons::PhoneRinging => icons::Bootstrap::, - Icons::PieChart => icons::Bootstrap::PieChart, - Icons::Capsule => icons::Bootstrap::Capsule, - // Icons::Pin => icons::Bootstrap::, - // Icons::PinDisabled => icons::Bootstrap::, - Icons::Play => icons::Bootstrap::Play, - Icons::Plug => icons::Bootstrap::Plug, - Icons::Plus => icons::Bootstrap::Plus, - // Icons::PlusMinusDivideMultiply => icons::Bootstrap::, - Icons::Power => icons::Bootstrap::Power, - Icons::Printer => icons::Bootstrap::Printer, - Icons::QuestionMark => icons::Bootstrap::QuestionLg, - Icons::Quotes => icons::Bootstrap::Quote, - Icons::Receipt => icons::Bootstrap::Receipt, - Icons::Repeat => icons::Bootstrap::Repeat, - Icons::Reply => icons::Bootstrap::Reply, - Icons::Rewind => icons::Bootstrap::Rewind, - Icons::Rocket => icons::Bootstrap::Rocket, - // Icons::Ruler => icons::Bootstrap::, - Icons::Shield => icons::Bootstrap::Shield, - Icons::Shuffle => icons::Bootstrap::Shuffle, - Icons::Snippets => icons::Bootstrap::BodyText, - Icons::Snowflake => icons::Bootstrap::Snow, - // Icons::VolumeHigh => icons::Bootstrap::VolumeUp, - // Icons::VolumeLow => icons::Bootstrap::VolumeDown, - // Icons::VolumeOff => icons::Bootstrap::VolumeOff, - // Icons::VolumeOn => icons::Bootstrap::, - Icons::Star => icons::Bootstrap::Star, - // Icons::StarDisabled => icons::Bootstrap::, - Icons::Stop => icons::Bootstrap::Stop, - Icons::Stopwatch => icons::Bootstrap::Stopwatch, - Icons::StrikeThrough => icons::Bootstrap::TypeStrikethrough, - Icons::Sun => icons::Bootstrap::SunFill, // TODO why Sun isn't in iced_aw? - Icons::Scissors => icons::Bootstrap::Scissors, - // Icons::Syringe => icons::Bootstrap::, - Icons::Tag => icons::Bootstrap::Tag, - Icons::Thermometer => icons::Bootstrap::Thermometer, - Icons::Terminal => icons::Bootstrap::Terminal, - Icons::Text => icons::Bootstrap::Fonts, - Icons::TextCursor => icons::Bootstrap::CursorText, - // Icons::TextSelection => icons::Bootstrap::, - // Icons::Torch => icons::Bootstrap::, - // Icons::Train => icons::Bootstrap::, - Icons::Trash => icons::Bootstrap::Trash, - Icons::Tree => icons::Bootstrap::Tree, - Icons::Trophy => icons::Bootstrap::Trophy, - Icons::People => icons::Bootstrap::People, - Icons::Umbrella => icons::Bootstrap::Umbrella, - Icons::Underline => icons::Bootstrap::TypeUnderline, - Icons::Upload => icons::Bootstrap::Upload, - // Icons::Uppercase => icons::Bootstrap::, - Icons::Wallet => icons::Bootstrap::Wallet, - Icons::Wand => icons::Bootstrap::Magic, - // Icons::Warning => icons::Bootstrap::, - // Icons::Weights => icons::Bootstrap::, - Icons::Wifi => icons::Bootstrap::Wifi, - Icons::WifiDisabled => icons::Bootstrap::WifiOff, - Icons::Window => icons::Bootstrap::Window, - Icons::Tools => icons::Bootstrap::Tools, - Icons::Watch => icons::Bootstrap::Watch, - Icons::XMark => icons::Bootstrap::XLg, - Icons::Indent => icons::Bootstrap::Indent, - Icons::Unindent => icons::Bootstrap::Unindent, + Icons::Airplane => Bootstrap::Airplane, + Icons::Alarm => Bootstrap::Alarm, + Icons::AlignCentre => Bootstrap::AlignCenter, + Icons::AlignLeft => Bootstrap::AlignStart, + Icons::AlignRight => Bootstrap::AlignEnd, + // Icons::Anchor => Bootstrap::, + Icons::ArrowClockwise => Bootstrap::ArrowClockwise, + Icons::ArrowCounterClockwise => Bootstrap::ArrowCounterclockwise, + Icons::ArrowDown => Bootstrap::ArrowDown, + Icons::ArrowLeft => Bootstrap::ArrowLeft, + Icons::ArrowRight => Bootstrap::ArrowRight, + Icons::ArrowUp => Bootstrap::ArrowUp, + Icons::ArrowLeftRight => Bootstrap::ArrowLeftRight, + Icons::ArrowsContract => Bootstrap::ArrowsAngleContract, + Icons::ArrowsExpand => Bootstrap::ArrowsAngleExpand, + Icons::AtSymbol => Bootstrap::At, + // Icons::BandAid => Bootstrap::Bandaid, + Icons::Cash => Bootstrap::Cash, + // Icons::BarChart => Bootstrap::BarChart, + // Icons::BarCode => Bootstrap::, + Icons::Battery => Bootstrap::Battery, + Icons::BatteryCharging => Bootstrap::BatteryCharging, + // Icons::BatteryDisabled => Bootstrap::, + Icons::Bell => Bootstrap::Bell, + Icons::BellDisabled => Bootstrap::BellSlash, + // Icons::Bike => Bootstrap::Bicycle, + // Icons::Binoculars => Bootstrap::Binoculars, + // Icons::Bird => Bootstrap::, + Icons::Bluetooth => Bootstrap::Bluetooth, + // Icons::Boat => Bootstrap::, + Icons::Bold => Bootstrap::TypeBold, + // Icons::Bolt => Bootstrap::, + // Icons::BoltDisabled => Bootstrap::, + Icons::Book => Bootstrap::Book, + Icons::Bookmark => Bootstrap::Bookmark, + Icons::Box => Bootstrap::Box, + // Icons::Brush => Bootstrap::Brush, + Icons::Bug => Bootstrap::Bug, + Icons::Building => Bootstrap::Building, + Icons::BulletPoints => Bootstrap::ListUl, + Icons::Calculator => Bootstrap::Calculator, + Icons::Calendar => Bootstrap::Calendar, + Icons::Camera => Bootstrap::Camera, + Icons::Car => Bootstrap::CarFront, + Icons::Cart => Bootstrap::Cart, + // Icons::Cd => Bootstrap::, + // Icons::Center => Bootstrap::, + Icons::Checkmark => Bootstrap::Checktwo, + // Icons::ChessPiece => Bootstrap::, + Icons::ChevronDown => Bootstrap::ChevronDown, + Icons::ChevronLeft => Bootstrap::ChevronLeft, + Icons::ChevronRight => Bootstrap::ChevronRight, + Icons::ChevronUp => Bootstrap::ChevronUp, + Icons::ChevronExpand => Bootstrap::ChevronExpand, + Icons::Circle => Bootstrap::Circle, + // Icons::CircleProgress100 => Bootstrap::, + // Icons::CircleProgress25 => Bootstrap::, + // Icons::CircleProgress50 => Bootstrap::, + // Icons::CircleProgress75 => Bootstrap::, + // Icons::ClearFormatting => Bootstrap::, + Icons::Clipboard => Bootstrap::Clipboard, + Icons::Clock => Bootstrap::Clock, + Icons::Cloud => Bootstrap::Cloud, + Icons::CloudLightning => Bootstrap::CloudLightning, + Icons::CloudRain => Bootstrap::CloudRain, + Icons::CloudSnow => Bootstrap::CloudSnow, + Icons::CloudSun => Bootstrap::CloudSun, + Icons::Code => Bootstrap::Code, + Icons::Gear => Bootstrap::Gear, + Icons::Coin => Bootstrap::Coin, + Icons::Command => Bootstrap::Command, + Icons::Compass => Bootstrap::Compass, + // Icons::ComputerChip => Bootstrap::, + // Icons::Contrast => Bootstrap::, + Icons::CreditCard => Bootstrap::CreditCard, + Icons::Crop => Bootstrap::Crop, + // Icons::Crown => Bootstrap::, + Icons::Document => Bootstrap::FileEarmark, + Icons::DocumentAdd => Bootstrap::FileEarmarkPlus, + Icons::DocumentDelete => Bootstrap::FileEarmarkX, + Icons::Dot => Bootstrap::Dot, + Icons::Download => Bootstrap::Download, + // Icons::Duplicate => Bootstrap::, + Icons::Eject => Bootstrap::Eject, + Icons::ThreeDots => Bootstrap::ThreeDots, + Icons::Envelope => Bootstrap::Envelope, + Icons::Eraser => Bootstrap::Eraser, + Icons::ExclamationMark => Bootstrap::ExclamationLg, + Icons::Eye => Bootstrap::Eye, + Icons::EyeDisabled => Bootstrap::EyeSlash, + Icons::EyeDropper => Bootstrap::Eyedropper, + Icons::Female => Bootstrap::GenderFemale, + Icons::Film => Bootstrap::Film, + Icons::Filter => Bootstrap::Filter, + Icons::Fingerprint => Bootstrap::Fingerprint, + Icons::Flag => Bootstrap::Flag, + Icons::Folder => Bootstrap::Folder, + Icons::FolderAdd => Bootstrap::FolderPlus, + Icons::FolderDelete => Bootstrap::FolderMinus, + Icons::Forward => Bootstrap::Forward, + Icons::GameController => Bootstrap::Controller, + Icons::Virus => Bootstrap::Virus, + Icons::Gift => Bootstrap::Gift, + Icons::Glasses => Bootstrap::Eyeglasses, + Icons::Globe => Bootstrap::Globe, + Icons::Hammer => Bootstrap::Hammer, + Icons::HardDrive => Bootstrap::DeviceHdd, + Icons::Headphones => Bootstrap::Headphones, + Icons::Heart => Bootstrap::Heart, + // Icons::HeartDisabled => Bootstrap::, + Icons::Heartbeat => Bootstrap::Activity, + Icons::Hourglass => Bootstrap::Hourglass, + Icons::House => Bootstrap::House, + Icons::Image => Bootstrap::Image, + Icons::Info => Bootstrap::InfoLg, + Icons::Italics => Bootstrap::TypeItalic, + Icons::Key => Bootstrap::Key, + Icons::Keyboard => Bootstrap::Keyboard, + Icons::Layers => Bootstrap::Layers, + // Icons::Leaf => Bootstrap::, + Icons::LightBulb => Bootstrap::Lightbulb, + Icons::LightBulbDisabled => Bootstrap::LightbulbOff, + Icons::Link => Bootstrap::LinkFourfivedeg, + Icons::List => Bootstrap::List, + Icons::Lock => Bootstrap::Lock, + // Icons::LockDisabled => Bootstrap::, + Icons::LockUnlocked => Bootstrap::Unlock, + // Icons::Logout => Bootstrap::, + // Icons::Lowercase => Bootstrap::, + // Icons::MagnifyingGlass => Bootstrap::, + Icons::Male => Bootstrap::GenderMale, + Icons::Map => Bootstrap::Map, + Icons::Maximize => Bootstrap::Fullscreen, + Icons::Megaphone => Bootstrap::Megaphone, + Icons::MemoryModule => Bootstrap::Memory, + Icons::MemoryStick => Bootstrap::UsbDrive, + Icons::Message => Bootstrap::Chat, + Icons::Microphone => Bootstrap::Mic, + Icons::MicrophoneDisabled => Bootstrap::MicMute, + Icons::Minimize => Bootstrap::FullscreenExit, + Icons::Minus => Bootstrap::Dash, + Icons::Mobile => Bootstrap::Phone, + // Icons::Monitor => Bootstrap::, + Icons::Moon => Bootstrap::Moon, + // Icons::Mountain => Bootstrap::, + Icons::Mouse => Bootstrap::Mouse, + Icons::Multiply => Bootstrap::X, + Icons::Music => Bootstrap::MusicNoteBeamed, + Icons::Network => Bootstrap::BroadcastPin, + Icons::Paperclip => Bootstrap::Paperclip, + Icons::Paragraph => Bootstrap::TextParagraph, + Icons::Pause => Bootstrap::Pause, + Icons::Pencil => Bootstrap::Pencil, + Icons::Person => Bootstrap::Person, + Icons::PersonAdd => Bootstrap::PersonAdd, + Icons::PersonRemove => Bootstrap::PersonDash, + Icons::Phone => Bootstrap::Telephone, + // Icons::PhoneRinging => Bootstrap::, + Icons::PieChart => Bootstrap::PieChart, + Icons::Capsule => Bootstrap::Capsule, + // Icons::Pin => Bootstrap::, + // Icons::PinDisabled => Bootstrap::, + Icons::Play => Bootstrap::Play, + Icons::Plug => Bootstrap::Plug, + Icons::Plus => Bootstrap::Plus, + // Icons::PlusMinusDivideMultiply => Bootstrap::, + Icons::Power => Bootstrap::Power, + Icons::Printer => Bootstrap::Printer, + Icons::QuestionMark => Bootstrap::QuestionLg, + Icons::Quotes => Bootstrap::Quote, + Icons::Receipt => Bootstrap::Receipt, + Icons::Repeat => Bootstrap::Repeat, + Icons::Reply => Bootstrap::Reply, + Icons::Rewind => Bootstrap::Rewind, + Icons::Rocket => Bootstrap::Rocket, + // Icons::Ruler => Bootstrap::, + Icons::Shield => Bootstrap::Shield, + Icons::Shuffle => Bootstrap::Shuffle, + Icons::Snippets => Bootstrap::BodyText, + Icons::Snowflake => Bootstrap::Snow, + // Icons::VolumeHigh => Bootstrap::VolumeUp, + // Icons::VolumeLow => Bootstrap::VolumeDown, + // Icons::VolumeOff => Bootstrap::VolumeOff, + // Icons::VolumeOn => Bootstrap::, + Icons::Star => Bootstrap::Star, + // Icons::StarDisabled => Bootstrap::, + Icons::Stop => Bootstrap::Stop, + Icons::Stopwatch => Bootstrap::Stopwatch, + Icons::StrikeThrough => Bootstrap::TypeStrikethrough, + Icons::Sun => Bootstrap::SunFill, // TODO why Sun isn't in iced_aw? + Icons::Scissors => Bootstrap::Scissors, + // Icons::Syringe => Bootstrap::, + Icons::Tag => Bootstrap::Tag, + Icons::Thermometer => Bootstrap::Thermometer, + Icons::Terminal => Bootstrap::Terminal, + Icons::Text => Bootstrap::Fonts, + Icons::TextCursor => Bootstrap::CursorText, + // Icons::TextSelection => Bootstrap::, + // Icons::Torch => Bootstrap::, + // Icons::Train => Bootstrap::, + Icons::Trash => Bootstrap::Trash, + Icons::Tree => Bootstrap::Tree, + Icons::Trophy => Bootstrap::Trophy, + Icons::People => Bootstrap::People, + Icons::Umbrella => Bootstrap::Umbrella, + Icons::Underline => Bootstrap::TypeUnderline, + Icons::Upload => Bootstrap::Upload, + // Icons::Uppercase => Bootstrap::, + Icons::Wallet => Bootstrap::Wallet, + Icons::Wand => Bootstrap::Magic, + // Icons::Warning => Bootstrap::, + // Icons::Weights => Bootstrap::, + Icons::Wifi => Bootstrap::Wifi, + Icons::WifiDisabled => Bootstrap::WifiOff, + Icons::Window => Bootstrap::Window, + Icons::Tools => Bootstrap::Tools, + Icons::Watch => Bootstrap::Watch, + Icons::XMark => Bootstrap::XLg, + Icons::Indent => Bootstrap::Indent, + Icons::Unindent => Bootstrap::Unindent, } } diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index bb2ec12..555e0b6 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -6,11 +6,10 @@ use crate::ui::widget::{create_state, ActionPanel, ComponentWidgetEvent, Compone use common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiWidgetId}; use std::collections::HashMap; use std::mem; -use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; -use iced::Command; +use std::ops::DerefMut; +use std::sync::{Arc, Mutex}; +use iced::Task; use crate::ui::AppMsg; -use crate::ui::scroll_handle::ScrollHandle; pub struct PluginWidgetContainer { root_widget: Arc>>, @@ -119,21 +118,21 @@ impl PluginWidgetContainer { .render_root_inline_widget(self.plugin_name.as_ref(), self.entrypoint_name.as_ref()) } - pub fn append_text(&self, text: &str) -> Command { + pub fn append_text(&self, text: &str) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); ComponentWidgets::new(&mut root_widget, &mut state, &self.images).append_text(text) } - pub fn backspace_text(&self) -> Command { + pub fn backspace_text(&self) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); ComponentWidgets::new(&mut root_widget, &mut state, &self.images).backspace_text() } - pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Command { + pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); @@ -161,28 +160,28 @@ impl PluginWidgetContainer { ComponentWidgets::new(&mut root_widget, &mut state, &self.images).get_action_panel(action_shortcuts) } - pub fn focus_up(&self) -> Command { + pub fn focus_up(&self) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_up() } - pub fn focus_down(&self) -> Command { + pub fn focus_down(&self) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_down() } - pub fn focus_left(&self) -> Command { + pub fn focus_left(&self) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_left() } - pub fn focus_right(&self) -> Command { + pub fn focus_right(&self) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); diff --git a/rust/common/build.rs b/rust/common/build.rs index bea5763..8c37c59 100644 --- a/rust/common/build.rs +++ b/rust/common/build.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::env; use std::fs::File; use std::io::Write; @@ -132,7 +131,7 @@ fn component_model_generator() -> Result<(), Box> { .unique_by(|component_ref| component_ref.component_name.clone()) .collect::>(); - let mut per_type_component_refs = per_type_members + let per_type_component_refs = per_type_members .iter() .map(|(_member_name, component_ref)| component_ref) .collect::>(); diff --git a/rust/common_ui/Cargo.toml b/rust/common_ui/Cargo.toml index b4404ec..a7f6ec2 100644 --- a/rust/common_ui/Cargo.toml +++ b/rust/common_ui/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" common = { path = "../common" } iced.workspace = true iced_aw.workspace = true +iced_fonts.workspace = true diff --git a/rust/common_ui/src/lib.rs b/rust/common_ui/src/lib.rs index d4414a6..555336f 100644 --- a/rust/common_ui/src/lib.rs +++ b/rust/common_ui/src/lib.rs @@ -1,12 +1,29 @@ -use iced::advanced::widget::Text; -use iced::Element; +use iced::{Element, Padding, Pixels}; +use iced::border::Radius; use iced::keyboard::Modifiers; -use iced::widget::text; -use iced_aw::core::icons; +use iced::widget::{text, value}; +use iced_aw::iced_fonts::{bootstrap, Bootstrap, BOOTSTRAP_FONT}; use common::model::{PhysicalKey, PhysicalShortcut}; -pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>( +pub fn padding(top: impl Into, right: impl Into, bottom: impl Into, left: impl Into) -> Padding { + Padding { + top: top.into().0, + right: right.into().0, + bottom: bottom.into().0, + left: left.into().0 + } +} +pub fn radius(top_left: impl Into, top_right: impl Into, bottom_right: impl Into, bottom_left: impl Into) -> Radius { + Radius { + top_left: top_left.into().0, + top_right: top_right.into().0, + bottom_right: bottom_right.into().0, + bottom_left: bottom_left.into().0 + } +} + +pub fn shortcut_to_text<'a, Message, Theme: text::Catalog + 'a>( shortcut: &PhysicalShortcut ) -> ( Element<'a, Message, Theme>, @@ -18,8 +35,8 @@ pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>( let (key_name, show_shift) = match shortcut.physical_key { PhysicalKey::Enter => { let key_name = if cfg!(target_os = "macos") { - text(icons::Bootstrap::ArrowReturnLeft) - .font(icons::BOOTSTRAP_FONT) + value(Bootstrap::ArrowReturnLeft) + .font(BOOTSTRAP_FONT) .into() } else { text("Enter") @@ -41,8 +58,8 @@ pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>( let alt_modifier_text = if shortcut.modifier_alt { if cfg!(target_os = "macos") { Some( - text(icons::Bootstrap::Option) - .font(icons::BOOTSTRAP_FONT) + value(Bootstrap::Option) + .font(BOOTSTRAP_FONT) .into() ) } else { @@ -58,8 +75,8 @@ pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>( let meta_modifier_text = if shortcut.modifier_meta { if cfg!(target_os = "macos") { Some( - text(icons::Bootstrap::Command) - .font(icons::BOOTSTRAP_FONT) + value(Bootstrap::Command) + .font(BOOTSTRAP_FONT) .into() ) } else if cfg!(target_os = "windows") { @@ -81,7 +98,7 @@ pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>( if cfg!(target_os = "macos") { Some( text("^") // TODO bootstrap doesn't have proper macos ctrl icon - .font(icons::BOOTSTRAP_FONT) + .font(BOOTSTRAP_FONT) .into() ) } else { @@ -97,8 +114,8 @@ pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>( let shift_modifier_text = if show_shift && shortcut.modifier_shift { if cfg!(target_os = "macos") { Some( - text(icons::Bootstrap::Shift) - .font(icons::BOOTSTRAP_FONT) + value(Bootstrap::Shift) + .font(BOOTSTRAP_FONT) .into() ) } else { @@ -114,203 +131,202 @@ pub fn shortcut_to_text<'a, Message, Theme: text::StyleSheet + 'a>( (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) } -pub fn physical_key_model(key: iced::keyboard::key::PhysicalKey, modifiers: Modifiers) -> Option { +pub fn physical_key_model(key: iced::keyboard::key::Code, modifiers: Modifiers) -> Option { let model_key = match key { - iced::keyboard::key::PhysicalKey::Backquote => PhysicalKey::Backquote, - iced::keyboard::key::PhysicalKey::Backslash => PhysicalKey::Backslash, - iced::keyboard::key::PhysicalKey::BracketLeft => PhysicalKey::BracketLeft, - iced::keyboard::key::PhysicalKey::BracketRight => PhysicalKey::BracketRight, - iced::keyboard::key::PhysicalKey::Comma => PhysicalKey::Comma, - iced::keyboard::key::PhysicalKey::Digit0 => PhysicalKey::Digit0, - iced::keyboard::key::PhysicalKey::Digit1 => PhysicalKey::Digit1, - iced::keyboard::key::PhysicalKey::Digit2 => PhysicalKey::Digit2, - iced::keyboard::key::PhysicalKey::Digit3 => PhysicalKey::Digit3, - iced::keyboard::key::PhysicalKey::Digit4 => PhysicalKey::Digit4, - iced::keyboard::key::PhysicalKey::Digit5 => PhysicalKey::Digit5, - iced::keyboard::key::PhysicalKey::Digit6 => PhysicalKey::Digit6, - iced::keyboard::key::PhysicalKey::Digit7 => PhysicalKey::Digit7, - iced::keyboard::key::PhysicalKey::Digit8 => PhysicalKey::Digit8, - iced::keyboard::key::PhysicalKey::Digit9 => PhysicalKey::Digit9, - iced::keyboard::key::PhysicalKey::Equal => PhysicalKey::Equal, - iced::keyboard::key::PhysicalKey::IntlBackslash => PhysicalKey::IntlBackslash, - iced::keyboard::key::PhysicalKey::IntlRo => PhysicalKey::IntlRo, - iced::keyboard::key::PhysicalKey::IntlYen => PhysicalKey::IntlYen, - iced::keyboard::key::PhysicalKey::KeyA => PhysicalKey::KeyA, - iced::keyboard::key::PhysicalKey::KeyB => PhysicalKey::KeyB, - iced::keyboard::key::PhysicalKey::KeyC => PhysicalKey::KeyC, - iced::keyboard::key::PhysicalKey::KeyD => PhysicalKey::KeyD, - iced::keyboard::key::PhysicalKey::KeyE => PhysicalKey::KeyE, - iced::keyboard::key::PhysicalKey::KeyF => PhysicalKey::KeyF, - iced::keyboard::key::PhysicalKey::KeyG => PhysicalKey::KeyG, - iced::keyboard::key::PhysicalKey::KeyH => PhysicalKey::KeyH, - iced::keyboard::key::PhysicalKey::KeyI => PhysicalKey::KeyI, - iced::keyboard::key::PhysicalKey::KeyJ => PhysicalKey::KeyJ, - iced::keyboard::key::PhysicalKey::KeyK => PhysicalKey::KeyK, - iced::keyboard::key::PhysicalKey::KeyL => PhysicalKey::KeyL, - iced::keyboard::key::PhysicalKey::KeyM => PhysicalKey::KeyM, - iced::keyboard::key::PhysicalKey::KeyN => PhysicalKey::KeyN, - iced::keyboard::key::PhysicalKey::KeyO => PhysicalKey::KeyO, - iced::keyboard::key::PhysicalKey::KeyP => PhysicalKey::KeyP, - iced::keyboard::key::PhysicalKey::KeyQ => PhysicalKey::KeyQ, - iced::keyboard::key::PhysicalKey::KeyR => PhysicalKey::KeyR, - iced::keyboard::key::PhysicalKey::KeyS => PhysicalKey::KeyS, - iced::keyboard::key::PhysicalKey::KeyT => PhysicalKey::KeyT, - iced::keyboard::key::PhysicalKey::KeyU => PhysicalKey::KeyU, - iced::keyboard::key::PhysicalKey::KeyV => PhysicalKey::KeyV, - iced::keyboard::key::PhysicalKey::KeyW => PhysicalKey::KeyW, - iced::keyboard::key::PhysicalKey::KeyX => PhysicalKey::KeyX, - iced::keyboard::key::PhysicalKey::KeyY => PhysicalKey::KeyY, - iced::keyboard::key::PhysicalKey::KeyZ => PhysicalKey::KeyZ, - iced::keyboard::key::PhysicalKey::Minus => PhysicalKey::Minus, - iced::keyboard::key::PhysicalKey::Period => PhysicalKey::Period, - iced::keyboard::key::PhysicalKey::Quote => PhysicalKey::Quote, - iced::keyboard::key::PhysicalKey::Semicolon => PhysicalKey::Semicolon, - iced::keyboard::key::PhysicalKey::Slash => PhysicalKey::Slash, - iced::keyboard::key::PhysicalKey::Backspace => PhysicalKey::Backspace, - iced::keyboard::key::PhysicalKey::CapsLock => PhysicalKey::CapsLock, - iced::keyboard::key::PhysicalKey::ContextMenu => PhysicalKey::ContextMenu, - iced::keyboard::key::PhysicalKey::Enter => PhysicalKey::Enter, - iced::keyboard::key::PhysicalKey::Space => PhysicalKey::Space, - iced::keyboard::key::PhysicalKey::Tab => PhysicalKey::Tab, - iced::keyboard::key::PhysicalKey::Convert => PhysicalKey::Convert, - iced::keyboard::key::PhysicalKey::KanaMode => PhysicalKey::KanaMode, - iced::keyboard::key::PhysicalKey::Lang1 => PhysicalKey::Lang1, - iced::keyboard::key::PhysicalKey::Lang2 => PhysicalKey::Lang2, - iced::keyboard::key::PhysicalKey::Lang3 => PhysicalKey::Lang3, - iced::keyboard::key::PhysicalKey::Lang4 => PhysicalKey::Lang4, - iced::keyboard::key::PhysicalKey::Lang5 => PhysicalKey::Lang5, - iced::keyboard::key::PhysicalKey::NonConvert => PhysicalKey::NonConvert, - iced::keyboard::key::PhysicalKey::Delete => PhysicalKey::Delete, - iced::keyboard::key::PhysicalKey::End => PhysicalKey::End, - iced::keyboard::key::PhysicalKey::Help => PhysicalKey::Help, - iced::keyboard::key::PhysicalKey::Home => PhysicalKey::Home, - iced::keyboard::key::PhysicalKey::Insert => PhysicalKey::Insert, - iced::keyboard::key::PhysicalKey::PageDown => PhysicalKey::PageDown, - iced::keyboard::key::PhysicalKey::PageUp => PhysicalKey::PageUp, - iced::keyboard::key::PhysicalKey::ArrowDown => PhysicalKey::ArrowDown, - iced::keyboard::key::PhysicalKey::ArrowLeft => PhysicalKey::ArrowLeft, - iced::keyboard::key::PhysicalKey::ArrowRight => PhysicalKey::ArrowRight, - iced::keyboard::key::PhysicalKey::ArrowUp => PhysicalKey::ArrowUp, - iced::keyboard::key::PhysicalKey::NumLock => PhysicalKey::NumLock, - iced::keyboard::key::PhysicalKey::Numpad0 => PhysicalKey::Numpad0, - iced::keyboard::key::PhysicalKey::Numpad1 => PhysicalKey::Numpad1, - iced::keyboard::key::PhysicalKey::Numpad2 => PhysicalKey::Numpad2, - iced::keyboard::key::PhysicalKey::Numpad3 => PhysicalKey::Numpad3, - iced::keyboard::key::PhysicalKey::Numpad4 => PhysicalKey::Numpad4, - iced::keyboard::key::PhysicalKey::Numpad5 => PhysicalKey::Numpad5, - iced::keyboard::key::PhysicalKey::Numpad6 => PhysicalKey::Numpad6, - iced::keyboard::key::PhysicalKey::Numpad7 => PhysicalKey::Numpad7, - iced::keyboard::key::PhysicalKey::Numpad8 => PhysicalKey::Numpad8, - iced::keyboard::key::PhysicalKey::Numpad9 => PhysicalKey::Numpad9, - iced::keyboard::key::PhysicalKey::NumpadAdd => PhysicalKey::NumpadAdd, - iced::keyboard::key::PhysicalKey::NumpadBackspace => PhysicalKey::NumpadBackspace, - iced::keyboard::key::PhysicalKey::NumpadClear => PhysicalKey::NumpadClear, - iced::keyboard::key::PhysicalKey::NumpadClearEntry => PhysicalKey::NumpadClearEntry, - iced::keyboard::key::PhysicalKey::NumpadComma => PhysicalKey::NumpadComma, - iced::keyboard::key::PhysicalKey::NumpadDecimal => PhysicalKey::NumpadDecimal, - iced::keyboard::key::PhysicalKey::NumpadDivide => PhysicalKey::NumpadDivide, - iced::keyboard::key::PhysicalKey::NumpadEnter => PhysicalKey::NumpadEnter, - iced::keyboard::key::PhysicalKey::NumpadEqual => PhysicalKey::NumpadEqual, - iced::keyboard::key::PhysicalKey::NumpadHash => PhysicalKey::NumpadHash, - iced::keyboard::key::PhysicalKey::NumpadMemoryAdd => PhysicalKey::NumpadMemoryAdd, - iced::keyboard::key::PhysicalKey::NumpadMemoryClear => PhysicalKey::NumpadMemoryClear, - iced::keyboard::key::PhysicalKey::NumpadMemoryRecall => PhysicalKey::NumpadMemoryRecall, - iced::keyboard::key::PhysicalKey::NumpadMemoryStore => PhysicalKey::NumpadMemoryStore, - iced::keyboard::key::PhysicalKey::NumpadMemorySubtract => PhysicalKey::NumpadMemorySubtract, - iced::keyboard::key::PhysicalKey::NumpadMultiply => PhysicalKey::NumpadMultiply, - iced::keyboard::key::PhysicalKey::NumpadParenLeft => PhysicalKey::NumpadParenLeft, - iced::keyboard::key::PhysicalKey::NumpadParenRight => PhysicalKey::NumpadParenRight, - iced::keyboard::key::PhysicalKey::NumpadStar => PhysicalKey::NumpadStar, - iced::keyboard::key::PhysicalKey::NumpadSubtract => PhysicalKey::NumpadSubtract, - iced::keyboard::key::PhysicalKey::Escape => PhysicalKey::Escape, - iced::keyboard::key::PhysicalKey::Fn => PhysicalKey::Fn, - iced::keyboard::key::PhysicalKey::FnLock => PhysicalKey::FnLock, - iced::keyboard::key::PhysicalKey::PrintScreen => PhysicalKey::PrintScreen, - iced::keyboard::key::PhysicalKey::ScrollLock => PhysicalKey::ScrollLock, - iced::keyboard::key::PhysicalKey::Pause => PhysicalKey::Pause, - iced::keyboard::key::PhysicalKey::BrowserBack => PhysicalKey::BrowserBack, - iced::keyboard::key::PhysicalKey::BrowserFavorites => PhysicalKey::BrowserFavorites, - iced::keyboard::key::PhysicalKey::BrowserForward => PhysicalKey::BrowserForward, - iced::keyboard::key::PhysicalKey::BrowserHome => PhysicalKey::BrowserHome, - iced::keyboard::key::PhysicalKey::BrowserRefresh => PhysicalKey::BrowserRefresh, - iced::keyboard::key::PhysicalKey::BrowserSearch => PhysicalKey::BrowserSearch, - iced::keyboard::key::PhysicalKey::BrowserStop => PhysicalKey::BrowserStop, - iced::keyboard::key::PhysicalKey::Eject => PhysicalKey::Eject, - iced::keyboard::key::PhysicalKey::LaunchApp1 => PhysicalKey::LaunchApp1, - iced::keyboard::key::PhysicalKey::LaunchApp2 => PhysicalKey::LaunchApp2, - iced::keyboard::key::PhysicalKey::LaunchMail => PhysicalKey::LaunchMail, - iced::keyboard::key::PhysicalKey::MediaPlayPause => PhysicalKey::MediaPlayPause, - iced::keyboard::key::PhysicalKey::MediaSelect => PhysicalKey::MediaSelect, - iced::keyboard::key::PhysicalKey::MediaStop => PhysicalKey::MediaStop, - iced::keyboard::key::PhysicalKey::MediaTrackNext => PhysicalKey::MediaTrackNext, - iced::keyboard::key::PhysicalKey::MediaTrackPrevious => PhysicalKey::MediaTrackPrevious, - iced::keyboard::key::PhysicalKey::Power => PhysicalKey::Power, - iced::keyboard::key::PhysicalKey::Sleep => PhysicalKey::Sleep, - iced::keyboard::key::PhysicalKey::AudioVolumeDown => PhysicalKey::AudioVolumeDown, - iced::keyboard::key::PhysicalKey::AudioVolumeMute => PhysicalKey::AudioVolumeMute, - iced::keyboard::key::PhysicalKey::AudioVolumeUp => PhysicalKey::AudioVolumeUp, - iced::keyboard::key::PhysicalKey::WakeUp => PhysicalKey::WakeUp, - iced::keyboard::key::PhysicalKey::Abort => PhysicalKey::Abort, - iced::keyboard::key::PhysicalKey::Resume => PhysicalKey::Resume, - iced::keyboard::key::PhysicalKey::Suspend => PhysicalKey::Suspend, - iced::keyboard::key::PhysicalKey::Again => PhysicalKey::Again, - iced::keyboard::key::PhysicalKey::Copy => PhysicalKey::Copy, - iced::keyboard::key::PhysicalKey::Cut => PhysicalKey::Cut, - iced::keyboard::key::PhysicalKey::Find => PhysicalKey::Find, - iced::keyboard::key::PhysicalKey::Open => PhysicalKey::Open, - iced::keyboard::key::PhysicalKey::Paste => PhysicalKey::Paste, - iced::keyboard::key::PhysicalKey::Props => PhysicalKey::Props, - iced::keyboard::key::PhysicalKey::Select => PhysicalKey::Select, - iced::keyboard::key::PhysicalKey::Undo => PhysicalKey::Undo, - iced::keyboard::key::PhysicalKey::Hiragana => PhysicalKey::Hiragana, - iced::keyboard::key::PhysicalKey::Katakana => PhysicalKey::Katakana, - iced::keyboard::key::PhysicalKey::F1 => PhysicalKey::F1, - iced::keyboard::key::PhysicalKey::F2 => PhysicalKey::F2, - iced::keyboard::key::PhysicalKey::F3 => PhysicalKey::F3, - iced::keyboard::key::PhysicalKey::F4 => PhysicalKey::F4, - iced::keyboard::key::PhysicalKey::F5 => PhysicalKey::F5, - iced::keyboard::key::PhysicalKey::F6 => PhysicalKey::F6, - iced::keyboard::key::PhysicalKey::F7 => PhysicalKey::F7, - iced::keyboard::key::PhysicalKey::F8 => PhysicalKey::F8, - iced::keyboard::key::PhysicalKey::F9 => PhysicalKey::F9, - iced::keyboard::key::PhysicalKey::F10 => PhysicalKey::F10, - iced::keyboard::key::PhysicalKey::F11 => PhysicalKey::F11, - iced::keyboard::key::PhysicalKey::F12 => PhysicalKey::F12, - iced::keyboard::key::PhysicalKey::F13 => PhysicalKey::F13, - iced::keyboard::key::PhysicalKey::F14 => PhysicalKey::F14, - iced::keyboard::key::PhysicalKey::F15 => PhysicalKey::F15, - iced::keyboard::key::PhysicalKey::F16 => PhysicalKey::F16, - iced::keyboard::key::PhysicalKey::F17 => PhysicalKey::F17, - iced::keyboard::key::PhysicalKey::F18 => PhysicalKey::F18, - iced::keyboard::key::PhysicalKey::F19 => PhysicalKey::F19, - iced::keyboard::key::PhysicalKey::F20 => PhysicalKey::F20, - iced::keyboard::key::PhysicalKey::F21 => PhysicalKey::F21, - iced::keyboard::key::PhysicalKey::F22 => PhysicalKey::F22, - iced::keyboard::key::PhysicalKey::F23 => PhysicalKey::F23, - iced::keyboard::key::PhysicalKey::F24 => PhysicalKey::F24, - iced::keyboard::key::PhysicalKey::F25 => PhysicalKey::F25, - iced::keyboard::key::PhysicalKey::F26 => PhysicalKey::F26, - iced::keyboard::key::PhysicalKey::F27 => PhysicalKey::F27, - iced::keyboard::key::PhysicalKey::F28 => PhysicalKey::F28, - iced::keyboard::key::PhysicalKey::F29 => PhysicalKey::F29, - iced::keyboard::key::PhysicalKey::F30 => PhysicalKey::F30, - iced::keyboard::key::PhysicalKey::F31 => PhysicalKey::F31, - iced::keyboard::key::PhysicalKey::F32 => PhysicalKey::F32, - iced::keyboard::key::PhysicalKey::F33 => PhysicalKey::F33, - iced::keyboard::key::PhysicalKey::F34 => PhysicalKey::F34, - iced::keyboard::key::PhysicalKey::F35 => PhysicalKey::F35, - iced::keyboard::key::PhysicalKey::Meta - | iced::keyboard::key::PhysicalKey::AltLeft - | iced::keyboard::key::PhysicalKey::AltRight - | iced::keyboard::key::PhysicalKey::ControlLeft - | iced::keyboard::key::PhysicalKey::ControlRight - | iced::keyboard::key::PhysicalKey::ShiftRight - | iced::keyboard::key::PhysicalKey::ShiftLeft - | iced::keyboard::key::PhysicalKey::SuperLeft - | iced::keyboard::key::PhysicalKey::SuperRight - | iced::keyboard::key::PhysicalKey::Hyper - | iced::keyboard::key::PhysicalKey::Turbo - | iced::keyboard::key::PhysicalKey::Unidentified + iced::keyboard::key::Code::Backquote => PhysicalKey::Backquote, + iced::keyboard::key::Code::Backslash => PhysicalKey::Backslash, + iced::keyboard::key::Code::BracketLeft => PhysicalKey::BracketLeft, + iced::keyboard::key::Code::BracketRight => PhysicalKey::BracketRight, + iced::keyboard::key::Code::Comma => PhysicalKey::Comma, + iced::keyboard::key::Code::Digit0 => PhysicalKey::Digit0, + iced::keyboard::key::Code::Digit1 => PhysicalKey::Digit1, + iced::keyboard::key::Code::Digit2 => PhysicalKey::Digit2, + iced::keyboard::key::Code::Digit3 => PhysicalKey::Digit3, + iced::keyboard::key::Code::Digit4 => PhysicalKey::Digit4, + iced::keyboard::key::Code::Digit5 => PhysicalKey::Digit5, + iced::keyboard::key::Code::Digit6 => PhysicalKey::Digit6, + iced::keyboard::key::Code::Digit7 => PhysicalKey::Digit7, + iced::keyboard::key::Code::Digit8 => PhysicalKey::Digit8, + iced::keyboard::key::Code::Digit9 => PhysicalKey::Digit9, + iced::keyboard::key::Code::Equal => PhysicalKey::Equal, + iced::keyboard::key::Code::IntlBackslash => PhysicalKey::IntlBackslash, + iced::keyboard::key::Code::IntlRo => PhysicalKey::IntlRo, + iced::keyboard::key::Code::IntlYen => PhysicalKey::IntlYen, + iced::keyboard::key::Code::KeyA => PhysicalKey::KeyA, + iced::keyboard::key::Code::KeyB => PhysicalKey::KeyB, + iced::keyboard::key::Code::KeyC => PhysicalKey::KeyC, + iced::keyboard::key::Code::KeyD => PhysicalKey::KeyD, + iced::keyboard::key::Code::KeyE => PhysicalKey::KeyE, + iced::keyboard::key::Code::KeyF => PhysicalKey::KeyF, + iced::keyboard::key::Code::KeyG => PhysicalKey::KeyG, + iced::keyboard::key::Code::KeyH => PhysicalKey::KeyH, + iced::keyboard::key::Code::KeyI => PhysicalKey::KeyI, + iced::keyboard::key::Code::KeyJ => PhysicalKey::KeyJ, + iced::keyboard::key::Code::KeyK => PhysicalKey::KeyK, + iced::keyboard::key::Code::KeyL => PhysicalKey::KeyL, + iced::keyboard::key::Code::KeyM => PhysicalKey::KeyM, + iced::keyboard::key::Code::KeyN => PhysicalKey::KeyN, + iced::keyboard::key::Code::KeyO => PhysicalKey::KeyO, + iced::keyboard::key::Code::KeyP => PhysicalKey::KeyP, + iced::keyboard::key::Code::KeyQ => PhysicalKey::KeyQ, + iced::keyboard::key::Code::KeyR => PhysicalKey::KeyR, + iced::keyboard::key::Code::KeyS => PhysicalKey::KeyS, + iced::keyboard::key::Code::KeyT => PhysicalKey::KeyT, + iced::keyboard::key::Code::KeyU => PhysicalKey::KeyU, + iced::keyboard::key::Code::KeyV => PhysicalKey::KeyV, + iced::keyboard::key::Code::KeyW => PhysicalKey::KeyW, + iced::keyboard::key::Code::KeyX => PhysicalKey::KeyX, + iced::keyboard::key::Code::KeyY => PhysicalKey::KeyY, + iced::keyboard::key::Code::KeyZ => PhysicalKey::KeyZ, + iced::keyboard::key::Code::Minus => PhysicalKey::Minus, + iced::keyboard::key::Code::Period => PhysicalKey::Period, + iced::keyboard::key::Code::Quote => PhysicalKey::Quote, + iced::keyboard::key::Code::Semicolon => PhysicalKey::Semicolon, + iced::keyboard::key::Code::Slash => PhysicalKey::Slash, + iced::keyboard::key::Code::Backspace => PhysicalKey::Backspace, + iced::keyboard::key::Code::CapsLock => PhysicalKey::CapsLock, + iced::keyboard::key::Code::ContextMenu => PhysicalKey::ContextMenu, + iced::keyboard::key::Code::Enter => PhysicalKey::Enter, + iced::keyboard::key::Code::Space => PhysicalKey::Space, + iced::keyboard::key::Code::Tab => PhysicalKey::Tab, + iced::keyboard::key::Code::Convert => PhysicalKey::Convert, + iced::keyboard::key::Code::KanaMode => PhysicalKey::KanaMode, + iced::keyboard::key::Code::Lang1 => PhysicalKey::Lang1, + iced::keyboard::key::Code::Lang2 => PhysicalKey::Lang2, + iced::keyboard::key::Code::Lang3 => PhysicalKey::Lang3, + iced::keyboard::key::Code::Lang4 => PhysicalKey::Lang4, + iced::keyboard::key::Code::Lang5 => PhysicalKey::Lang5, + iced::keyboard::key::Code::NonConvert => PhysicalKey::NonConvert, + iced::keyboard::key::Code::Delete => PhysicalKey::Delete, + iced::keyboard::key::Code::End => PhysicalKey::End, + iced::keyboard::key::Code::Help => PhysicalKey::Help, + iced::keyboard::key::Code::Home => PhysicalKey::Home, + iced::keyboard::key::Code::Insert => PhysicalKey::Insert, + iced::keyboard::key::Code::PageDown => PhysicalKey::PageDown, + iced::keyboard::key::Code::PageUp => PhysicalKey::PageUp, + iced::keyboard::key::Code::ArrowDown => PhysicalKey::ArrowDown, + iced::keyboard::key::Code::ArrowLeft => PhysicalKey::ArrowLeft, + iced::keyboard::key::Code::ArrowRight => PhysicalKey::ArrowRight, + iced::keyboard::key::Code::ArrowUp => PhysicalKey::ArrowUp, + iced::keyboard::key::Code::NumLock => PhysicalKey::NumLock, + iced::keyboard::key::Code::Numpad0 => PhysicalKey::Numpad0, + iced::keyboard::key::Code::Numpad1 => PhysicalKey::Numpad1, + iced::keyboard::key::Code::Numpad2 => PhysicalKey::Numpad2, + iced::keyboard::key::Code::Numpad3 => PhysicalKey::Numpad3, + iced::keyboard::key::Code::Numpad4 => PhysicalKey::Numpad4, + iced::keyboard::key::Code::Numpad5 => PhysicalKey::Numpad5, + iced::keyboard::key::Code::Numpad6 => PhysicalKey::Numpad6, + iced::keyboard::key::Code::Numpad7 => PhysicalKey::Numpad7, + iced::keyboard::key::Code::Numpad8 => PhysicalKey::Numpad8, + iced::keyboard::key::Code::Numpad9 => PhysicalKey::Numpad9, + iced::keyboard::key::Code::NumpadAdd => PhysicalKey::NumpadAdd, + iced::keyboard::key::Code::NumpadBackspace => PhysicalKey::NumpadBackspace, + iced::keyboard::key::Code::NumpadClear => PhysicalKey::NumpadClear, + iced::keyboard::key::Code::NumpadClearEntry => PhysicalKey::NumpadClearEntry, + iced::keyboard::key::Code::NumpadComma => PhysicalKey::NumpadComma, + iced::keyboard::key::Code::NumpadDecimal => PhysicalKey::NumpadDecimal, + iced::keyboard::key::Code::NumpadDivide => PhysicalKey::NumpadDivide, + iced::keyboard::key::Code::NumpadEnter => PhysicalKey::NumpadEnter, + iced::keyboard::key::Code::NumpadEqual => PhysicalKey::NumpadEqual, + iced::keyboard::key::Code::NumpadHash => PhysicalKey::NumpadHash, + iced::keyboard::key::Code::NumpadMemoryAdd => PhysicalKey::NumpadMemoryAdd, + iced::keyboard::key::Code::NumpadMemoryClear => PhysicalKey::NumpadMemoryClear, + iced::keyboard::key::Code::NumpadMemoryRecall => PhysicalKey::NumpadMemoryRecall, + iced::keyboard::key::Code::NumpadMemoryStore => PhysicalKey::NumpadMemoryStore, + iced::keyboard::key::Code::NumpadMemorySubtract => PhysicalKey::NumpadMemorySubtract, + iced::keyboard::key::Code::NumpadMultiply => PhysicalKey::NumpadMultiply, + iced::keyboard::key::Code::NumpadParenLeft => PhysicalKey::NumpadParenLeft, + iced::keyboard::key::Code::NumpadParenRight => PhysicalKey::NumpadParenRight, + iced::keyboard::key::Code::NumpadStar => PhysicalKey::NumpadStar, + iced::keyboard::key::Code::NumpadSubtract => PhysicalKey::NumpadSubtract, + iced::keyboard::key::Code::Escape => PhysicalKey::Escape, + iced::keyboard::key::Code::Fn => PhysicalKey::Fn, + iced::keyboard::key::Code::FnLock => PhysicalKey::FnLock, + iced::keyboard::key::Code::PrintScreen => PhysicalKey::PrintScreen, + iced::keyboard::key::Code::ScrollLock => PhysicalKey::ScrollLock, + iced::keyboard::key::Code::Pause => PhysicalKey::Pause, + iced::keyboard::key::Code::BrowserBack => PhysicalKey::BrowserBack, + iced::keyboard::key::Code::BrowserFavorites => PhysicalKey::BrowserFavorites, + iced::keyboard::key::Code::BrowserForward => PhysicalKey::BrowserForward, + iced::keyboard::key::Code::BrowserHome => PhysicalKey::BrowserHome, + iced::keyboard::key::Code::BrowserRefresh => PhysicalKey::BrowserRefresh, + iced::keyboard::key::Code::BrowserSearch => PhysicalKey::BrowserSearch, + iced::keyboard::key::Code::BrowserStop => PhysicalKey::BrowserStop, + iced::keyboard::key::Code::Eject => PhysicalKey::Eject, + iced::keyboard::key::Code::LaunchApp1 => PhysicalKey::LaunchApp1, + iced::keyboard::key::Code::LaunchApp2 => PhysicalKey::LaunchApp2, + iced::keyboard::key::Code::LaunchMail => PhysicalKey::LaunchMail, + iced::keyboard::key::Code::MediaPlayPause => PhysicalKey::MediaPlayPause, + iced::keyboard::key::Code::MediaSelect => PhysicalKey::MediaSelect, + iced::keyboard::key::Code::MediaStop => PhysicalKey::MediaStop, + iced::keyboard::key::Code::MediaTrackNext => PhysicalKey::MediaTrackNext, + iced::keyboard::key::Code::MediaTrackPrevious => PhysicalKey::MediaTrackPrevious, + iced::keyboard::key::Code::Power => PhysicalKey::Power, + iced::keyboard::key::Code::Sleep => PhysicalKey::Sleep, + iced::keyboard::key::Code::AudioVolumeDown => PhysicalKey::AudioVolumeDown, + iced::keyboard::key::Code::AudioVolumeMute => PhysicalKey::AudioVolumeMute, + iced::keyboard::key::Code::AudioVolumeUp => PhysicalKey::AudioVolumeUp, + iced::keyboard::key::Code::WakeUp => PhysicalKey::WakeUp, + iced::keyboard::key::Code::Abort => PhysicalKey::Abort, + iced::keyboard::key::Code::Resume => PhysicalKey::Resume, + iced::keyboard::key::Code::Suspend => PhysicalKey::Suspend, + iced::keyboard::key::Code::Again => PhysicalKey::Again, + iced::keyboard::key::Code::Copy => PhysicalKey::Copy, + iced::keyboard::key::Code::Cut => PhysicalKey::Cut, + iced::keyboard::key::Code::Find => PhysicalKey::Find, + iced::keyboard::key::Code::Open => PhysicalKey::Open, + iced::keyboard::key::Code::Paste => PhysicalKey::Paste, + iced::keyboard::key::Code::Props => PhysicalKey::Props, + iced::keyboard::key::Code::Select => PhysicalKey::Select, + iced::keyboard::key::Code::Undo => PhysicalKey::Undo, + iced::keyboard::key::Code::Hiragana => PhysicalKey::Hiragana, + iced::keyboard::key::Code::Katakana => PhysicalKey::Katakana, + iced::keyboard::key::Code::F1 => PhysicalKey::F1, + iced::keyboard::key::Code::F2 => PhysicalKey::F2, + iced::keyboard::key::Code::F3 => PhysicalKey::F3, + iced::keyboard::key::Code::F4 => PhysicalKey::F4, + iced::keyboard::key::Code::F5 => PhysicalKey::F5, + iced::keyboard::key::Code::F6 => PhysicalKey::F6, + iced::keyboard::key::Code::F7 => PhysicalKey::F7, + iced::keyboard::key::Code::F8 => PhysicalKey::F8, + iced::keyboard::key::Code::F9 => PhysicalKey::F9, + iced::keyboard::key::Code::F10 => PhysicalKey::F10, + iced::keyboard::key::Code::F11 => PhysicalKey::F11, + iced::keyboard::key::Code::F12 => PhysicalKey::F12, + iced::keyboard::key::Code::F13 => PhysicalKey::F13, + iced::keyboard::key::Code::F14 => PhysicalKey::F14, + iced::keyboard::key::Code::F15 => PhysicalKey::F15, + iced::keyboard::key::Code::F16 => PhysicalKey::F16, + iced::keyboard::key::Code::F17 => PhysicalKey::F17, + iced::keyboard::key::Code::F18 => PhysicalKey::F18, + iced::keyboard::key::Code::F19 => PhysicalKey::F19, + iced::keyboard::key::Code::F20 => PhysicalKey::F20, + iced::keyboard::key::Code::F21 => PhysicalKey::F21, + iced::keyboard::key::Code::F22 => PhysicalKey::F22, + iced::keyboard::key::Code::F23 => PhysicalKey::F23, + iced::keyboard::key::Code::F24 => PhysicalKey::F24, + iced::keyboard::key::Code::F25 => PhysicalKey::F25, + iced::keyboard::key::Code::F26 => PhysicalKey::F26, + iced::keyboard::key::Code::F27 => PhysicalKey::F27, + iced::keyboard::key::Code::F28 => PhysicalKey::F28, + iced::keyboard::key::Code::F29 => PhysicalKey::F29, + iced::keyboard::key::Code::F30 => PhysicalKey::F30, + iced::keyboard::key::Code::F31 => PhysicalKey::F31, + iced::keyboard::key::Code::F32 => PhysicalKey::F32, + iced::keyboard::key::Code::F33 => PhysicalKey::F33, + iced::keyboard::key::Code::F34 => PhysicalKey::F34, + iced::keyboard::key::Code::F35 => PhysicalKey::F35, + iced::keyboard::key::Code::Meta + | iced::keyboard::key::Code::AltLeft + | iced::keyboard::key::Code::AltRight + | iced::keyboard::key::Code::ControlLeft + | iced::keyboard::key::Code::ControlRight + | iced::keyboard::key::Code::ShiftRight + | iced::keyboard::key::Code::ShiftLeft + | iced::keyboard::key::Code::SuperLeft + | iced::keyboard::key::Code::SuperRight + | iced::keyboard::key::Code::Hyper + | iced::keyboard::key::Code::Turbo | _ => { return None } diff --git a/rust/management_client/Cargo.toml b/rust/management_client/Cargo.toml index 6390ff3..c7eb68b 100644 --- a/rust/management_client/Cargo.toml +++ b/rust/management_client/Cargo.toml @@ -10,6 +10,7 @@ thiserror = "1.0.48" iced.workspace = true iced_aw.workspace = true iced_table.workspace = true +iced_fonts.workspace = true tracing = "0.1" tracing-subscriber = "0.3" common = { path = "../common" } diff --git a/rust/management_client/src/components/shortcut_selector.rs b/rust/management_client/src/components/shortcut_selector.rs index bb08b23..bfe118f 100644 --- a/rust/management_client/src/components/shortcut_selector.rs +++ b/rust/management_client/src/components/shortcut_selector.rs @@ -2,16 +2,17 @@ use iced::{alignment, Element, Event, Length, Padding, Rectangle, Renderer, Size use iced::advanced::{Clipboard, layout, Layout, mouse, renderer, Shell, Widget}; use iced::advanced::graphics::core::{event, keyboard}; use iced::advanced::widget::{Tree, tree}; +use iced::keyboard::key::Physical; use iced::mouse::Button; use iced::widget::{container, row, text}; -use iced::widget::container::{Appearance, draw_background, layout}; +use iced::widget::container::{draw_background, layout}; use common::model::PhysicalShortcut; use common_ui::{physical_key_model, shortcut_to_text}; pub struct ShortcutSelector<'a, Message, Theme> where - Theme: StyleSheet + text::StyleSheet + container::StyleSheet + Theme: Catalog + text::Catalog + container::Catalog { padding: Padding, width: Length, @@ -21,8 +22,6 @@ where horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, - style: ::Style, - on_shortcut_captured: Box) -> Message + 'a>, on_capturing_change: Box Message + 'a>, @@ -31,13 +30,12 @@ where impl<'a, Message: 'a, Theme> ShortcutSelector<'a, Message, Theme> where - Theme: StyleSheet + text::StyleSheet + container::StyleSheet + 'a + Theme: Catalog + text::Catalog + container::Catalog + 'a { pub fn new( current_shortcut: &Option, on_shortcut_captured: F, on_capturing_change: F2, - style: ::Style ) -> Self where F: 'a + Fn(Option) -> Message, @@ -89,8 +87,6 @@ where horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, - style, - on_shortcut_captured: Box::new(on_shortcut_captured), on_capturing_change: Box::new(on_capturing_change), @@ -107,7 +103,7 @@ struct State { impl<'a, Message, Theme> Widget for ShortcutSelector<'a, Message, Theme> where - Theme: StyleSheet + text::StyleSheet + container::StyleSheet + Theme: Catalog + text::Catalog + container::Catalog { fn size(&self) -> Size { Size { @@ -148,11 +144,13 @@ where let state = tree.state.downcast_ref::(); let style = if state.is_capturing { - theme.capturing(&self.style) + Status::Capturing } else { - theme.active(&self.style) + Status::Active }; + let style = Catalog::style(theme, &::default(), style); + draw_background(renderer, &style, layout.bounds()); self.content.as_widget().draw( @@ -160,9 +158,7 @@ where renderer, theme, &renderer::Style { - text_color: style - .text_color - .unwrap_or(renderer_style.text_color), + text_color: renderer_style.text_color, }, layout.children().next().unwrap(), cursor, @@ -205,42 +201,47 @@ where match event { keyboard::Event::KeyReleased { physical_key, modifiers, .. } => { match physical_key { - keyboard::key::PhysicalKey::Backspace => { - state.is_capturing = false; - - let message = (self.on_capturing_change)(false); - shell.publish(message); - - let message = (self.on_shortcut_captured)(None); - shell.publish(message); - - event::Status::Ignored - } - keyboard::key::PhysicalKey::Escape => { - state.is_capturing = false; - - let message = (self.on_capturing_change)(false); - shell.publish(message); - - event::Status::Ignored - } - _ => { - match physical_key_model(physical_key, modifiers) { - None => event::Status::Ignored, - Some(shortcut) => { + Physical::Code(code) => { + match code { + keyboard::key::Code::Backspace => { state.is_capturing = false; let message = (self.on_capturing_change)(false); shell.publish(message); - let message = (self.on_shortcut_captured)(Some(shortcut)); + let message = (self.on_shortcut_captured)(None); shell.publish(message); - event::Status::Captured + event::Status::Ignored + } + keyboard::key::Code::Escape => { + state.is_capturing = false; + + let message = (self.on_capturing_change)(false); + shell.publish(message); + + event::Status::Ignored + } + _ => { + match physical_key_model(code, modifiers) { + None => event::Status::Ignored, + Some(shortcut) => { + state.is_capturing = false; + + let message = (self.on_capturing_change)(false); + shell.publish(message); + + let message = (self.on_shortcut_captured)(Some(shortcut)); + shell.publish(message); + + event::Status::Captured + } + } + } } - } + Physical::Unidentified(_) => event::Status::Ignored } } _ => event::Status::Ignored @@ -295,16 +296,23 @@ where impl<'a, Message, Theme> From> for Element<'a, Message, Theme> where Message: 'a, - Theme: StyleSheet + text::StyleSheet + container::StyleSheet + 'a + Theme: Catalog + text::Catalog + container::Catalog + 'a { fn from(shortcut_selector: ShortcutSelector<'a, Message, Theme>) -> Self { Self::new(shortcut_selector) } } -pub trait StyleSheet { - type Style: Default; - - fn active(&self, style: &Self::Style) -> Appearance; - fn capturing(&self, style: &Self::Style) -> Appearance; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + Active, + Capturing +} + +pub trait Catalog { + type Class<'a>; + + fn default<'a>() -> Self::Class<'a>; + + fn style(&self, class: &Self::Class<'_>, status: Status) -> container::Style; } diff --git a/rust/management_client/src/theme.rs b/rust/management_client/src/theme.rs index 08c83ce..e1e81c3 100644 --- a/rust/management_client/src/theme.rs +++ b/rust/management_client/src/theme.rs @@ -1,4 +1,4 @@ -use iced::application::{Appearance, StyleSheet}; +use iced::application::{Appearance, DefaultStyle}; pub mod container; pub mod text; @@ -11,17 +11,14 @@ pub mod checkbox; pub mod pick_list; pub mod scrollable; pub mod shortcut_selector; -mod spinner; pub type Element<'a, Message> = iced::Element<'a, Message, GauntletSettingsTheme>; #[derive(Default)] pub struct GauntletSettingsTheme; -impl StyleSheet for GauntletSettingsTheme { - type Style = (); - - fn appearance(&self, _: &Self::Style) -> Appearance { +impl DefaultStyle for GauntletSettingsTheme { + fn default_style(&self) -> Appearance { Appearance { background_color: BACKGROUND_DARKEST.to_iced(), text_color: TEXT_LIGHTEST.to_iced(), diff --git a/rust/management_client/src/theme/button.rs b/rust/management_client/src/theme/button.rs index eb25ee0..be1e62c 100644 --- a/rust/management_client/src/theme/button.rs +++ b/rust/management_client/src/theme/button.rs @@ -1,12 +1,10 @@ use iced::widget::button; -use iced::widget::button::Appearance; +use iced::widget::button::{Status, Style}; use iced::Border; use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, BACKGROUND_LIGHTER, BUTTON_BORDER_RADIUS, DANGER, PRIMARY, PRIMARY_HOVERED, SUCCESS, TEXT_DARKEST, TEXT_LIGHTEST}; -#[derive(Default)] pub enum ButtonStyle { - #[default] Primary, Positive, Destructive, @@ -16,142 +14,167 @@ pub enum ButtonStyle { DownloadInfo, } -//noinspection RsSortImplTraitMembers -impl button::StyleSheet for GauntletSettingsTheme { - type Style = ButtonStyle; +impl button::Catalog for GauntletSettingsTheme { + type Class<'a> = ButtonStyle; - fn active(&self, style: &Self::Style) -> Appearance { - let (background_color, text_color) = match style { - ButtonStyle::Primary => (PRIMARY.to_iced(), TEXT_DARKEST.to_iced()), - ButtonStyle::Positive => (SUCCESS.to_iced(), TEXT_DARKEST.to_iced()), - ButtonStyle::Destructive => (DANGER.to_iced(), TEXT_LIGHTEST.to_iced()), - ButtonStyle::TableRow => { - return Appearance { - background: None, - text_color: TEXT_LIGHTEST.to_iced(), - ..Default::default() - } - } - ButtonStyle::ViewSwitcher => { - return Appearance { - background: None, - text_color: TEXT_LIGHTEST.to_iced(), - border: Border { - radius: BUTTON_BORDER_RADIUS.into(), - ..Default::default() - }, - ..Default::default() - } - } - ButtonStyle::ViewSwitcherSelected => { - return Appearance { - background: Some(BACKGROUND_DARKER.to_iced().into()), - text_color: TEXT_LIGHTEST.to_iced(), - border: Border { - radius: BUTTON_BORDER_RADIUS.into(), - ..Default::default() - }, - ..Default::default() - } - } - ButtonStyle::DownloadInfo => { - return Appearance { - background: None, - text_color: TEXT_LIGHTEST.to_iced(), - border: Border { - radius: BUTTON_BORDER_RADIUS.into(), - ..Default::default() - }, - ..Default::default() - } - } - }; - - Appearance { - background: Some(background_color.into()), - text_color, - border: Border { - radius: BUTTON_BORDER_RADIUS.into(), - ..Default::default() - }, - ..Default::default() - } + fn default<'a>() -> Self::Class<'a> { + ButtonStyle::Primary } - fn hovered(&self, style: &Self::Style) -> Appearance { - let (background_color, text_color) = match style { - ButtonStyle::Primary => (PRIMARY_HOVERED.to_iced(), TEXT_DARKEST.to_iced()), - ButtonStyle::Positive => (SUCCESS.to_iced(), TEXT_DARKEST.to_iced()), // TODO - ButtonStyle::Destructive => (DANGER.to_iced(), TEXT_LIGHTEST.to_iced()), // TODO - ButtonStyle::TableRow => { - return Appearance { - background: None, - text_color: TEXT_LIGHTEST.to_iced(), // TODO - ..Default::default() - } - } - ButtonStyle::ViewSwitcher => { - return Appearance { - background: Some(BACKGROUND_LIGHTER.to_iced().into()), - text_color: TEXT_LIGHTEST.to_iced(), - border: Border { - radius: BUTTON_BORDER_RADIUS.into(), - ..Default::default() - }, - ..Default::default() - } - } - ButtonStyle::ViewSwitcherSelected => { - return Appearance { - background: Some(BACKGROUND_LIGHTER.to_iced().into()), - text_color: TEXT_LIGHTEST.to_iced(), - border: Border { - radius: BUTTON_BORDER_RADIUS.into(), - ..Default::default() - }, - ..Default::default() - } - } - ButtonStyle::DownloadInfo => { - return Appearance { - background: Some(BACKGROUND_LIGHTER.to_iced().into()), - text_color: TEXT_LIGHTEST.to_iced(), - border: Border { - radius: BUTTON_BORDER_RADIUS.into(), - ..Default::default() - }, - ..Default::default() - } - } - }; - - Appearance { - background: Some(background_color.into()), - text_color, - border: Border { - radius: BUTTON_BORDER_RADIUS.into(), - ..Default::default() - }, - ..Default::default() + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + match status { + Status::Active => active(class), + Status::Hovered => hovered(class), + Status::Pressed => pressed(class), + Status::Disabled => disabled(class) } } +} - fn pressed(&self, style: &Self::Style) -> Appearance { - match style { - ButtonStyle::ViewSwitcher | ButtonStyle::ViewSwitcherSelected => { - Appearance { - background: Some(BACKGROUND_DARKER.to_iced().into()), - text_color: TEXT_LIGHTEST.to_iced(), - border: Border { - radius: BUTTON_BORDER_RADIUS.into(), - ..Default::default() - }, - ..Default::default() - } - } - _ => { - self.active(style) + +fn active(class: &ButtonStyle) -> Style { + let (background_color, text_color) = match class { + ButtonStyle::Primary => (PRIMARY.to_iced(), TEXT_DARKEST.to_iced()), + ButtonStyle::Positive => (SUCCESS.to_iced(), TEXT_DARKEST.to_iced()), + ButtonStyle::Destructive => (DANGER.to_iced(), TEXT_LIGHTEST.to_iced()), + ButtonStyle::TableRow => { + return Style { + background: None, + text_color: TEXT_LIGHTEST.to_iced(), + ..Default::default() } } + ButtonStyle::ViewSwitcher => { + return Style { + background: None, + text_color: TEXT_LIGHTEST.to_iced(), + border: Border { + radius: BUTTON_BORDER_RADIUS.into(), + ..Default::default() + }, + ..Default::default() + } + } + ButtonStyle::ViewSwitcherSelected => { + return Style { + background: Some(BACKGROUND_DARKER.to_iced().into()), + text_color: TEXT_LIGHTEST.to_iced(), + border: Border { + radius: BUTTON_BORDER_RADIUS.into(), + ..Default::default() + }, + ..Default::default() + } + } + ButtonStyle::DownloadInfo => { + return Style { + background: None, + text_color: TEXT_LIGHTEST.to_iced(), + border: Border { + radius: BUTTON_BORDER_RADIUS.into(), + ..Default::default() + }, + ..Default::default() + } + } + }; + + Style { + background: Some(background_color.into()), + text_color, + border: Border { + radius: BUTTON_BORDER_RADIUS.into(), + ..Default::default() + }, + ..Default::default() + } +} + +fn hovered(class: &ButtonStyle) -> Style { + let (background_color, text_color) = match class { + ButtonStyle::Primary => (PRIMARY_HOVERED.to_iced(), TEXT_DARKEST.to_iced()), + ButtonStyle::Positive => (SUCCESS.to_iced(), TEXT_DARKEST.to_iced()), // TODO + ButtonStyle::Destructive => (DANGER.to_iced(), TEXT_LIGHTEST.to_iced()), // TODO + ButtonStyle::TableRow => { + return Style { + background: None, + text_color: TEXT_LIGHTEST.to_iced(), // TODO + ..Default::default() + } + } + ButtonStyle::ViewSwitcher => { + return Style { + background: Some(BACKGROUND_LIGHTER.to_iced().into()), + text_color: TEXT_LIGHTEST.to_iced(), + border: Border { + radius: BUTTON_BORDER_RADIUS.into(), + ..Default::default() + }, + ..Default::default() + } + } + ButtonStyle::ViewSwitcherSelected => { + return Style { + background: Some(BACKGROUND_LIGHTER.to_iced().into()), + text_color: TEXT_LIGHTEST.to_iced(), + border: Border { + radius: BUTTON_BORDER_RADIUS.into(), + ..Default::default() + }, + ..Default::default() + } + } + ButtonStyle::DownloadInfo => { + return Style { + background: Some(BACKGROUND_LIGHTER.to_iced().into()), + text_color: TEXT_LIGHTEST.to_iced(), + border: Border { + radius: BUTTON_BORDER_RADIUS.into(), + ..Default::default() + }, + ..Default::default() + } + } + }; + + Style { + background: Some(background_color.into()), + text_color, + border: Border { + radius: BUTTON_BORDER_RADIUS.into(), + ..Default::default() + }, + ..Default::default() + } +} + +fn pressed(class: &ButtonStyle) -> Style { + match class { + ButtonStyle::ViewSwitcher | ButtonStyle::ViewSwitcherSelected => { + Style { + background: Some(BACKGROUND_DARKER.to_iced().into()), + text_color: TEXT_LIGHTEST.to_iced(), + border: Border { + radius: BUTTON_BORDER_RADIUS.into(), + ..Default::default() + }, + ..Default::default() + } + } + _ => { + active(class) + } + } +} + +fn disabled(class: &ButtonStyle) -> Style { + let style = active(class); + + Style { + background: style + .background + .map(|background| background.scale_alpha(0.5)), + text_color: style.text_color.scale_alpha(0.5), + ..style } } \ No newline at end of file diff --git a/rust/management_client/src/theme/checkbox.rs b/rust/management_client/src/theme/checkbox.rs index 51ed81f..574122d 100644 --- a/rust/management_client/src/theme/checkbox.rs +++ b/rust/management_client/src/theme/checkbox.rs @@ -1,68 +1,73 @@ -use iced::Border; +use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, BACKGROUND_DARKEST, BACKGROUND_LIGHTER, PRIMARY, PRIMARY_HOVERED}; use iced::widget::checkbox; -use iced::widget::checkbox::Appearance; +use iced::widget::checkbox::{Status, Style}; +use iced::Border; -use crate::theme::{BACKGROUND_LIGHTER, GauntletSettingsTheme, PRIMARY, PRIMARY_HOVERED, BACKGROUND_DARKER, BACKGROUND_DARKEST}; +impl checkbox::Catalog for GauntletSettingsTheme { + type Class<'a> = (); -#[derive(Default)] -pub enum CheckboxStyle { - #[default] - Default, + fn default<'a>() -> Self::Class<'a> { + () + } + + fn style(&self, _class: &Self::Class<'_>, status: Status) -> Style { + match status { + Status::Active { is_checked } => active(is_checked), + Status::Hovered { is_checked } => hovered(is_checked), + Status::Disabled { is_checked } => disabled(is_checked), + } + } } -impl checkbox::StyleSheet for GauntletSettingsTheme { - type Style = CheckboxStyle; +fn active(is_checked: bool) -> Style { + let background = if is_checked { + PRIMARY.to_iced().into() + } else { + BACKGROUND_DARKEST.to_iced().into() + }; - fn active(&self, _: &Self::Style, is_checked: bool) -> Appearance { - let background = if is_checked { - PRIMARY.to_iced().into() - } else { - BACKGROUND_DARKEST.to_iced().into() - }; - - Appearance { - background, - icon_color: BACKGROUND_DARKEST.to_iced(), - border: Border { - radius: 4.0.into(), - width: 1.0, - color: PRIMARY.to_iced().into(), - }, - text_color: None, - } + Style { + background, + icon_color: BACKGROUND_DARKEST.to_iced(), + border: Border { + radius: 4.0.into(), + width: 1.0, + color: PRIMARY.to_iced().into(), + }, + text_color: None, } +} - fn hovered(&self, _: &Self::Style, is_checked: bool) -> Appearance { - let background = if is_checked { - PRIMARY_HOVERED.to_iced().into() - } else { - BACKGROUND_DARKER.to_iced().into() - }; +fn hovered(is_checked: bool) -> Style { + let background = if is_checked { + PRIMARY_HOVERED.to_iced().into() + } else { + BACKGROUND_DARKER.to_iced().into() + }; - Appearance { - background, - icon_color: BACKGROUND_DARKEST.to_iced(), - border: Border { - radius: 4.0.into(), - width: 1.0, - color: PRIMARY.to_iced().into(), - }, - text_color: None, - } + Style { + background, + icon_color: BACKGROUND_DARKEST.to_iced(), + border: Border { + radius: 4.0.into(), + width: 1.0, + color: PRIMARY.to_iced().into(), + }, + text_color: None, } +} - fn disabled(&self, _: &Self::Style, is_checked: bool) -> Appearance { - let background = if is_checked { - BACKGROUND_LIGHTER.to_iced().into() - } else { - BACKGROUND_DARKER.to_iced().into() - }; +fn disabled(is_checked: bool) -> Style { + let background = if is_checked { + BACKGROUND_LIGHTER.to_iced().into() + } else { + BACKGROUND_DARKER.to_iced().into() + }; - Appearance { - background, - icon_color: BACKGROUND_DARKEST.to_iced(), - border: Default::default(), - text_color: None, - } + Style { + background, + icon_color: BACKGROUND_DARKEST.to_iced(), + border: Default::default(), + text_color: None, } } \ No newline at end of file diff --git a/rust/management_client/src/theme/container.rs b/rust/management_client/src/theme/container.rs index dfd7126..14a240d 100644 --- a/rust/management_client/src/theme/container.rs +++ b/rust/management_client/src/theme/container.rs @@ -1,25 +1,27 @@ -use iced::{Border, Color}; +use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, BACKGROUND_LIGHTER, BACKGROUND_LIGHTEST, DANGER, TRANSPARENT}; use iced::widget::container; +use iced::widget::container::Style; +use iced::{Border, Color}; -use crate::theme::{GauntletSettingsTheme, BACKGROUND_LIGHTEST, BACKGROUND_DARKER, BACKGROUND_LIGHTER, DANGER, TRANSPARENT}; - -#[derive(Default)] pub enum ContainerStyle { - #[default] Transparent, Box, TextInputLike, TextInputMissingValue } -impl container::StyleSheet for GauntletSettingsTheme { - type Style = ContainerStyle; +impl container::Catalog for GauntletSettingsTheme { + type Class<'a> = ContainerStyle; - fn appearance(&self, style: &Self::Style) -> container::Appearance { - match style { + fn default<'a>() -> Self::Class<'a> { + ContainerStyle::Transparent + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + match class { ContainerStyle::Transparent => Default::default(), ContainerStyle::Box => { - container::Appearance { + Style { background: Some(BACKGROUND_DARKER.to_iced().into()), border: Border { color: BACKGROUND_LIGHTER.to_iced(), @@ -30,7 +32,7 @@ impl container::StyleSheet for GauntletSettingsTheme { } } ContainerStyle::TextInputLike => { - container::Appearance { + Style { background: Some(BACKGROUND_LIGHTEST.to_iced().into()), border: Border { radius: 4.0.into(), @@ -43,7 +45,7 @@ impl container::StyleSheet for GauntletSettingsTheme { ContainerStyle::TextInputMissingValue => { let color = DANGER.to_iced(); - container::Appearance { + Style { background: Some(Color::new(color.r, color.g, color.b, 0.3).into()), border: Border { color: TRANSPARENT.to_iced(), diff --git a/rust/management_client/src/theme/number_input.rs b/rust/management_client/src/theme/number_input.rs index 569e482..e696e2b 100644 --- a/rust/management_client/src/theme/number_input.rs +++ b/rust/management_client/src/theme/number_input.rs @@ -1,34 +1,49 @@ -use iced_aw::number_input; - +use iced_aw::style::Status; +use iced_aw::number_input::{number_input, Style}; use crate::theme::{GauntletSettingsTheme, PRIMARY, PRIMARY_HOVERED, TEXT_DARKER, TEXT_LIGHTEST}; -#[derive(Default)] -pub enum NumberInputStyle { - #[default] - Default +impl number_input::ExtendedCatalog for GauntletSettingsTheme { + fn style(&self, class: &(), status: Status) -> Style { + number_input::Catalog::style(self, class, status) + } } -impl number_input::StyleSheet for GauntletSettingsTheme { - type Style = NumberInputStyle; +impl number_input::Catalog for GauntletSettingsTheme { + type Class<'a> = (); - fn active(&self, _: &Self::Style) -> number_input::Appearance { - number_input::Appearance { - button_background: Some(PRIMARY.to_iced().into()), - icon_color: TEXT_DARKER.to_iced(), - } + fn default<'a>() -> Self::Class<'a> { + () } - fn pressed(&self, _: &Self::Style) -> number_input::Appearance { - number_input::Appearance { - button_background: Some(PRIMARY_HOVERED.to_iced().into()), - icon_color: TEXT_DARKER.to_iced(), + fn style(&self, _class: &Self::Class<'_>, status: Status) -> Style { + match status { + Status::Active => active(), + Status::Hovered => active(), // TODO proper style + Status::Pressed => pressed(), + Status::Disabled => disabled(), + Status::Focused => active(), // TODO proper style + Status::Selected => pressed(), // TODO proper style } } +} - fn disabled(&self, _: &Self::Style) -> number_input::Appearance { - number_input::Appearance { - button_background: None, - icon_color: TEXT_LIGHTEST.to_iced(), - } +fn active() -> Style { + Style { + button_background: Some(PRIMARY.to_iced().into()), + icon_color: TEXT_DARKER.to_iced(), + } +} + +fn pressed() -> Style { + Style { + button_background: Some(PRIMARY_HOVERED.to_iced().into()), + icon_color: TEXT_DARKER.to_iced(), + } +} + +fn disabled() -> Style { + Style { + button_background: None, + icon_color: TEXT_LIGHTEST.to_iced(), } } \ No newline at end of file diff --git a/rust/management_client/src/theme/pick_list.rs b/rust/management_client/src/theme/pick_list.rs index 27db849..24ccbda 100644 --- a/rust/management_client/src/theme/pick_list.rs +++ b/rust/management_client/src/theme/pick_list.rs @@ -1,49 +1,33 @@ use iced::{Border, overlay}; use iced::widget::pick_list; - use crate::theme::{BUTTON_BORDER_RADIUS, GauntletSettingsTheme, PRIMARY, PRIMARY_HOVERED, TEXT_DARKEST, BACKGROUND_DARKER, BACKGROUND_DARKEST, TEXT_LIGHTEST}; -#[derive(Clone, Default)] -pub enum PickListStyle { - #[default] - Default, -} +impl pick_list::Catalog for GauntletSettingsTheme { + type Class<'a> = (); -#[derive(Clone, Default)] -pub enum MenuStyle { - #[default] - Default, -} - -impl pick_list::StyleSheet for GauntletSettingsTheme { - type Style = PickListStyle; - - fn active(&self, _: &Self::Style) -> pick_list::Appearance { - pick_list_appearance(PickListState::Active) + fn default<'a>() -> ::Class<'a> { + () } - fn hovered(&self, _: &Self::Style) -> pick_list::Appearance { - pick_list_appearance(PickListState::Hovered) + fn style(&self, _class: &(), status: pick_list::Status) -> pick_list::Style { + pick_list_appearance(status) } } -enum PickListState { - Active, - Hovered -} +fn pick_list_appearance(status: pick_list::Status) -> pick_list::Style { + use iced::widget::pick_list::Status; -fn pick_list_appearance(state: PickListState) -> pick_list::Appearance { - let background_color = match state { - PickListState::Active => PRIMARY.to_iced(), - PickListState::Hovered => PRIMARY_HOVERED.to_iced(), + let background_color = match status { + Status::Active | Status::Opened => PRIMARY.to_iced(), + Status::Hovered => PRIMARY_HOVERED.to_iced(), }; - let text_color = match state { - PickListState::Active => TEXT_DARKEST.to_iced(), - PickListState::Hovered => TEXT_DARKEST.to_iced(), + let text_color = match status { + Status::Active | Status::Opened => TEXT_DARKEST.to_iced(), + Status::Hovered => TEXT_DARKEST.to_iced(), }; - pick_list::Appearance { + pick_list::Style { text_color, background: background_color.into(), placeholder_color: BACKGROUND_DARKER.to_iced(), @@ -56,11 +40,15 @@ fn pick_list_appearance(state: PickListState) -> pick_list::Appearance { } } -impl overlay::menu::StyleSheet for GauntletSettingsTheme { - type Style = MenuStyle; +impl overlay::menu::Catalog for GauntletSettingsTheme { + type Class<'a> = (); - fn appearance(&self, _: &Self::Style) -> overlay::menu::Appearance { - overlay::menu::Appearance { + fn default<'a>() -> ::Class<'a> { + () + } + + fn style(&self, _class: &()) -> overlay::menu::Style { + overlay::menu::Style { text_color: TEXT_LIGHTEST.to_iced(), background: BACKGROUND_DARKEST.to_iced().into(), border: Border { @@ -73,11 +61,3 @@ impl overlay::menu::StyleSheet for GauntletSettingsTheme { } } } - -impl From for MenuStyle { - fn from(pick_list: PickListStyle) -> Self { - match pick_list { - PickListStyle::Default => Self::Default, - } - } -} \ No newline at end of file diff --git a/rust/management_client/src/theme/rule.rs b/rust/management_client/src/theme/rule.rs index bc98df8..5f17689 100644 --- a/rust/management_client/src/theme/rule.rs +++ b/rust/management_client/src/theme/rule.rs @@ -1,17 +1,17 @@ -use iced::widget::rule; use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER}; +use iced::widget::rule; +use iced::widget::rule::Style; -#[derive(Default)] -pub enum RuleStyle { - #[default] - Default, -} -impl rule::StyleSheet for GauntletSettingsTheme { - type Style = RuleStyle; +impl rule::Catalog for GauntletSettingsTheme { + type Class<'a> = (); - fn appearance(&self, _: &Self::Style) -> rule::Appearance { - rule::Appearance { + fn default<'a>() -> Self::Class<'a> { + () + } + + fn style(&self, _class: &Self::Class<'_>) -> Style { + Style { color: BACKGROUND_DARKER.to_iced(), width: 1, radius: 0.0.into(), diff --git a/rust/management_client/src/theme/scrollable.rs b/rust/management_client/src/theme/scrollable.rs index 613b629..ac79030 100644 --- a/rust/management_client/src/theme/scrollable.rs +++ b/rust/management_client/src/theme/scrollable.rs @@ -1,58 +1,87 @@ -use iced::{Border, Color}; -use iced::widget::scrollable; -use iced::widget::scrollable::Appearance; +use iced::widget::scrollable::{Status, Style}; +use iced::widget::{container, scrollable}; +use iced::{border, Border, Color}; use crate::theme::{GauntletSettingsTheme, PRIMARY}; -#[derive(Default)] -pub enum ScrollableStyle { - #[default] - Default -} +impl scrollable::Catalog for GauntletSettingsTheme { + type Class<'a> = (); -impl scrollable::StyleSheet for GauntletSettingsTheme { - type Style = ScrollableStyle; - - fn active(&self, _: &Self::Style) -> Appearance { - appearance(ScrollbarState::Active) + fn default<'a>() -> Self::Class<'a> { + () } - fn hovered(&self, style: &Self::Style, is_mouse_over_scrollbar: bool) -> Appearance { - if is_mouse_over_scrollbar { - appearance(ScrollbarState::Hovered) - } else { - self.active(style) + fn style(&self, _class: &Self::Class<'_>, status: Status) -> Style { + let scrollbar = scrollable::Rail { + background: None, + border: Border::default(), + scroller: scrollable::Scroller { + color: Color::TRANSPARENT, + border: border::rounded(4.0), + }, + }; + + match status { + Status::Active => Style { + container: container::Style::default(), + vertical_rail: scrollbar, + horizontal_rail: scrollbar, + gap: None, + }, + Status::Hovered { + is_horizontal_scrollbar_hovered, + is_vertical_scrollbar_hovered, + } => { + let hovered_scrollbar = scrollable::Rail { + scroller: scrollable::Scroller { + color: PRIMARY.to_iced(), + ..scrollbar.scroller + }, + ..scrollbar + }; + + Style { + container: container::Style::default(), + vertical_rail: if is_vertical_scrollbar_hovered { + hovered_scrollbar + } else { + scrollbar + }, + horizontal_rail: if is_horizontal_scrollbar_hovered { + hovered_scrollbar + } else { + scrollbar + }, + gap: None, + } + } + Status::Dragged { + is_horizontal_scrollbar_dragged, + is_vertical_scrollbar_dragged, + } => { + let dragged_scrollbar = scrollable::Rail { + scroller: scrollable::Scroller { + color: PRIMARY.to_iced(), + ..scrollbar.scroller + }, + ..scrollbar + }; + + Style { + container: container::Style::default(), + vertical_rail: if is_vertical_scrollbar_dragged { + dragged_scrollbar + } else { + scrollbar + }, + horizontal_rail: if is_horizontal_scrollbar_dragged { + dragged_scrollbar + } else { + scrollbar + }, + gap: None, + } + } } } -} - -enum ScrollbarState { - Active, - Hovered -} - -fn appearance(state: ScrollbarState) -> Appearance { - let scroller_color = match state { - ScrollbarState::Active => Color::TRANSPARENT, - ScrollbarState::Hovered => PRIMARY.to_iced(), - }; - - Appearance { - container: Default::default(), - scrollbar: scrollable::Scrollbar { - background: None, - border: Border { - color: Color::TRANSPARENT, - ..Border::default() - }, - scroller: scrollable::Scroller { - color: scroller_color, - border: Border { - radius: 4.0.into(), - ..Default::default() - }, - }, - }, - gap: None, - } } \ No newline at end of file diff --git a/rust/management_client/src/theme/shortcut_selector.rs b/rust/management_client/src/theme/shortcut_selector.rs index 54ff022..d7eb99d 100644 --- a/rust/management_client/src/theme/shortcut_selector.rs +++ b/rust/management_client/src/theme/shortcut_selector.rs @@ -1,21 +1,20 @@ -use iced::Border; -use iced::widget::container::Appearance; use crate::components::shortcut_selector; -use crate::theme::{BUTTON_BORDER_RADIUS, GauntletSettingsTheme, PRIMARY, BACKGROUND_DARKER}; +use crate::components::shortcut_selector::Status; +use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, BUTTON_BORDER_RADIUS, PRIMARY}; +use iced::widget::container::Style; +use iced::Border; -#[derive(Default)] -pub enum ShortcutSelectorStyle { - #[default] - Default, -} +impl shortcut_selector::Catalog for GauntletSettingsTheme { + type Class<'a> = (); -impl shortcut_selector::StyleSheet for GauntletSettingsTheme { - type Style = ShortcutSelectorStyle; + fn default<'a>() -> Self::Class<'a> { + () + } - fn active(&self, style: &Self::Style) -> Appearance { - match style { - ShortcutSelectorStyle::Default => { - Appearance { + fn style(&self, _class: &Self::Class<'_>, status: Status) -> Style { + match status { + Status::Active => { + Style { background: Some(BACKGROUND_DARKER.to_iced().into()), border: Border { radius: BUTTON_BORDER_RADIUS.into(), @@ -24,13 +23,8 @@ impl shortcut_selector::StyleSheet for GauntletSettingsTheme { ..Default::default() } } - } - } - - fn capturing(&self, style: &Self::Style) -> Appearance { - match style { - ShortcutSelectorStyle::Default => { - Appearance { + Status::Capturing => { + Style { background: Some(BACKGROUND_DARKER.to_iced().into()), border: Border { radius: BUTTON_BORDER_RADIUS.into(), diff --git a/rust/management_client/src/theme/spinner.rs b/rust/management_client/src/theme/spinner.rs deleted file mode 100644 index 04afbd7..0000000 --- a/rust/management_client/src/theme/spinner.rs +++ /dev/null @@ -1,16 +0,0 @@ -use iced_aw::style::spinner; -use crate::theme::GauntletSettingsTheme; - -#[derive(Default)] -pub enum SpinnerStyle { - #[default] - Default, -} - -impl spinner::StyleSheet for GauntletSettingsTheme { - type Style = SpinnerStyle; - - fn appearance(&self, _style: &Self::Style) -> spinner::Appearance { - spinner::Appearance {} - } -} diff --git a/rust/management_client/src/theme/table.rs b/rust/management_client/src/theme/table.rs index 9212db2..bb44927 100644 --- a/rust/management_client/src/theme/table.rs +++ b/rust/management_client/src/theme/table.rs @@ -1,19 +1,14 @@ -use iced::Border; use iced::widget::container; +use iced::Border; -use crate::theme::{BACKGROUND_DARKER, BACKGROUND_LIGHTEST, GauntletSettingsTheme, TEXT_LIGHTEST, BACKGROUND_LIGHTER}; +use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, TEXT_LIGHTEST}; -#[derive(Default, Clone)] -pub enum TableStyle { - #[default] - Default -} -impl iced_table::StyleSheet for GauntletSettingsTheme { - type Style = TableStyle; +impl iced_table::Catalog for GauntletSettingsTheme { + type Style = (); - fn header(&self, _: &Self::Style) -> container::Appearance { - container::Appearance { + fn header(&self, _: &Self::Style) -> container::Style { + container::Style { text_color: Some(TEXT_LIGHTEST.to_iced()), background: Some(BACKGROUND_DARKER.to_iced().into()), border: Border { @@ -24,8 +19,8 @@ impl iced_table::StyleSheet for GauntletSettingsTheme { } } - fn footer(&self, _: &Self::Style) -> container::Appearance { - container::Appearance { + fn footer(&self, _: &Self::Style) -> container::Style { + container::Style { text_color: Some(TEXT_LIGHTEST.to_iced()), background: Some(BACKGROUND_DARKER.to_iced().into()), border: Border { @@ -37,14 +32,14 @@ impl iced_table::StyleSheet for GauntletSettingsTheme { } // TODO selected and hovered upstream - fn row(&self, _: &Self::Style, index: usize) -> container::Appearance { + fn row(&self, _: &Self::Style, index: usize) -> container::Style { let background = if index % 2 == 0 { None } else { Some(BACKGROUND_DARKER.to_iced().into()) }; - container::Appearance { + container::Style { background, border: Border { radius: 4.0.into(), @@ -54,8 +49,8 @@ impl iced_table::StyleSheet for GauntletSettingsTheme { } } - fn divider(&self, _: &Self::Style, hovered: bool) -> container::Appearance { - container::Appearance { + fn divider(&self, _: &Self::Style, _hovered: bool) -> container::Style { + container::Style { ..Default::default() } } diff --git a/rust/management_client/src/theme/text.rs b/rust/management_client/src/theme/text.rs index d8e8f74..c924ec4 100644 --- a/rust/management_client/src/theme/text.rs +++ b/rust/management_client/src/theme/text.rs @@ -1,38 +1,40 @@ use iced::widget::text; - +use iced::widget::text::Style; use crate::theme::{DANGER_BRIGHT, GauntletSettingsTheme, SUCCESS, TEXT_DARKER}; -#[derive(Default, Clone)] pub enum TextStyle { - #[default] Default, Subtitle, Positive, Destructive, } -impl text::StyleSheet for GauntletSettingsTheme { - type Style = TextStyle; +impl text::Catalog for GauntletSettingsTheme { + type Class<'a> = TextStyle; - fn appearance(&self, style: Self::Style) -> text::Appearance { - match style { + fn default<'a>() -> Self::Class<'a> { + TextStyle::Default + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + match class { TextStyle::Default => { - text::Appearance { + Style { color: None, } } TextStyle::Subtitle => { - text::Appearance { + Style { color: Some(TEXT_DARKER.to_iced()), } } TextStyle::Positive => { - text::Appearance { + Style { color: Some(SUCCESS.to_iced()), } } TextStyle::Destructive => { - text::Appearance { + Style { color: Some(DANGER_BRIGHT.to_iced()), } } diff --git a/rust/management_client/src/theme/text_input.rs b/rust/management_client/src/theme/text_input.rs index 6140df5..903cb4a 100644 --- a/rust/management_client/src/theme/text_input.rs +++ b/rust/management_client/src/theme/text_input.rs @@ -1,75 +1,48 @@ -use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, NOT_INTENDED_TO_BE_USED, TEXT_DARKER, TEXT_LIGHTEST, TRANSPARENT}; +use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, TEXT_DARKER, TEXT_LIGHTEST, TRANSPARENT}; use iced::widget::text_input; -use iced::widget::text_input::Appearance; -use iced::{Border, Color}; +use iced::widget::text_input::{Status, Style}; +use iced::{Background, Border}; -#[derive(Default)] pub enum TextInputStyle { - #[default] FormInput } -//noinspection RsSortImplTraitMembers -impl text_input::StyleSheet for GauntletSettingsTheme { - type Style = TextInputStyle; +impl text_input::Catalog for GauntletSettingsTheme { + type Class<'a> = TextInputStyle; - fn active(&self, style: &Self::Style) -> Appearance { - match style { - TextInputStyle::FormInput => { - Appearance { - background: TRANSPARENT.to_iced().into(), - border: Border { - radius: 4.0.into(), - width: 1.0, - color: BACKGROUND_DARKER.to_iced().into(), - }, - icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), - } - } - } + fn default<'a>() -> Self::Class<'a> { + TextInputStyle::FormInput } - fn focused(&self, style: &Self::Style) -> Appearance { - match style { - TextInputStyle::FormInput => { - Appearance { - background: BACKGROUND_DARKER.to_iced().into(), - border: Border { - radius: 4.0.into(), - width: 1.0, - color: BACKGROUND_DARKER.to_iced().into(), - }, - icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), - } - } - } - } - - fn disabled(&self, _: &Self::Style) -> Appearance { - Appearance { - background: BACKGROUND_DARKER.to_iced().into(), + fn style(&self, _class: &Self::Class<'_>, status: Status) -> Style { + let active = Style { + background: Background::Color(TRANSPARENT.to_iced().into()), border: Border { radius: 4.0.into(), width: 1.0, color: BACKGROUND_DARKER.to_iced().into(), }, - icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), + icon: TEXT_LIGHTEST.to_iced(), + placeholder: TEXT_DARKER.to_iced(), + value: TEXT_LIGHTEST.to_iced(), + selection: BACKGROUND_DARKER.to_iced(), + }; + + match status { + Status::Active => active, + Status::Hovered => Style { + background: Background::Color(BACKGROUND_DARKER.to_iced().into()), + ..active + }, + Status::Focused => Style { + background: Background::Color(BACKGROUND_DARKER.to_iced().into()), + ..active + }, + Status::Disabled => Style { + background: Background::Color(BACKGROUND_DARKER.to_iced().into()), + value: active.placeholder, + ..active + }, } } - - fn placeholder_color(&self, _: &Self::Style) -> Color { - TEXT_DARKER.to_iced() - } - - fn value_color(&self, _: &Self::Style) -> Color { - TEXT_LIGHTEST.to_iced() - } - - fn disabled_color(&self, style: &Self::Style) -> Color { - self.placeholder_color(style) - } - - fn selection_color(&self, _: &Self::Style) -> Color { - BACKGROUND_DARKER.to_iced() - } -} \ No newline at end of file +} diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index 0aac6b6..bc7afd7 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -1,18 +1,16 @@ use std::collections::HashMap; use std::time::Duration; -use iced::{Alignment, alignment, Application, color, Command, executor, font, futures, Length, Padding, Settings, Size, Subscription, time, window}; +use iced::{Alignment, alignment, font, futures, Length, Padding, Size, Subscription, time, window, Task, Renderer, padding}; use iced::advanced::text::Shaping; -use iced::advanced::Widget; -use iced::widget::{button, column, container, horizontal_rule, horizontal_space, mouse_area, row, scrollable, text}; -use iced_aw::{floating_element, Spinner}; -use iced_aw::core::icons; -use iced_aw::floating_element::{Anchor, Offset}; +use iced::widget::{button, column, container, horizontal_rule, horizontal_space, mouse_area, row, scrollable, stack, text, value}; +use iced_aw::Spinner; +use iced_fonts::{Bootstrap, BOOTSTRAP_FONT, BOOTSTRAP_FONT_BYTES}; use itertools::Itertools; -use common::model::{DownloadStatus, PhysicalShortcut, PluginId}; +use common::model::{DownloadStatus, PluginId}; use common::rpc::backend_api::{BackendApi, BackendApiError}; - +use common_ui::padding; use crate::theme::{Element, GauntletSettingsTheme}; use crate::theme::button::ButtonStyle; use crate::theme::container::ContainerStyle; @@ -21,14 +19,15 @@ use crate::views::general::{ManagementAppGeneralMsgIn, ManagementAppGeneralMsgOu use crate::views::plugins::{ManagementAppPluginMsgIn, ManagementAppPluginMsgOut, ManagementAppPluginsState}; pub fn run() { - ManagementAppModel::run(Settings { - id: None, - window: window::Settings { + iced::application::("Gauntlet Settings", update, view) + .window(window::Settings { size: Size::new(1000.0, 600.0), ..Default::default() - }, - ..Default::default() - }).expect("Unable to start settings application"); + }) + .subscription(subscription) + .theme(|_| GauntletSettingsTheme::default()) + .run_with(new) + .expect("Unable to start settings application"); } struct ManagementAppModel { @@ -79,695 +78,685 @@ pub enum DownloadInfo { Successful, } -//noinspection RsSortImplTraitMembers -impl Application for ManagementAppModel { - type Executor = executor::Default; - type Message = ManagementAppMsg; - type Theme = GauntletSettingsTheme; - type Flags = (); +fn new() -> (ManagementAppModel, Task) { + let backend_api = futures::executor::block_on(async { + anyhow::Ok(BackendApi::new().await?) + }) + .inspect_err(|err| tracing::error!("Unable to connect to server: {:?}", err)) + .ok(); - fn new(_flags: Self::Flags) -> (Self, Command) { - let backend_api = futures::executor::block_on(async { - anyhow::Ok(BackendApi::new().await?) - }) - .inspect_err(|err| tracing::error!("Unable to connect to server: {:?}", err)) - .ok(); + ( + ManagementAppModel { + backend_api: backend_api.clone(), + error_view: None, + downloads_info: HashMap::new(), + download_info_shown: false, + current_settings_view: SettingsView::Plugins, + general_state: ManagementAppGeneralState::new(backend_api.clone()), + plugins_state: ManagementAppPluginsState::new(backend_api.clone()), + }, + Task::batch([ + font::load(BOOTSTRAP_FONT_BYTES).map(ManagementAppMsg::FontLoaded), + Task::perform( + async {}, + |()| ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::RequestPluginReload) + ), + Task::perform( + async { + match backend_api { + Some(mut backend_api) => { + let shortcut = backend_api.get_global_shortcut() + .await; - ( - ManagementAppModel { - backend_api: backend_api.clone(), - error_view: None, - downloads_info: HashMap::new(), - download_info_shown: false, - current_settings_view: SettingsView::Plugins, - general_state: ManagementAppGeneralState::new(backend_api.clone()), - plugins_state: ManagementAppPluginsState::new(backend_api.clone()), - }, - Command::batch([ - font::load(icons::BOOTSTRAP_FONT_BYTES).map(ManagementAppMsg::FontLoaded), - Command::perform( - async {}, - |plugins| ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::RequestPluginReload) - ), - Command::perform( - async { - match backend_api { - Some(mut backend_api) => { - let shortcut = backend_api.get_global_shortcut() - .await; - - Some(shortcut) - } - None => None - } - }, - |shortcut| { - match shortcut { - None => ManagementAppMsg::General(ManagementAppGeneralMsgIn::Noop), - Some(shortcut) => { - match shortcut { - Ok((shortcut, error)) => ManagementAppMsg::General(ManagementAppGeneralMsgIn::RefreshShortcut { shortcut, error }), - Err(err) => ManagementAppMsg::HandleBackendError(err) - } - } + Some(shortcut) } + None => None } - ), - ]), - ) - } - - fn title(&self) -> String { - "Gauntlet Settings".to_owned() - } - - fn update(&mut self, message: Self::Message) -> Command { - let backend_api = match &self.backend_api { - Some(backend_api) => backend_api.clone(), - None => { - return Command::none() - } - }; - - match message { - ManagementAppMsg::Plugin(message) => { - self.plugins_state.update(message) - .map(|msg| { - match msg { - ManagementAppPluginMsgOut::PluginsReloaded(plugins) => { - ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::PluginsReloaded(plugins)) + }, + |shortcut| { + match shortcut { + None => ManagementAppMsg::General(ManagementAppGeneralMsgIn::Noop), + Some(shortcut) => { + match shortcut { + Ok((shortcut, error)) => ManagementAppMsg::General(ManagementAppGeneralMsgIn::RefreshShortcut { shortcut, error }), + Err(err) => ManagementAppMsg::HandleBackendError(err) } - ManagementAppPluginMsgOut::Noop => { - ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::Noop) - } - ManagementAppPluginMsgOut::DownloadPlugin { plugin_id } => { - ManagementAppMsg::DownloadPlugin { plugin_id } - } - ManagementAppPluginMsgOut::SelectedItem(selected_item) => { - ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::SelectItem(selected_item)) - } - ManagementAppPluginMsgOut::HandleBackendError(err) => { - ManagementAppMsg::HandleBackendError(err) - } - } - }) - } - ManagementAppMsg::General(message) => { - self.general_state.update(message) - .map(|msg| { - match msg { - ManagementAppGeneralMsgOut::Noop => { - ManagementAppMsg::General(ManagementAppGeneralMsgIn::Noop) - }, - ManagementAppGeneralMsgOut::HandleBackendError(err) => { - ManagementAppMsg::HandleBackendError(err) - } - } - }) - } - ManagementAppMsg::FontLoaded(result) => { - result.expect("unable to load font"); - Command::none() - } - ManagementAppMsg::SwitchView(view) => { - self.current_settings_view = view; - - Command::none() - } - ManagementAppMsg::HandleBackendError(err) => { - self.error_view = Some(match err { - BackendApiError::Timeout => ErrorView::Timeout, - BackendApiError::Internal { display } => ErrorView::UnknownError { display } - }); - - Command::none() - } - ManagementAppMsg::DownloadStatus { plugins } => { - for (plugin, status) in plugins { - match status { - DownloadStatus::InProgress => { - self.downloads_info.insert(plugin.clone(), DownloadInfo::InProgress); - } - DownloadStatus::Done => { - self.downloads_info.insert(plugin.clone(), DownloadInfo::Successful); - } - DownloadStatus::Failed { message } => { - self.downloads_info.insert(plugin.clone(), DownloadInfo::Error { message }); } } } + ), + ]), + ) +} - let mut backend_api = backend_api.clone(); +fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task { + let backend_api = match &state.backend_api { + Some(backend_api) => backend_api.clone(), + None => { + return Task::none() + } + }; - Command::perform( + match message { + ManagementAppMsg::Plugin(message) => { + state.plugins_state.update(message) + .map(|msg| { + match msg { + ManagementAppPluginMsgOut::PluginsReloaded(plugins) => { + ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::PluginsReloaded(plugins)) + } + ManagementAppPluginMsgOut::Noop => { + ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::Noop) + } + ManagementAppPluginMsgOut::DownloadPlugin { plugin_id } => { + ManagementAppMsg::DownloadPlugin { plugin_id } + } + ManagementAppPluginMsgOut::SelectedItem(selected_item) => { + ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::SelectItem(selected_item)) + } + ManagementAppPluginMsgOut::HandleBackendError(err) => { + ManagementAppMsg::HandleBackendError(err) + } + } + }) + } + ManagementAppMsg::General(message) => { + state.general_state.update(message) + .map(|msg| { + match msg { + ManagementAppGeneralMsgOut::Noop => { + ManagementAppMsg::General(ManagementAppGeneralMsgIn::Noop) + }, + ManagementAppGeneralMsgOut::HandleBackendError(err) => { + ManagementAppMsg::HandleBackendError(err) + } + } + }) + } + ManagementAppMsg::FontLoaded(result) => { + result.expect("unable to load font"); + Task::none() + } + ManagementAppMsg::SwitchView(view) => { + state.current_settings_view = view; + + Task::none() + } + ManagementAppMsg::HandleBackendError(err) => { + state.error_view = Some(match err { + BackendApiError::Timeout => ErrorView::Timeout, + BackendApiError::Internal { display } => ErrorView::UnknownError { display } + }); + + Task::none() + } + ManagementAppMsg::DownloadStatus { plugins } => { + for (plugin, status) in plugins { + match status { + DownloadStatus::InProgress => { + state.downloads_info.insert(plugin.clone(), DownloadInfo::InProgress); + } + DownloadStatus::Done => { + state.downloads_info.insert(plugin.clone(), DownloadInfo::Successful); + } + DownloadStatus::Failed { message } => { + state.downloads_info.insert(plugin.clone(), DownloadInfo::Error { message }); + } + } + } + + let mut backend_api = backend_api.clone(); + + Task::perform( + async move { + let plugins = backend_api.plugins() + .await?; + + Ok(plugins) + }, + |result| handle_backend_error(result, |plugins| ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::PluginsReloaded(plugins))) + ) + } + ManagementAppMsg::CheckDownloadStatus => { + if state.downloads_info.is_empty() { + Task::none() + } else { + let mut backend_client = backend_api.clone(); + + Task::perform( async move { - let plugins = backend_api.plugins() + let plugins = backend_client.download_status() .await?; Ok(plugins) }, - |result| handle_backend_error(result, |plugins| ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::PluginsReloaded(plugins))) + |result| handle_backend_error(result, |plugins| ManagementAppMsg::DownloadStatus { plugins }), ) } - ManagementAppMsg::CheckDownloadStatus => { - if self.downloads_info.is_empty() { - Command::none() - } else { - let mut backend_client = backend_api.clone(); + } + ManagementAppMsg::DownloadPlugin { plugin_id } => { + let mut backend_client = backend_api.clone(); - Command::perform( - async move { - let plugins = backend_client.download_status() - .await?; + let already_downloading = state.downloads_info.insert(plugin_id.clone(), DownloadInfo::InProgress) + .is_some(); - Ok(plugins) - }, - |result| handle_backend_error(result, |plugins| ManagementAppMsg::DownloadStatus { plugins }), - ) - } + if already_downloading { + Task::none() + } else { + Task::perform( + async move { + backend_client.download_plugin(plugin_id) + .await?; + + Ok(()) + }, + |result| handle_backend_error(result, |()| ManagementAppMsg::Noop) + ) } - ManagementAppMsg::DownloadPlugin { plugin_id } => { - let mut backend_client = backend_api.clone(); + } + ManagementAppMsg::Noop => Task::none(), + ManagementAppMsg::ToggleDownloadInfo => { + state.download_info_shown = !state.download_info_shown; + Task::none() + } + } +} - let already_downloading = self.downloads_info.insert(plugin_id.clone(), DownloadInfo::InProgress) - .is_some(); +fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { + if let None = &state.backend_api { + let description: Element<_> = text("Unable to connect to server. Please check if you have Gauntlet running on your PC") + .into(); - if already_downloading { - Command::none() - } else { - Command::perform( - async move { - backend_client.download_plugin(plugin_id) - .await?; + let content: Element<_> = container(description) + .align_x(Alignment::Center) + .align_y(Alignment::Center) + .width(Length::Fill) + .height(Length::Fill) + .into(); - Ok(()) - }, - |result| handle_backend_error(result, |()| ManagementAppMsg::Noop) - ) - } + return content + } + + if let Some(err) = &state.error_view { + return match err { + ErrorView::Timeout => { + let description: Element<_> = text("Error occurred") + .into(); + + let description = container(description) + .width(Length::Fill) + .align_x(Alignment::Center) + .padding(12) + .into(); + + let sub_description: Element<_> = text("Backend was unable to process message in a timely manner") + .into(); + + let sub_description = container(sub_description) + .width(Length::Fill) + .align_x(Alignment::Center) + .padding(12) + .into(); + + let content: Element<_> = column([ + description, + sub_description, + ]).into(); + + let content: Element<_> = container(content) + .align_x(Alignment::Center) + .align_y(Alignment::Center) + .width(Length::Fill) + .height(Length::Fill) + .into(); + + content } - ManagementAppMsg::Noop => Command::none(), - ManagementAppMsg::ToggleDownloadInfo => { - self.download_info_shown = !self.download_info_shown; - Command::none() + ErrorView::UnknownError { display } => { + let description: Element<_> = text("Unknown error occurred") + .into(); + + let description = container(description) + .width(Length::Fill) + .align_x(Alignment::Center) + .padding(12) + .into(); + + let sub_description: Element<_> = text("Please report") + .into(); + + let sub_description = container(sub_description) + .width(Length::Fill) + .align_x(Alignment::Center) + .padding(12) + .into(); + + let error_description: Element<_> = text(display) + .shaping(Shaping::Advanced) + .into(); + + let error_description = container(error_description) + .width(Length::Fill) + .align_x(Alignment::Center) + .padding(12) + .into(); + + let content: Element<_> = column([ + description, + sub_description, + error_description, + ]).into(); + + let content: Element<_> = container(content) + .align_x(Alignment::Center) + .align_y(Alignment::Center) + .width(Length::Fill) + .height(Length::Fill) + .into(); + + content } } } - fn view(&self) -> Element<'_, Self::Message> { - if let None = &self.backend_api { - let description: Element<_> = text("Unable to connect to server. Please check if you have Gauntlet running on your PC") - .into(); - let content: Element<_> = container(description) - .center_x() - .center_y() - .width(Length::Fill) + let content = match state.current_settings_view { + SettingsView::General => { + state.general_state.view() + .map(|msg| ManagementAppMsg::General(msg)) + } + SettingsView::Plugins => { + state.plugins_state.view() + .map(|msg| ManagementAppMsg::Plugin(msg)) + } + }; + + let icon_general: Element<_> = value(Bootstrap::GearFill) + .font(BOOTSTRAP_FONT) + .height(Length::Fill) + .width(Length::Fill) + .align_y(alignment::Vertical::Center) + .align_x(alignment::Horizontal::Center) + .into(); + + let text_general: Element<_> = text("General") + .height(Length::Fill) + .align_y(alignment::Vertical::Center) + .align_x(alignment::Horizontal::Center) + .into(); + + let general_button: Element<_> = column(vec![icon_general, text_general]) + .align_x(Alignment::Center) + .height(Length::Fill) + .width(Length::Fill) + .into(); + + let general_button: Element<_> = button(general_button) + .on_press(ManagementAppMsg::SwitchView(SettingsView::General)) + .height(Length::Fill) + .width(80) + .class(if state.current_settings_view == SettingsView::General { ButtonStyle::ViewSwitcherSelected } else { ButtonStyle::ViewSwitcher }) + .into(); + + let general_button: Element<_> = container(general_button) + .padding(8.0) + .into(); + + let icon_plugins: Element<_> = value(Bootstrap::PuzzleFill) + .font(BOOTSTRAP_FONT) + .height(Length::Fill) + .width(Length::Fill) + .align_y(alignment::Vertical::Center) + .align_x(alignment::Horizontal::Center) + .into(); + + let text_plugins: Element<_> = text("Plugins") + .height(Length::Fill) + .align_y(alignment::Vertical::Center) + .align_x(alignment::Horizontal::Center) + .into(); + + let plugins_button: Element<_> = column(vec![icon_plugins, text_plugins]) + .align_x(Alignment::Center) + .height(Length::Fill) + .width(Length::Fill) + .into(); + + let plugins_button: Element<_> = button(plugins_button) + .on_press(ManagementAppMsg::SwitchView(SettingsView::Plugins)) + .height(Length::Fill) + .width(80) + .class(if state.current_settings_view == SettingsView::Plugins { ButtonStyle::ViewSwitcherSelected } else { ButtonStyle::ViewSwitcher }) + .into(); + + let plugins_button: Element<_> = container(plugins_button) + .padding(8.0) + .into(); + + let top_bar_buttons: Element<_> = row(vec![general_button, plugins_button]) + .into(); + + let top_bar_buttons: Element<_> = container(top_bar_buttons) + .width(Length::Fill) + .align_x(alignment::Horizontal::Center) + .into(); + + let top_bar_left_space: Element<_> = horizontal_space() + .width(Length::Fill) + .into(); + + let top_bar_right = { + let mut successful_count = 0; + let mut in_progress_count = 0; + let mut error_count = 0; + + for (_, download_info) in state.downloads_info.iter() { + match download_info { + DownloadInfo::Successful => { + successful_count += 1; + } + DownloadInfo::InProgress => { + in_progress_count += 1; + } + DownloadInfo::Error { .. } => { + error_count += 1; + } + } + } + + let mut download_info_icons = vec![]; + + if in_progress_count > 0 { + let spinner: Element<_> = Spinner::new() + .width(Length::Fixed(16.0)) .height(Length::Fill) .into(); - return content - } - - if let Some(err) = &self.error_view { - return match err { - ErrorView::Timeout => { - let description: Element<_> = text("Error occurred") - .into(); - - let description = container(description) - .width(Length::Fill) - .center_x() - .padding(12) - .into(); - - let sub_description: Element<_> = text("Backend was unable to process message in a timely manner") - .into(); - - let sub_description = container(sub_description) - .width(Length::Fill) - .center_x() - .padding(12) - .into(); - - let content: Element<_> = column([ - description, - sub_description, - ]).into(); - - let content: Element<_> = container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .into(); - - content - } - ErrorView::UnknownError { display } => { - let description: Element<_> = text("Unknown error occurred") - .into(); - - let description = container(description) - .width(Length::Fill) - .center_x() - .padding(12) - .into(); - - let sub_description: Element<_> = text("Please report") - .into(); - - let sub_description = container(sub_description) - .width(Length::Fill) - .center_x() - .padding(12) - .into(); - - let error_description: Element<_> = text(display) - .shaping(Shaping::Advanced) - .into(); - - let error_description = container(error_description) - .width(Length::Fill) - .center_x() - .padding(12) - .into(); - - let content: Element<_> = column([ - description, - sub_description, - error_description, - ]).into(); - - let content: Element<_> = container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .into(); - - content - } - } - } - - - let content = match self.current_settings_view { - SettingsView::General => { - self.general_state.view() - .map(|msg| ManagementAppMsg::General(msg)) - } - SettingsView::Plugins => { - self.plugins_state.view() - .map(|msg| ManagementAppMsg::Plugin(msg)) - } - }; - - let icon_general: Element<_> = text(icons::Bootstrap::GearFill) - .font(icons::BOOTSTRAP_FONT) - .height(Length::Fill) - .width(Length::Fill) - .vertical_alignment(alignment::Vertical::Center) - .horizontal_alignment(alignment::Horizontal::Center) - .into(); - - let text_general: Element<_> = text("General") - .height(Length::Fill) - .vertical_alignment(alignment::Vertical::Center) - .horizontal_alignment(alignment::Horizontal::Center) - .into(); - - let general_button: Element<_> = column(vec![icon_general, text_general]) - .align_items(Alignment::Center) - .height(Length::Fill) - .width(Length::Fill) - .into(); - - let general_button: Element<_> = button(general_button) - .on_press(ManagementAppMsg::SwitchView(SettingsView::General)) - .height(Length::Fill) - .width(80) - .style(if self.current_settings_view == SettingsView::General { ButtonStyle::ViewSwitcherSelected } else { ButtonStyle::ViewSwitcher }) - .into(); - - let general_button: Element<_> = container(general_button) - .padding(8.0) - .into(); - - let icon_plugins: Element<_> = text(icons::Bootstrap::PuzzleFill) - .font(icons::BOOTSTRAP_FONT) - .height(Length::Fill) - .width(Length::Fill) - .vertical_alignment(alignment::Vertical::Center) - .horizontal_alignment(alignment::Horizontal::Center) - .into(); - - let text_plugins: Element<_> = text("Plugins") - .height(Length::Fill) - .vertical_alignment(alignment::Vertical::Center) - .horizontal_alignment(alignment::Horizontal::Center) - .into(); - - let plugins_button: Element<_> = column(vec![icon_plugins, text_plugins]) - .align_items(Alignment::Center) - .height(Length::Fill) - .width(Length::Fill) - .into(); - - let plugins_button: Element<_> = button(plugins_button) - .on_press(ManagementAppMsg::SwitchView(SettingsView::Plugins)) - .height(Length::Fill) - .width(80) - .style(if self.current_settings_view == SettingsView::Plugins { ButtonStyle::ViewSwitcherSelected } else { ButtonStyle::ViewSwitcher }) - .into(); - - let plugins_button: Element<_> = container(plugins_button) - .padding(8.0) - .into(); - - let top_bar_buttons: Element<_> = row(vec![general_button, plugins_button]) - .into(); - - let top_bar_buttons: Element<_> = container(top_bar_buttons) - .width(Length::Fill) - .align_x(alignment::Horizontal::Center) - .into(); - - let top_bar_left_space: Element<_> = horizontal_space() - .width(Length::Fill) - .into(); - - let top_bar_right = { - let mut successful_count = 0; - let mut in_progress_count = 0; - let mut error_count = 0; - - for (_, download_info) in self.downloads_info.iter() { - match download_info { - DownloadInfo::Successful => { - successful_count += 1; - } - DownloadInfo::InProgress => { - in_progress_count += 1; - } - DownloadInfo::Error { .. } => { - error_count += 1; - } - } - } - - let mut download_info_icons = vec![]; - - if in_progress_count > 0 { - let spinner: Element<_> = Spinner::new() - .width(Length::Fixed(16.0)) - .height(Length::Fill) - .into(); - - let spinner: Element<_> = container(spinner) - .height(Length::Fill) - .into(); - - let text: Element<_> = text(in_progress_count) - .height(Length::Fill) - .vertical_alignment(alignment::Vertical::Center) - .into(); - - let spinner: Element<_> = row(vec![text, spinner]) - .spacing(8.0) - .into(); - - download_info_icons.push(spinner); - } - if successful_count > 0 { - let icon: Element<_> = text(icons::Bootstrap::PatchCheckFill) - .size(16) - .vertical_alignment(alignment::Vertical::Center) - .font(icons::BOOTSTRAP_FONT) - .height(Length::Fill) - .style(TextStyle::Positive) - .into(); - - let icon: Element<_> = container(icon) - .height(Length::Fill) - .into(); - - let text: Element<_> = text(successful_count) - .height(Length::Fill) - .vertical_alignment(alignment::Vertical::Center) - .into(); - - let icon: Element<_> = row(vec![text, icon]) - .spacing(8.0) - .into(); - - download_info_icons.push(icon); - } - if error_count > 0 { - let icon: Element<_> = text(icons::Bootstrap::ExclamationTriangleFill) - .font(icons::BOOTSTRAP_FONT) - .height(Length::Fill) - .vertical_alignment(alignment::Vertical::Center) - .size(16) - .style(TextStyle::Destructive) - .into(); - - let icon: Element<_> = container(icon) - .height(Length::Fill) - .into(); - - let text: Element<_> = text(error_count) - .height(Length::Fill) - .vertical_alignment(alignment::Vertical::Center) - .into(); - - let icon: Element<_> = row(vec![text, icon]) - .spacing(8.0) - .into(); - - download_info_icons.push(icon); - } - - if download_info_icons.is_empty() { - horizontal_space() - .width(Length::Fill) - .into() - } else { - let top_bar_right: Element<_> = row(download_info_icons) - .spacing(12.0) - .height(Length::Fill) - .align_items(Alignment::Center) - .into(); - - let top_bar_right: Element<_> = button(top_bar_right) - .style(ButtonStyle::DownloadInfo) - .on_press(ManagementAppMsg::ToggleDownloadInfo) - .padding(Padding::from([4, 8])) - .height(Length::Fill) - .into(); - - let top_bar_right: Element<_> = container(top_bar_right) - .height(Length::Fill) - .padding(Padding::from([18.0, 12.0])) - .into(); - - let top_bar_right: Element<_> = container(top_bar_right) - .width(Length::Fill) - .height(Length::Fill) - .center_y() - .align_x(alignment::Horizontal::Right) - .into(); - - top_bar_right - } - }; - - let top_bar: Element<_> = row(vec![top_bar_left_space, top_bar_buttons, top_bar_right]) - .width(Length::Fill) - .into(); - - let top_bar: Element<_> = container(top_bar) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Shrink) - .max_height(70) - .into(); - - let separator: Element<_> = horizontal_rule(1) - .into(); - - let content: Element<_> = column(vec![top_bar, separator, content]) - .into(); - - let download_info_panel: Element<_> = { - let downloads: Vec> = self.downloads_info.iter() - .sorted_by_key(|(_, info)| info.clone()) - .map(|(plugin_id, info)| { - match info { - DownloadInfo::InProgress => { - let kind_text: Element<_> = text("Download in progress") - .into(); - - let kind_text: Element<_> = container(kind_text) - .padding(Padding::from([16, 0, 8, 0])) - .into(); - - let plugin_id: Element<_> = text(plugin_id.to_string()) - .shaping(Shaping::Advanced) - .style(TextStyle::Subtitle) - .size(14) - .into(); - - let plugin_id: Element<_> = container(plugin_id) - .padding(Padding::from([0, 0, 16, 0])) - .into(); - - let spinner: Element<_> = Spinner::new() - .width(Length::Fixed(32.0)) - .into(); - - let spinner: Element<_> = container(spinner) - .padding(16) - .into(); - - let content: Element<_> = column(vec![kind_text, plugin_id]) - .into(); - - let content: Element<_> = row(vec![spinner, content]) - .into(); - - container(content) - .width(Length::Fill) - .into() - } - DownloadInfo::Error { message } => { - let kind_text: Element<_> = text("Download failed") - .into(); - - let kind_text: Element<_> = container(kind_text) - .padding(Padding::from([16, 0, 8, 0])) - .into(); - - let plugin_id: Element<_> = text(plugin_id.to_string()) - .shaping(Shaping::Advanced) - .style(TextStyle::Subtitle) - .size(14) - .into(); - - let icon: Element<_> = text(icons::Bootstrap::ExclamationTriangleFill) - .font(icons::BOOTSTRAP_FONT) - .vertical_alignment(alignment::Vertical::Center) - .size(32) - .style(TextStyle::Destructive) - .into(); - - let icon: Element<_> = container(icon) - .padding(16) - .into(); - - let message: Element<_> = text(message.to_string()) - .shaping(Shaping::Advanced) - .into(); - - let message: Element<_> = container(message) - .padding(Padding::from([8, 0, 16, 0])) - .into(); - - let content: Element<_> = column(vec![kind_text, plugin_id, message]) - .into(); - - let content: Element<_> = row(vec![icon, content]) - .into(); - - container(content) - .width(Length::Fill) - .into() - } - DownloadInfo::Successful => { - let kind_text: Element<_> = text("Download successful") - .into(); - - let kind_text: Element<_> = container(kind_text) - .padding(Padding::from([16, 0, 8, 0])) - .into(); - - let plugin_id: Element<_> = text(plugin_id.to_string()) - .shaping(Shaping::Advanced) - .size(14) - .style(TextStyle::Subtitle) - .into(); - - let plugin_id: Element<_> = container(plugin_id) - .padding(Padding::from([0, 0, 16, 0])) - .into(); - - let icon: Element<_> = text(icons::Bootstrap::PatchCheckFill) - .size(32) - .vertical_alignment(alignment::Vertical::Center) - .font(icons::BOOTSTRAP_FONT) - .style(TextStyle::Positive) - .into(); - - let icon: Element<_> = container(icon) - .padding(16) - .into(); - - let content: Element<_> = column(vec![kind_text, plugin_id]) - .into(); - - let content: Element<_> = row(vec![icon, content]) - .into(); - - container(content) - .width(Length::Fill) - .into() - } - } - }) - .intersperse_with(|| horizontal_rule(1).into()) - .collect(); - - let downloads: Element<_> = column(downloads) + let spinner: Element<_> = container(spinner) + .height(Length::Fill) .into(); - let downloads: Element<_> = scrollable(downloads) + let text: Element<_> = text(in_progress_count) + .height(Length::Fill) + .align_y(alignment::Vertical::Center) + .into(); + + let spinner: Element<_> = row(vec![text, spinner]) + .spacing(8.0) + .into(); + + download_info_icons.push(spinner); + } + if successful_count > 0 { + let icon: Element<_> = value(Bootstrap::PatchCheckFill) + .size(16) + .align_y(alignment::Vertical::Center) + .font(BOOTSTRAP_FONT) + .height(Length::Fill) + .class(TextStyle::Positive) + .into(); + + let icon: Element<_> = container(icon) + .height(Length::Fill) + .into(); + + let text: Element<_> = text(successful_count) + .height(Length::Fill) + .align_y(alignment::Vertical::Center) + .into(); + + let icon: Element<_> = row(vec![text, icon]) + .spacing(8.0) + .into(); + + download_info_icons.push(icon); + } + if error_count > 0 { + let icon: Element<_> = value(Bootstrap::ExclamationTriangleFill) + .font(BOOTSTRAP_FONT) + .height(Length::Fill) + .align_y(alignment::Vertical::Center) + .size(16) + .class(TextStyle::Destructive) + .into(); + + let icon: Element<_> = container(icon) + .height(Length::Fill) + .into(); + + let text: Element<_> = text(error_count) + .height(Length::Fill) + .align_y(alignment::Vertical::Center) + .into(); + + let icon: Element<_> = row(vec![text, icon]) + .spacing(8.0) + .into(); + + download_info_icons.push(icon); + } + + if download_info_icons.is_empty() { + horizontal_space() .width(Length::Fill) + .into() + } else { + let top_bar_right: Element<_> = row(download_info_icons) + .spacing(12.0) + .height(Length::Fill) + .align_y(Alignment::Center) .into(); - container(downloads) - .padding(4) - .width(Length::Fixed(400.0)) - .max_height(500.0) - .style(ContainerStyle::Box) - .into() - }; - - let content = if !self.download_info_shown { - content - } else { - mouse_area(content) + let top_bar_right: Element<_> = button(top_bar_right) + .class(ButtonStyle::DownloadInfo) .on_press(ManagementAppMsg::ToggleDownloadInfo) - .into() - }; + .padding(Padding::from([4, 8])) + .height(Length::Fill) + .into(); - floating_element(content, download_info_panel) - .offset(Offset::from([8.0, 60.0])) - .anchor(Anchor::NorthEast) - .hide(!self.download_info_shown) + let top_bar_right: Element<_> = container(top_bar_right) + .height(Length::Fill) + .padding(Padding::from([18.0, 12.0])) + .into(); + + let top_bar_right: Element<_> = container(top_bar_right) + .width(Length::Fill) + .height(Length::Fill) + .center_y(Length::Fill) + .align_x(alignment::Horizontal::Right) + .into(); + + top_bar_right + } + }; + + let top_bar: Element<_> = row(vec![top_bar_left_space, top_bar_buttons, top_bar_right]) + .width(Length::Fill) + .into(); + + let top_bar: Element<_> = container(top_bar) + .center_x(Length::Fill) + .center_y(Length::Fill) + .width(Length::Fill) + .height(Length::Shrink) + .max_height(70) + .into(); + + let separator: Element<_> = horizontal_rule(1) + .into(); + + let content: Element<_> = column(vec![top_bar, separator, content]) + .into(); + + let download_info_panel: Element<_> = { + let downloads: Vec> = state.downloads_info.iter() + .sorted_by_key(|(_, info)| info.clone()) + .map(|(plugin_id, info)| { + match info { + DownloadInfo::InProgress => { + let kind_text: Element<_> = text("Download in progress") + .into(); + + let kind_text: Element<_> = container(kind_text) + .padding(padding(16, 0, 8, 0)) + .into(); + + let plugin_id: Element<_> = text(plugin_id.to_string()) + .shaping(Shaping::Advanced) + .class(TextStyle::Subtitle) + .size(14) + .into(); + + let plugin_id: Element<_> = container(plugin_id) + .padding(padding::bottom(16)) + .into(); + + let spinner: Element<_> = Spinner::new() + .width(Length::Fixed(32.0)) + .into(); + + let spinner: Element<_> = container(spinner) + .padding(16) + .into(); + + let content: Element<_> = column(vec![kind_text, plugin_id]) + .into(); + + let content: Element<_> = row(vec![spinner, content]) + .into(); + + container(content) + .width(Length::Fill) + .into() + } + DownloadInfo::Error { message } => { + let kind_text: Element<_> = text("Download failed") + .into(); + + let kind_text: Element<_> = container(kind_text) + .padding(padding(16, 0, 8, 0)) + .into(); + + let plugin_id: Element<_> = text(plugin_id.to_string()) + .shaping(Shaping::Advanced) + .class(TextStyle::Subtitle) + .size(14) + .into(); + + let icon: Element<_> = value(Bootstrap::ExclamationTriangleFill) + .font(BOOTSTRAP_FONT) + .align_y(alignment::Vertical::Center) + .size(32) + .class(TextStyle::Destructive) + .into(); + + let icon: Element<_> = container(icon) + .padding(16) + .into(); + + let message: Element<_> = text(message.to_string()) + .shaping(Shaping::Advanced) + .into(); + + let message: Element<_> = container(message) + .padding(padding(8, 0, 16, 0)) + .into(); + + let content: Element<_> = column(vec![kind_text, plugin_id, message]) + .into(); + + let content: Element<_> = row(vec![icon, content]) + .into(); + + container(content) + .width(Length::Fill) + .into() + } + DownloadInfo::Successful => { + let kind_text: Element<_> = text("Download successful") + .into(); + + let kind_text: Element<_> = container(kind_text) + .padding(padding(16, 0, 8, 0)) + .into(); + + let plugin_id: Element<_> = text(plugin_id.to_string()) + .shaping(Shaping::Advanced) + .size(14) + .class(TextStyle::Subtitle) + .into(); + + let plugin_id: Element<_> = container(plugin_id) + .padding(padding::bottom(16)) + .into(); + + let icon: Element<_> = value(Bootstrap::PatchCheckFill) + .size(32) + .align_y(alignment::Vertical::Center) + .font(BOOTSTRAP_FONT) + .class(TextStyle::Positive) + .into(); + + let icon: Element<_> = container(icon) + .padding(16) + .into(); + + let content: Element<_> = column(vec![kind_text, plugin_id]) + .into(); + + let content: Element<_> = row(vec![icon, content]) + .into(); + + container(content) + .width(Length::Fill) + .into() + } + } + }) + .intersperse_with(|| horizontal_rule(1).into()) + .collect(); + + let downloads: Element<_> = column(downloads) + .into(); + + let downloads: Element<_> = scrollable(downloads) + .width(Length::Fill) + .into(); + + let content: Element<_> = container(downloads) + .padding(4) + .width(Length::Fixed(400.0)) + .max_height(500.0) + .class(ContainerStyle::Box) + .into(); + + container(content) + .padding(common_ui::padding(8.0, 60.0, 0.0, 0.0)) + .align_right(Length::Fill) + .align_top(Length::Fill) .into() + }; + + let content: Element<_> = mouse_area(content) + .on_press(if state.download_info_shown { ManagementAppMsg::ToggleDownloadInfo } else { ManagementAppMsg::Noop }) + .into(); + + let mut content = vec![content]; + + if state.download_info_shown { + content.push(download_info_panel); } - fn subscription(&self) -> Subscription { - time::every(Duration::from_millis(300)) - .map(|_| ManagementAppMsg::CheckDownloadStatus) - } - - fn theme(&self) -> Self::Theme { - GauntletSettingsTheme::default() - } + stack(content) + .into() } +fn subscription(_state: &ManagementAppModel) -> Subscription { + time::every(Duration::from_millis(300)) + .map(|_| ManagementAppMsg::CheckDownloadStatus) +} + + pub fn handle_backend_error(result: Result, convert: impl FnOnce(T) -> ManagementAppMsg) -> ManagementAppMsg { match result { Ok(val) => convert(val), diff --git a/rust/management_client/src/views/general.rs b/rust/management_client/src/views/general.rs index 570bf5d..8b0ba93 100644 --- a/rust/management_client/src/views/general.rs +++ b/rust/management_client/src/views/general.rs @@ -1,5 +1,4 @@ use crate::components::shortcut_selector::ShortcutSelector; -use crate::theme::shortcut_selector::ShortcutSelectorStyle; use crate::theme::text::TextStyle; use crate::theme::Element; use common::model::PhysicalShortcut; @@ -7,9 +6,9 @@ use common::rpc::backend_api::{BackendApi, BackendApiError}; use iced::alignment::Horizontal; use iced::widget::text::Shaping; use iced::widget::tooltip::Position; -use iced::widget::{column, container, row, text, tooltip, Space}; -use iced::{alignment, Alignment, Command, Length, Padding}; -use iced_aw::core::icons; +use iced::widget::{column, container, row, text, tooltip, value, Space}; +use iced::{alignment, Alignment, Length, Padding, Task}; +use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; use crate::theme::container::ContainerStyle; pub struct ManagementAppGeneralState { @@ -46,11 +45,11 @@ impl ManagementAppGeneralState { } } - pub fn update(&mut self, message: ManagementAppGeneralMsgIn) -> Command { + pub fn update(&mut self, message: ManagementAppGeneralMsgIn) -> Task { let backend_api = match &self.backend_api { Some(backend_api) => backend_api.clone(), None => { - return Command::none() + return Task::none() } }; @@ -60,7 +59,7 @@ impl ManagementAppGeneralState { let mut backend_api = backend_api.clone(); - Command::perform(async move { + Task::perform(async move { backend_api.set_global_shortcut(shortcut) .await?; @@ -68,18 +67,18 @@ impl ManagementAppGeneralState { }, |result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop)) } ManagementAppGeneralMsgIn::Noop => { - Command::none() + Task::none() } ManagementAppGeneralMsgIn::RefreshShortcut { shortcut, error } => { self.current_shortcut = shortcut; self.current_shortcut_error = error; - Command::perform(async move {}, |_| ManagementAppGeneralMsgOut::Noop) + Task::perform(async move {}, |_| ManagementAppGeneralMsgOut::Noop) } ManagementAppGeneralMsgIn::CapturingChanged(capturing) => { self.currently_capturing = capturing; - Command::none() + Task::none() } } } @@ -90,7 +89,6 @@ impl ManagementAppGeneralState { &self.current_shortcut, move |value| { ManagementAppGeneralMsgIn::ShortcutCaptured(value) }, move |value| { ManagementAppGeneralMsgIn::CapturingChanged(value) }, - ShortcutSelectorStyle::Default ).into(); let field: Element<_> = container(shortcut_selector) @@ -114,10 +112,10 @@ impl ManagementAppGeneralState { content } - fn view_field<'a>(&self, label: &str, input: Element<'a, ManagementAppGeneralMsgIn>) -> Element<'a, ManagementAppGeneralMsgIn> { + fn view_field<'a>(&'a self, label: &'a str, input: Element<'a, ManagementAppGeneralMsgIn>) -> Element<'a, ManagementAppGeneralMsgIn> { let label: Element<_> = text(label) .shaping(Shaping::Advanced) - .horizontal_alignment(Horizontal::Right) + .align_x(Horizontal::Right) .width(Length::Fill) .into(); @@ -134,34 +132,34 @@ impl ManagementAppGeneralState { let after = if self.currently_capturing { let hint1: Element<_> = text("Backspace - Unset Shortcut") .width(Length::Fill) - .style(TextStyle::Subtitle) + .class(TextStyle::Subtitle) .into(); let hint2: Element<_> = text("Escape - Stop Capturing") .width(Length::Fill) - .style(TextStyle::Subtitle) + .class(TextStyle::Subtitle) .into(); column(vec![hint1, hint2]) .width(Length::FillPortion(3)) - .align_items(Alignment::Center) + .align_x(Alignment::Center) .padding(Padding::from([0.0, 8.0])) .into() } else { if let Some(current_shortcut_error) = &self.current_shortcut_error { - let error_icon: Element<_> = text(icons::Bootstrap::ExclamationTriangleFill) - .font(icons::BOOTSTRAP_FONT) - .style(TextStyle::Destructive) + let error_icon: Element<_> = value(Bootstrap::ExclamationTriangleFill) + .font(BOOTSTRAP_FONT) + .class(TextStyle::Destructive) .into(); let error_text: Element<_> = text(current_shortcut_error) - .style(TextStyle::Destructive) + .class(TextStyle::Destructive) .into(); let error_text: Element<_> = container(error_text) .padding(16.0) .max_width(300) - .style(ContainerStyle::Box) + .class(ContainerStyle::Box) .into(); let tooltip: Element<_> = tooltip(error_icon, error_text, Position::Bottom) @@ -187,7 +185,7 @@ impl ManagementAppGeneralState { ]; let row: Element<_> = row(content) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .padding(12) .into(); diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index fc5d898..06444da 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -2,11 +2,10 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use iced::{Alignment, Command, Length, Padding}; -use iced::widget::{button, column, container, row, scrollable, text, text_input, vertical_rule}; +use iced::{padding, Alignment, Length, Padding, Task}; +use iced::widget::{button, column, container, row, scrollable, text, text_input, value, vertical_rule}; use iced::widget::text::Shaping; -use iced_aw::core::icons; - +use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; use common::{settings_env_data_from_string, SettingsEnvData}; use common::model::{EntrypointId, PluginId, PluginPreferenceUserData, SettingsPlugin}; use common::rpc::backend_api::{BackendApi, BackendApiError}; @@ -85,11 +84,11 @@ impl ManagementAppPluginsState { } } - pub fn update(&mut self, message: ManagementAppPluginMsgIn) -> Command { + pub fn update(&mut self, message: ManagementAppPluginMsgIn) -> Task { let backend_api = match &self.backend_api { Some(backend_api) => backend_api.clone(), None => { - return Command::none() + return Task::none() } }; @@ -102,7 +101,7 @@ impl ManagementAppPluginsState { PluginTableMsgOut::SetPluginState { enabled, plugin_id } => { let mut backend_client = backend_api.clone(); - Command::perform( + Task::perform( async move { backend_client.set_plugin_state(plugin_id, enabled) .await?; @@ -118,7 +117,7 @@ impl ManagementAppPluginsState { PluginTableMsgOut::SetEntrypointState { enabled, plugin_id, entrypoint_id } => { let mut backend_client = backend_api.clone(); - Command::perform( + Task::perform( async move { backend_client.set_entrypoint_state(plugin_id, entrypoint_id, enabled) .await?; @@ -132,7 +131,7 @@ impl ManagementAppPluginsState { ) } PluginTableMsgOut::SelectItem(selected_item) => { - Command::perform(async move { selected_item }, ManagementAppPluginMsgOut::SelectedItem) + Task::perform(async move { selected_item }, ManagementAppPluginMsgOut::SelectedItem) } PluginTableMsgOut::ToggleShowEntrypoints { plugin_id } => { let plugins = { @@ -145,7 +144,7 @@ impl ManagementAppPluginsState { self.apply_plugin_reload(plugins); - Command::none() + Task::none() } } } @@ -159,7 +158,7 @@ impl ManagementAppPluginsState { let mut backend_api = backend_api.clone(); - Command::perform( + Task::perform( async move { backend_api.set_preference_value(plugin_id, entrypoint_id, id, user_data.to_user_data()) .await?; @@ -174,7 +173,7 @@ impl ManagementAppPluginsState { ManagementAppPluginMsgIn::RequestPluginReload => { let mut backend_api = backend_api.clone(); - Command::perform( + Task::perform( async move { let plugins = backend_api.plugins() .await?; @@ -187,14 +186,14 @@ impl ManagementAppPluginsState { ManagementAppPluginMsgIn::PluginsReloaded(plugins) => { self.apply_plugin_reload(plugins); - Command::none() + Task::none() } ManagementAppPluginMsgIn::RemovePlugin { plugin_id } => { self.selected_item = SelectedItem::None; let mut backend_client = backend_api.clone(); - Command::perform( + Task::perform( async move { backend_client.remove_plugin(plugin_id) .await?; @@ -208,18 +207,15 @@ impl ManagementAppPluginsState { ) } ManagementAppPluginMsgIn::DownloadPlugin { plugin_id } => { - Command::perform( - async { }, - |_| ManagementAppPluginMsgOut::DownloadPlugin { plugin_id } - ) + Task::done(ManagementAppPluginMsgOut::DownloadPlugin { plugin_id }) } ManagementAppPluginMsgIn::SelectItem(selected_item) => { self.selected_item = selected_item; - Command::none() + Task::none() } ManagementAppPluginMsgIn::Noop => { - Command::none() + Task::none() } } } @@ -247,7 +243,7 @@ impl ManagementAppPluginsState { let mut plugin_data = self.plugin_data.borrow_mut(); plugin_data.plugins_state = plugins.iter() - .map(|(id, plugin)| { + .map(|(id, _plugin)| { let show_entrypoints = plugin_data.plugins_state .get(&id) .map(|data| data.show_entrypoints) @@ -284,11 +280,11 @@ impl ManagementAppPluginsState { let text3: Element<_> = text("Click '+' to add new plugin").into(); let text_column = column(vec![text1, text2, text3]) - .align_items(Alignment::Center); + .align_x(Alignment::Center); container(text_column) - .center_y() - .center_x() + .align_y(Alignment::Center) + .align_x(Alignment::Center) .height(Length::Fill) .width(Length::Fill) .into() @@ -303,27 +299,27 @@ impl ManagementAppPluginsState { let loading_text: Element<_> = text("Loading...").into(); container(loading_text) - .center_y() - .center_x() + .align_y(Alignment::Center) + .align_x(Alignment::Center) .height(Length::Fill) .width(Length::Fill) .into() } Some(plugin) => { - let name = text(&plugin.plugin_name) + let name = text(plugin.plugin_name.to_string()) .shaping(Shaping::Advanced); let name = container(name) .padding(Padding::new(8.0)) .into(); - let id: Element<_> = text(&plugin.plugin_id.to_string()) + let id: Element<_> = text(plugin.plugin_id.to_string()) .shaping(Shaping::Advanced) - .style(TextStyle::Subtitle) + .class(TextStyle::Subtitle) .into(); let id = container(id) - .padding(Padding::from([0.0, 8.0, 8.0, 8.0])) + .padding(padding::bottom(8.0)) .into(); let mut column_content = vec![ @@ -334,14 +330,14 @@ impl ManagementAppPluginsState { if !plugin.plugin_description.is_empty() { let description_label: Element<_> = text("Description") .size(14) - .style(TextStyle::Subtitle) + .class(TextStyle::Subtitle) .into(); let description_label = container(description_label) - .padding(Padding::from([0.0, 0.0, 0.0, 8.0])) + .padding(padding::bottom(8.0)) .into(); - let description = text(&plugin.plugin_description) + let description = text(plugin.plugin_description.to_string()) .shaping(Shaping::Advanced); let description = container(description) @@ -376,13 +372,13 @@ impl ManagementAppPluginsState { let check_for_updates_text_container: Element<_> = container(check_for_updates_text) .width(Length::Fill) - .center_y() - .center_x() + .align_y(Alignment::Center) + .align_x(Alignment::Center) .into(); let check_for_updates_button: Element<_> = button(check_for_updates_text_container) .width(Length::Fill) - .style(ButtonStyle::Primary) + .class(ButtonStyle::Primary) .on_press(ManagementAppPluginMsgIn::DownloadPlugin { plugin_id: plugin.plugin_id.clone() }) .into(); @@ -393,13 +389,13 @@ impl ManagementAppPluginsState { let remove_button_text_container: Element<_> = container(remove_text) .width(Length::Fill) - .center_y() - .center_x() + .align_y(Alignment::Center) + .align_x(Alignment::Center) .into(); let remove_button: Element<_> = button(remove_button_text_container) .width(Length::Fill) - .style(ButtonStyle::Destructive) + .class(ButtonStyle::Destructive) .on_press(ManagementAppPluginMsgIn::RemovePlugin { plugin_id: plugin.plugin_id.clone() }) .into(); @@ -430,14 +426,14 @@ impl ManagementAppPluginsState { let loading_text: Element<_> = text("Loading...").into(); container(loading_text) - .center_y() - .center_x() + .align_y(Alignment::Center) + .align_x(Alignment::Center) .height(Length::Fill) .width(Length::Fill) .into() } Some(entrypoint) => { - let name = text(&entrypoint.entrypoint_name) + let name = text(entrypoint.entrypoint_name.to_string()) .shaping(Shaping::Advanced); let name = container(name) @@ -451,14 +447,14 @@ impl ManagementAppPluginsState { if !entrypoint.entrypoint_description.is_empty() { let description_label: Element<_> = text("Description") .size(14) - .style(TextStyle::Subtitle) + .class(TextStyle::Subtitle) .into(); let description_label = container(description_label) - .padding(Padding::from([0.0, 0.0, 0.0, 8.0])) + .padding(padding::bottom(8.0)) .into(); - let description = container(text(&entrypoint.entrypoint_description)) + let description = container(text(entrypoint.entrypoint_description.to_string())) .padding(Padding::new(8.0)) .into(); @@ -505,7 +501,7 @@ impl ManagementAppPluginsState { .padding(Padding::new(8.0)) .width(Length::Fill) .height(Length::Fill) - .center_x() + .align_x(Alignment::Center) .into() } }; @@ -524,14 +520,14 @@ impl ManagementAppPluginsState { let top_button_text = if plugin_url.is_some() { text("Download plugin") } else { - text(icons::Bootstrap::Plus) - .font(icons::BOOTSTRAP_FONT) + value(Bootstrap::Plus) + .font(BOOTSTRAP_FONT) }; let top_button_text_container: Element<_> = container(top_button_text) .width(Length::Fill) - .center_y() - .center_x() + .align_y(Alignment::Center) + .align_x(Alignment::Center) .into(); let top_button_action = match plugin_url { diff --git a/rust/management_client/src/views/plugins/preferences.rs b/rust/management_client/src/views/plugins/preferences.rs index f2208c9..b604f25 100644 --- a/rust/management_client/src/views/plugins/preferences.rs +++ b/rust/management_client/src/views/plugins/preferences.rs @@ -5,12 +5,12 @@ use crate::theme::Element; use crate::views::plugins::PluginPreferenceUserDataState; use common::model::{EntrypointId, PluginId, PluginPreference}; use iced::widget::{button, checkbox, column, container, pick_list, row, text, text_input}; -use iced::{Length, Padding}; -use iced_aw::core::icons; +use iced::{padding, widget, Length, Padding}; use iced_aw::number_input; use std::collections::HashMap; use std::fmt::Display; use iced::widget::text::Shaping; +use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; #[derive(Debug, Clone)] pub enum PluginPreferencesMsg { @@ -68,14 +68,14 @@ pub fn preferences_ui<'a>( let preference_name = preference_name.to_owned(); let description = description.to_owned(); - let preference_label: Element<_> = text(&preference_name) + let preference_label: Element<_> = text(preference_name.clone()) .shaping(Shaping::Advanced) .size(14) - .style(TextStyle::Subtitle) + .class(TextStyle::Subtitle) .into(); let preference_label = container(preference_label) - .padding(Padding::from([0.0, 0.0, 0.0, 8.0])) + .padding(padding::left(8.0)) .into(); let mut input_field_column = vec![]; @@ -105,8 +105,7 @@ pub fn preferences_ui<'a>( let value = value.or(default.to_owned()).unwrap_or_default(); - let input_field: Element<_> = number_input(value, f64::MAX, std::convert::identity) - .bounds((f64::MIN, f64::MAX)) + let input_field: Element<_> = number_input(value, f64::MIN..f64::MAX, std::convert::identity) .width(Length::Fill) .into(); @@ -124,7 +123,7 @@ pub fn preferences_ui<'a>( let input_field = container(input_field) .width(Length::Fill) .padding(Padding::from([4.0, 8.0])) - .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) .into(); input_field @@ -155,7 +154,7 @@ pub fn preferences_ui<'a>( let input_field = container(input_field) .padding(Padding::new(8.0)) - .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) .into(); input_field @@ -196,7 +195,7 @@ pub fn preferences_ui<'a>( let input_field = container(input_field) .padding(Padding::new(8.0)) .width(Length::Fill) - .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) .into(); input_field @@ -225,7 +224,7 @@ pub fn preferences_ui<'a>( let input_field = container(input_field) .padding(Padding::new(8.0)) - .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) .into(); input_field @@ -255,11 +254,11 @@ pub fn preferences_ui<'a>( .padding(Padding::new(4.0)) .into(); - let remove_icon = text(icons::Bootstrap::Dash) - .font(icons::BOOTSTRAP_FONT); + let remove_icon = widget::value(Bootstrap::Dash) + .font(BOOTSTRAP_FONT); let remove_button: Element<_> = button(remove_icon) - .style(ButtonStyle::Primary) + .class(ButtonStyle::Primary) .on_press(PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), @@ -273,7 +272,7 @@ pub fn preferences_ui<'a>( .into(); let remove_button = container(remove_button) - .padding(Padding::from([0.0, 0.0, 0.0, 8.0])) + .padding(padding::bottom(8.0)) .into(); let item: Element<_> = row([item_text, remove_button]) @@ -311,18 +310,18 @@ pub fn preferences_ui<'a>( }) }; - let add_icon: Element<_> = text(icons::Bootstrap::Plus) - .font(icons::BOOTSTRAP_FONT) + let add_icon: Element<_> = widget::value(Bootstrap::Plus) + .font(BOOTSTRAP_FONT) .into(); let add_button: Element<_> = button(add_icon) - .style(ButtonStyle::Primary) + .class(ButtonStyle::Primary) .on_press_maybe(add_msg) .padding(Padding::from([5.0, 7.0])) .into(); let add_button: Element<_> = container(add_button) - .padding(Padding::from([0.0, 0.0, 0.0, 8.0])) + .padding(padding::bottom(8.0)) .into(); let add_text_input: Element<_> = text_input("Enter value...", &new_value) @@ -351,7 +350,7 @@ pub fn preferences_ui<'a>( let content: Element<_> = container(content) .padding(0) - .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) .into(); content @@ -381,11 +380,11 @@ pub fn preferences_ui<'a>( .padding(Padding::new(4.0)) .into(); - let remove_icon = text(icons::Bootstrap::Dash) - .font(icons::BOOTSTRAP_FONT); + let remove_icon = widget::value(Bootstrap::Dash) + .font(BOOTSTRAP_FONT); let remove_button: Element<_> = button(remove_icon) - .style(ButtonStyle::Primary) + .class(ButtonStyle::Primary) .on_press(PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), @@ -399,7 +398,7 @@ pub fn preferences_ui<'a>( .into(); let remove_button = container(remove_button) - .padding(Padding::from([0.0, 0.0, 0.0, 8.0])) + .padding(padding::bottom(8.0)) .into(); let item: Element<_> = row([item_text, remove_button]) @@ -423,12 +422,12 @@ pub fn preferences_ui<'a>( } }; - let add_icon: Element<_> = text(icons::Bootstrap::Plus) - .font(icons::BOOTSTRAP_FONT) + let add_icon: Element<_> = widget::value(Bootstrap::Plus) + .font(BOOTSTRAP_FONT) .into(); let add_button: Element<_> = button(add_icon) - .style(ButtonStyle::Primary) + .class(ButtonStyle::Primary) .on_press(PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), @@ -442,11 +441,10 @@ pub fn preferences_ui<'a>( .into(); let add_button: Element<_> = container(add_button) - .padding(Padding::from([0.0, 0.0, 0.0, 8.0])) + .padding(padding::bottom(8.0)) .into(); - let add_number_input: Element<_> = number_input(new_value, f64::MAX, std::convert::identity) - .bounds((f64::MIN, f64::MAX)) + let add_number_input: Element<_> = number_input(new_value, f64::MIN..f64::MAX, std::convert::identity) .width(Length::Fill) .into(); @@ -476,7 +474,7 @@ pub fn preferences_ui<'a>( let content: Element<_> = container(content) .padding(0) - .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) .into(); content @@ -506,11 +504,11 @@ pub fn preferences_ui<'a>( .padding(Padding::new(4.0)) .into(); - let remove_icon = text(icons::Bootstrap::Dash) - .font(icons::BOOTSTRAP_FONT); + let remove_icon = widget::value(Bootstrap::Dash) + .font(BOOTSTRAP_FONT); let remove_button: Element<_> = button(remove_icon) - .style(ButtonStyle::Primary) + .class(ButtonStyle::Primary) .on_press(PluginPreferencesMsg::UpdatePreferenceValue { plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), @@ -524,7 +522,7 @@ pub fn preferences_ui<'a>( .into(); let remove_button = container(remove_button) - .padding(Padding::from([0.0, 0.0, 0.0, 8.0])) + .padding(padding::bottom(8.0)) .into(); let item: Element<_> = row([item_text, remove_button]) @@ -564,18 +562,18 @@ pub fn preferences_ui<'a>( }; - let add_icon: Element<_> = text(icons::Bootstrap::Plus) - .font(icons::BOOTSTRAP_FONT) + let add_icon: Element<_> = widget::value(Bootstrap::Plus) + .font(BOOTSTRAP_FONT) .into(); let add_button: Element<_> = button(add_icon) - .style(ButtonStyle::Primary) + .class(ButtonStyle::Primary) .on_press_maybe(add_msg) .padding(Padding::from([5.0, 7.0])) .into(); let add_button: Element<_> = container(add_button) - .padding(Padding::from([0.0, 0.0, 0.0, 8.0])) + .padding(padding::bottom(8.0)) .into(); let enum_values: Vec<_> = enum_values.iter() @@ -613,7 +611,7 @@ pub fn preferences_ui<'a>( let content: Element<_> = container(content) .padding(0) - .style(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) .into(); content diff --git a/rust/management_client/src/views/plugins/table.rs b/rust/management_client/src/views/plugins/table.rs index 5e14649..c58971d 100644 --- a/rust/management_client/src/views/plugins/table.rs +++ b/rust/management_client/src/views/plugins/table.rs @@ -1,10 +1,10 @@ use std::cell::RefCell; use std::rc::Rc; -use iced::{Command, Length, Renderer}; +use iced::{Alignment, Length, Renderer, Task}; use iced::advanced::text::Shaping; -use iced::widget::{button, checkbox, container, horizontal_space, row, scrollable, Space, text}; -use iced_aw::core::icons; +use iced::widget::{button, checkbox, container, horizontal_space, row, scrollable, Space, text, value}; +use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; use iced_table::table; use common::model::{EntrypointId, PluginId, SettingsEntrypointType, SettingsPlugin}; @@ -47,7 +47,7 @@ pub struct PluginTableState { } pub enum PluginTableUpdateResult { - Command(Command<()>), + Command(Task<()>), Value(PluginTableMsgOut) } @@ -202,19 +202,19 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo ColumnKind::Name => { container(text("Name")) .height(Length::Fixed(30.0)) - .center_y() + .align_y(Alignment::Center) .into() } ColumnKind::Type => { container(text("Type")) .height(Length::Fixed(30.0)) - .center_y() + .align_y(Alignment::Center) .into() } ColumnKind::EnableToggle => { container(text("Enabled")) .height(Length::Fixed(30.0)) - .center_y() + .align_y(Alignment::Center) .into() } } @@ -233,10 +233,10 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo let plugin_data = plugin_data.borrow(); let plugin_data = plugin_data.plugins_state.get(&plugin_id).unwrap(); - let icon = if plugin_data.show_entrypoints { icons::Bootstrap::CaretDown } else { icons::Bootstrap::CaretRight }; + let icon = if plugin_data.show_entrypoints { Bootstrap::CaretDown } else { Bootstrap::CaretRight }; - let icon: Element<_> = text(icon) - .font(icons::BOOTSTRAP_FONT) + let icon: Element<_> = value(icon) + .font(BOOTSTRAP_FONT) .into(); button(icon) @@ -244,7 +244,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo .width(Length::Fill) .height(Length::Fixed(40.0)) .padding(8.0) - .style(ButtonStyle::TableRow) + .class(ButtonStyle::TableRow) .into() } Row::Entrypoint { .. } => { @@ -259,11 +259,11 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); - let plugin_name = text(&plugin.plugin_name) + let plugin_name = text(plugin.plugin_name.to_string()) .shaping(Shaping::Advanced); container(plugin_name) - .center_y() + .align_y(Alignment::Center) .into() } Row::Entrypoint { plugin_data, plugin_id, entrypoint_id } => { @@ -271,7 +271,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); let entrypoint = plugin.entrypoints.get(&entrypoint_id).unwrap(); - let text: Element<_> = text(&entrypoint.entrypoint_name) + let text: Element<_> = text(entrypoint.entrypoint_name.to_string()) .shaping(Shaping::Advanced) .into(); @@ -281,7 +281,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo ]).into(); container(text) - .center_y() + .align_y(Alignment::Center) .into() } }; @@ -308,7 +308,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo }; button(content) - .style(ButtonStyle::TableRow) + .class(ButtonStyle::TableRow) .on_press(PluginTableMsgIn::SelectItem(msg)) .width(Length::Fill) .height(Length::Fixed(40.0)) @@ -333,8 +333,8 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo SettingsEntrypointType::CommandGenerator => "Command Generator" }; - container(text(entrypoint_type)) - .center_y() + container(text(entrypoint_type.to_string())) + .align_y(Alignment::Center) .into() } }; @@ -361,7 +361,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo }; button(content) - .style(ButtonStyle::TableRow) + .class(ButtonStyle::TableRow) .on_press(PluginTableMsgIn::SelectItem(msg)) .width(Length::Fill) .height(Length::Fixed(40.0)) @@ -421,8 +421,8 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo container(checkbox) .width(Length::Fill) .height(Length::Fixed(40.0)) - .center_y() - .center_x() + .align_y(Alignment::Center) + .align_x(Alignment::Center) .into() } } diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 95d44f3..cee7216 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -44,6 +44,9 @@ scenario_runner = { path = "../scenario_runner", optional = true } itertools = "0.10.5" vergen-pretty = "0.3.5" +# TODO REMOVE when updating dependencies +async-channel = "=2.3.1" # because concurrent-queue version is incorrect in automatically pulled version + [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] libc = "0.2.153" From ca0264fe1e96d77cf9cdd0dda05e81a952940bc8 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 6 Dec 2024 21:11:26 +0100 Subject: [PATCH 197/540] Bring back layer shell support using PopOS iced fork --- Cargo.lock | 1034 +++++++++++------ Cargo.toml | 4 +- rust/client/Cargo.toml | 3 +- rust/client/src/ui/hud/mod.rs | 114 +- rust/client/src/ui/mod.rs | 286 ++--- rust/client/src/ui/scroll_handle.rs | 6 +- rust/client/src/ui/theme/container.rs | 7 + rust/client/src/ui/theme/image.rs | 2 +- rust/client/src/ui/theme/mod.rs | 3 +- .../src/components/shortcut_selector.rs | 6 +- rust/management_client/src/theme.rs | 1 + .../src/views/plugins/table.rs | 9 +- 12 files changed, 903 insertions(+), 572 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7cf011..5d4cfe1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -28,6 +28,86 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" +[[package]] +name = "accesskit" +version = "0.16.0" +source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" + +[[package]] +name = "accesskit_atspi_common" +version = "0.9.0" +source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "serde", + "thiserror", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.24.0" +source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" +dependencies = [ + "accesskit", + "immutable-chunkmap", +] + +[[package]] +name = "accesskit_macos" +version = "0.17.0" +source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" +dependencies = [ + "accesskit", + "accesskit_consumer", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", +] + +[[package]] +name = "accesskit_unix" +version = "0.12.0" +source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "atspi", + "futures-lite 1.13.0", + "serde", + "tokio", + "tokio-stream", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.22.0" +source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" +dependencies = [ + "accesskit", + "accesskit_consumer", + "paste", + "static_assertions", + "windows 0.54.0", +] + +[[package]] +name = "accesskit_winit" +version = "0.22.0" +source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -93,17 +173,6 @@ dependencies = [ "aes", ] -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.14", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.11" @@ -391,14 +460,12 @@ dependencies = [ [[package]] name = "async-broadcast" -version = "0.7.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" dependencies = [ - "event-listener 5.3.1", - "event-listener-strategy", + "event-listener 2.5.3", "futures-core", - "pin-project-lite", ] [[package]] @@ -428,27 +495,23 @@ dependencies = [ ] [[package]] -name = "async-executor" -version = "1.11.0" +name = "async-io" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ - "async-task", + "async-lock 2.8.0", + "autocfg", + "cfg-if", "concurrent-queue", - "fastrand", - "futures-lite", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", "slab", -] - -[[package]] -name = "async-fs" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", + "socket2 0.4.10", + "waker-fn", ] [[package]] @@ -457,19 +520,28 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ - "async-lock", + "async-lock 3.4.0", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite", + "futures-lite 2.5.0", "parking", - "polling", - "rustix", + "polling 3.6.0", + "rustix 0.38.32", "slab", "tracing", "windows-sys 0.59.0", ] +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + [[package]] name = "async-lock" version = "3.4.0" @@ -483,21 +555,19 @@ dependencies = [ [[package]] name = "async-process" -version = "2.3.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" dependencies = [ - "async-channel", - "async-io", - "async-lock", + "async-io 1.13.0", + "async-lock 2.8.0", "async-signal", - "async-task", "blocking", "cfg-if", - "event-listener 5.3.1", - "futures-lite", - "rustix", - "tracing", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.32", + "windows-sys 0.48.0", ] [[package]] @@ -517,13 +587,13 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ - "async-io", - "async-lock", + "async-io 2.4.0", + "async-lock 3.4.0", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 0.38.32", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -606,6 +676,54 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atspi" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6059f350ab6f593ea00727b334265c4dfc7fd442ee32d264794bd9bdc68e87ca" +dependencies = [ + "atspi-common", + "atspi-connection", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92af95f966d2431f962bc632c2e68eda7777330158bf640c4af4249349b2cdf5" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-connection" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c65e7d70f86d4c0e3b2d585d9bf3f979f0b19d635a336725a88d279f76b939" +dependencies = [ + "atspi-common", + "atspi-proxies", + "futures-lite 1.13.0", + "zbus", +] + +[[package]] +name = "atspi-proxies" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6495661273703e7a229356dcbe8c8f38223d697aacfaf0e13590a9ac9977bb52" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + [[package]] name = "attohttpc" version = "0.27.0" @@ -676,7 +794,7 @@ dependencies = [ "itoa", "matchit", "memchr", - "mime", + "mime 0.3.17", "percent-encoding", "pin-project-lite", "rustversion", @@ -698,7 +816,7 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "mime", + "mime 0.3.17", "rustversion", "tower-layer", "tower-service", @@ -876,7 +994,7 @@ dependencies = [ "async-channel", "async-task", "futures-io", - "futures-lite", + "futures-lite 2.5.0", "piper", ] @@ -1033,28 +1151,28 @@ dependencies = [ [[package]] name = "calloop" -version = "0.12.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ "bitflags 2.6.0", "log", - "polling", - "rustix", + "polling 3.6.0", + "rustix 0.38.32", "slab", "thiserror", ] [[package]] name = "calloop-wayland-source" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", - "rustix", + "rustix 0.38.32", "wayland-backend", - "wayland-client 0.31.2", + "wayland-client 0.31.7", ] [[package]] @@ -1279,8 +1397,7 @@ dependencies = [ [[package]] name = "clipboard_macos" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145a7f9e9b89453bc0a5e32d166456405d389cea5b578f57f1274b1397588a95" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13#a83bf83784276aaa882ef13555295a2ad9edd265" dependencies = [ "objc", "objc-foundation", @@ -1290,20 +1407,20 @@ dependencies = [ [[package]] name = "clipboard_wayland" version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13#a83bf83784276aaa882ef13555295a2ad9edd265" dependencies = [ + "dnd", + "mime 0.1.0", "smithay-clipboard", ] [[package]] name = "clipboard_x11" version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13#a83bf83784276aaa882ef13555295a2ad9edd265" dependencies = [ "thiserror", - "x11rb 0.13.0", + "x11rb 0.13.1", ] [[package]] @@ -1315,6 +1432,22 @@ dependencies = [ "cc", ] +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", + "objc", +] + [[package]] name = "cocoa" version = "0.26.0" @@ -1323,7 +1456,7 @@ checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ "bitflags 2.6.0", "block", - "cocoa-foundation", + "cocoa-foundation 0.2.0", "core-foundation 0.10.0", "core-graphics 0.24.0", "foreign-types 0.5.0", @@ -1331,6 +1464,20 @@ dependencies = [ "objc", ] +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + [[package]] name = "cocoa-foundation" version = "0.2.0" @@ -1568,11 +1715,36 @@ dependencies = [ "libc", ] +[[package]] +name = "cosmic-client-toolkit" +version = "0.1.0" +source = "git+https://github.com/pop-os/cosmic-protocols?rev=d218c76#d218c76b58c7a3b20dd5e7943f93fc306a1b81b8" +dependencies = [ + "cosmic-protocols", + "libc", + "smithay-client-toolkit", + "wayland-client 0.31.7", + "wayland-protocols 0.32.5", +] + +[[package]] +name = "cosmic-protocols" +version = "0.1.0" +source = "git+https://github.com/pop-os/cosmic-protocols?rev=d218c76#d218c76b58c7a3b20dd5e7943f93fc306a1b81b8" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client 0.31.7", + "wayland-protocols 0.32.5", + "wayland-protocols-wlr", + "wayland-scanner 0.31.5", + "wayland-server", +] + [[package]] name = "cosmic-text" version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" +source = "git+https://github.com/pop-os/cosmic-text.git#1f4065c1c3399efad58841082212f7c039b58480" dependencies = [ "bitflags 2.6.0", "fontdb", @@ -1582,6 +1754,7 @@ dependencies = [ "rustc-hash 1.1.0", "rustybuzz", "self_cell", + "smol_str", "swash", "sys-locale", "ttf-parser 0.21.1", @@ -1709,14 +1882,10 @@ dependencies = [ ] [[package]] -name = "ctor" -version = "0.2.7" +name = "ctor-lite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c" -dependencies = [ - "quote 1.0.36", - "syn 2.0.89", -] +checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" [[package]] name = "ctr" @@ -1773,22 +1942,6 @@ dependencies = [ "syn 2.0.89", ] -[[package]] -name = "dark-light" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a76fa97167fa740dcdbfe18e8895601e1bc36525f09b044e00916e717c03a3c" -dependencies = [ - "dconf_rs", - "detect-desktop-environment", - "dirs 4.0.0", - "objc", - "rust-ini 0.18.0", - "web-sys", - "winreg 0.10.1", - "zbus", -] - [[package]] name = "darling" version = "0.20.10" @@ -1849,12 +2002,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41b319d1b62ffbd002e057f36bebd1f42b9f97927c9577461d855f3513c4289f" -[[package]] -name = "dconf_rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" - [[package]] name = "debugid" version = "0.8.0" @@ -2110,7 +2257,7 @@ dependencies = [ "hyper 0.14.28", "hyper 1.0.0-rc.4", "memmem", - "mime", + "mime 0.3.17", "once_cell", "percent-encoding", "phf 0.10.1", @@ -2211,7 +2358,7 @@ dependencies = [ "log", "pin-project", "serde", - "socket2", + "socket2 0.5.6", "tokio", "trust-dns-proto", "trust-dns-resolver", @@ -2524,6 +2671,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.36", + "syn 1.0.109", +] + [[package]] name = "derive-new" version = "0.5.9" @@ -2579,12 +2737,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "detect-desktop-environment" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" - [[package]] name = "digest" version = "0.8.1" @@ -2679,7 +2831,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.8.3", + "libloading 0.7.4", ] [[package]] @@ -2705,12 +2857,6 @@ dependencies = [ "syn 0.15.44", ] -[[package]] -name = "dlv-list" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" - [[package]] name = "dlv-list" version = "0.5.2" @@ -2720,6 +2866,18 @@ dependencies = [ "const-random", ] +[[package]] +name = "dnd" +version = "0.1.0" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13#a83bf83784276aaa882ef13555295a2ad9edd265" +dependencies = [ + "bitflags 2.6.0", + "mime 0.1.0", + "raw-window-handle", + "smithay-client-toolkit", + "smithay-clipboard", +] + [[package]] name = "document-features" version = "0.2.10" @@ -2750,7 +2908,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "dpi" version = "0.1.1" -source = "git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d#254d6b3420ce4e674f516f7a2bd440665e05484d" +source = "git+https://github.com/pop-os/winit.git?tag=iced-xdg-surface-0.13#1cc02bdab141072eaabad639d74b032fd0fcc62e" [[package]] name = "dprint-swc-ext" @@ -2770,25 +2928,25 @@ dependencies = [ [[package]] name = "drm" -version = "0.12.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" +checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" dependencies = [ "bitflags 2.6.0", "bytemuck", "drm-ffi", "drm-fourcc", - "rustix", + "rustix 0.38.32", ] [[package]] name = "drm-ffi" -version = "0.8.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" +checksum = "41334f8405792483e32ad05fbb9c5680ff4e84491883d2947a4757dc54cb2ac6" dependencies = [ "drm-sys", - "rustix", + "rustix 0.38.32", ] [[package]] @@ -2799,9 +2957,9 @@ checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" [[package]] name = "drm-sys" -version = "0.7.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" +checksum = "2d09ff881f92f118b11105ba5e34ff8f4adf27b30dae8f12e28c193af1c83176" dependencies = [ "libc", "linux-raw-sys 0.6.4", @@ -2951,12 +3109,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "endi" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" - [[package]] name = "enum-as-inner" version = "0.5.1" @@ -3089,6 +3241,17 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "event-listener" version = "5.3.1" @@ -3159,6 +3322,15 @@ dependencies = [ "serde", ] +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.0.2" @@ -3387,7 +3559,7 @@ checksum = "a8ef34245e0540c9a3ce7a28340b98d2c12b75da0d446da4e8224923fcaa0c16" dependencies = [ "dirs 5.0.1", "once_cell", - "rust-ini 0.20.0", + "rust-ini", "thiserror", "xdg", ] @@ -3431,7 +3603,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" dependencies = [ - "rustix", + "rustix 0.38.32", "windows-sys 0.52.0", ] @@ -3514,16 +3686,28 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-lite" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ - "fastrand", "futures-core", - "futures-io", - "parking", "pin-project-lite", ] @@ -3944,18 +4128,6 @@ dependencies = [ "gl_generator", ] -[[package]] -name = "glyphon" -version = "0.5.0" -source = "git+https://github.com/hecrj/glyphon.git?rev=09712a70df7431e9a3b1ac1bbd4fb634096cb3b4#09712a70df7431e9a3b1ac1bbd4fb634096cb3b4" -dependencies = [ - "cosmic-text", - "etagere", - "lru", - "rustc-hash 2.0.0", - "wgpu", -] - [[package]] name = "gobject-sys" version = "0.18.0" @@ -4136,9 +4308,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] [[package]] name = "hashbrown" @@ -4146,7 +4315,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.8.11", + "ahash", "allocator-api2", ] @@ -4323,7 +4492,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -4403,21 +4572,34 @@ dependencies = [ [[package]] name = "iced" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" dependencies = [ + "dnd", + "iced_accessibility", "iced_core", "iced_futures", "iced_renderer", "iced_widget", "iced_winit", "image 0.24.9", + "mime 0.1.0", "thiserror", + "window_clipboard", +] + +[[package]] +name = "iced_accessibility" +version = "0.1.0" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" +dependencies = [ + "accesskit", + "accesskit_winit", ] [[package]] name = "iced_aw" version = "0.11.99" -source = "git+https://github.com/project-gauntlet/iced_aw.git?branch=gauntlet-0.13#789d481640fbaa83d8e5e00c67440271cabae46b" +source = "git+https://github.com/project-gauntlet/iced_aw.git?branch=gauntlet-0.13#817cc1c8c4aadd22e300d0edb2adaed2d434d962" dependencies = [ "cfg-if", "chrono", @@ -4431,20 +4613,24 @@ dependencies = [ [[package]] name = "iced_core" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" dependencies = [ "bitflags 2.6.0", "bytes", - "dark-light", + "cosmic-client-toolkit", + "dnd", "glam", "log", + "mime 0.1.0", "num-traits", "once_cell", "palette", + "raw-window-handle", "rustc-hash 2.0.0", "smol_str", "thiserror", "web-time", + "window_clipboard", ] [[package]] @@ -4458,7 +4644,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" dependencies = [ "futures", "iced_core", @@ -4469,10 +4655,22 @@ dependencies = [ "wasm-timer", ] +[[package]] +name = "iced_glyphon" +version = "0.6.0" +source = "git+https://github.com/pop-os/glyphon.git?branch=iced-0.13#52b11b2a30e69c9b97c88a737138000c7eb6952a" +dependencies = [ + "cosmic-text", + "etagere", + "lru", + "rustc-hash 2.0.0", + "wgpu", +] + [[package]] name = "iced_graphics" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -4493,7 +4691,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -4505,19 +4703,22 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" dependencies = [ "bytes", + "cosmic-client-toolkit", + "dnd", "iced_core", "iced_futures", "raw-window-handle", "thiserror", + "window_clipboard", ] [[package]] name = "iced_table" -version = "0.13.0" -source = "git+https://github.com/project-gauntlet/iced_table.git?branch=gauntlet-0.13#392e36d40e04180bc89c313d3c6272bed2362677" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/iced_table.git?branch=gauntlet-0.13#3e0b0257e3efa34776226aa30566e7b3df659783" dependencies = [ "iced_core", "iced_widget", @@ -4526,7 +4727,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" dependencies = [ "bytemuck", "cosmic-text", @@ -4541,27 +4742,39 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" dependencies = [ + "as-raw-xcb-connection", "bitflags 2.6.0", "bytemuck", + "cosmic-client-toolkit", "futures", "glam", - "glyphon", "guillotiere", + "iced_glyphon", "iced_graphics", "log", "once_cell", + "raw-window-handle", "rustc-hash 2.0.0", + "rustix 0.38.32", "thiserror", + "tiny-xlib", + "wayland-backend", + "wayland-client 0.31.7", + "wayland-protocols 0.32.5", + "wayland-sys 0.31.5", "wgpu", + "x11rb 0.13.1", ] [[package]] name = "iced_widget" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" dependencies = [ + "cosmic-client-toolkit", + "dnd", "iced_renderer", "iced_runtime", "num-traits", @@ -4570,25 +4783,34 @@ dependencies = [ "rustc-hash 2.0.0", "thiserror", "unicode-segmentation", + "window_clipboard", ] [[package]] name = "iced_winit" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#c16f7e06213f620678056c803a2496e795178904" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" dependencies = [ + "cosmic-client-toolkit", + "dnd", "iced_futures", "iced_graphics", "iced_runtime", "log", + "raw-window-handle", "rustc-hash 2.0.0", "thiserror", "tracing", "wasm-bindgen-futures", + "wayland-backend", + "wayland-protocols 0.32.5", "web-sys", "winapi", "window_clipboard", "winit", + "xkbcommon", + "xkbcommon-dl", + "xkeysym", ] [[package]] @@ -4707,6 +4929,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" +[[package]] +name = "immutable-chunkmap" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f97096f508d54f8f8ab8957862eee2ccd628847b6217af1a335e1c44dee578" +dependencies = [ + "arrayvec", +] + [[package]] name = "include_dir" version = "0.7.3" @@ -4801,13 +5032,30 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.6", "widestring", "windows-sys 0.48.0", "winreg 0.50.0", @@ -5283,6 +5531,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -5474,6 +5728,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memmap2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" +dependencies = [ + "libc", +] + [[package]] name = "memmap2" version = "0.9.4" @@ -5540,6 +5803,14 @@ dependencies = [ "paste", ] +[[package]] +name = "mime" +version = "0.1.0" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13#a83bf83784276aaa882ef13555295a2ad9edd265" +dependencies = [ + "smithay-clipboard", +] + [[package]] name = "mime" version = "0.3.17" @@ -5552,7 +5823,7 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ - "mime", + "mime 0.3.17", "unicase", ] @@ -5605,7 +5876,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" dependencies = [ - "cocoa", + "cocoa 0.26.0", "crossbeam-channel", "dpi 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "gtk", @@ -5743,19 +6014,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.6.0", - "cfg-if", - "cfg_aliases 0.2.1", - "libc", - "memoffset 0.9.1", -] - [[package]] name = "nom" version = "7.1.3" @@ -5986,7 +6244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd1e3c3e4f9f22d0d7cdcb413f01194f6506a302a9029d95deedcd1c25df7718" dependencies = [ "attohttpc", - "quick-xml", + "quick-xml 0.31.0", ] [[package]] @@ -6251,7 +6509,7 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed29bb6f7d6ac14023acb332a356f3891265d780e254057c866dbe7a909d2d2d" dependencies = [ - "ahash 0.8.11", + "ahash", "hashbrown 0.15.0", "parking_lot 0.12.1", "stable_deref_trait", @@ -6335,23 +6593,13 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ordered-multimap" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" -dependencies = [ - "dlv-list 0.3.0", - "hashbrown 0.12.3", -] - [[package]] name = "ordered-multimap" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ - "dlv-list 0.5.2", + "dlv-list", "hashbrown 0.14.3", ] @@ -6805,7 +7053,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand", + "fastrand 2.0.2", "futures-io", ] @@ -6873,7 +7121,7 @@ dependencies = [ "base64 0.21.7", "indexmap 2.2.6", "line-wrap", - "quick-xml", + "quick-xml 0.31.0", "serde", "time 0.3.36", ] @@ -6944,6 +7192,22 @@ dependencies = [ "miniz_oxide 0.7.2", ] +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + [[package]] name = "polling" version = "3.6.0" @@ -6954,7 +7218,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix", + "rustix 0.38.32", "tracing", "windows-sys 0.52.0", ] @@ -7287,6 +7551,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "0.6.13" @@ -7470,15 +7743,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_syscall" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" -dependencies = [ - "bitflags 2.6.0", -] - [[package]] name = "redox_users" version = "0.4.5" @@ -7560,7 +7824,7 @@ dependencies = [ "ipnet", "js-sys", "log", - "mime", + "mime 0.3.17", "once_cell", "percent-encoding", "pin-project-lite", @@ -7692,7 +7956,7 @@ checksum = "1f1ae91455a4c82892d9513fcfa1ac8faff6c523602d0041536341882714aede" dependencies = [ "basic-toml", "memchr", - "mime", + "mime 0.3.17", "mime_guess", "once_map", "proc-macro2 1.0.92", @@ -7819,16 +8083,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "rust-ini" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" -dependencies = [ - "cfg-if", - "ordered-multimap 0.4.3", -] - [[package]] name = "rust-ini" version = "0.20.0" @@ -7836,7 +8090,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" dependencies = [ "cfg-if", - "ordered-multimap 0.7.3", + "ordered-multimap", ] [[package]] @@ -7894,6 +8148,20 @@ dependencies = [ "nom", ] +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno 0.3.8", + "io-lifetimes 1.0.11", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + [[package]] name = "rustix" version = "0.38.32" @@ -8107,9 +8375,9 @@ dependencies = [ [[package]] name = "sctk-adwaita" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7555fcb4f753d095d734fdefebb0ad8c98478a21db500492d87c55913d3b0086" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" dependencies = [ "ab_glyph", "log", @@ -8581,49 +8849,62 @@ dependencies = [ [[package]] name = "smithay-client-toolkit" -version = "0.18.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ "bitflags 2.6.0", + "bytemuck", "calloop", "calloop-wayland-source", "cursor-icon", "libc", "log", "memmap2 0.9.4", - "rustix", + "pkg-config", + "rustix 0.38.32", "thiserror", "wayland-backend", - "wayland-client 0.31.2", + "wayland-client 0.31.7", "wayland-csd-frame", "wayland-cursor", - "wayland-protocols 0.31.2", + "wayland-protocols 0.32.5", "wayland-protocols-wlr", - "wayland-scanner 0.31.1", + "wayland-scanner 0.31.5", + "xkbcommon", "xkeysym", ] [[package]] name = "smithay-clipboard" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" +version = "0.8.0" +source = "git+https://github.com/pop-os/smithay-clipboard?tag=pop-dnd-5#5a3007def49eb678d1144850c9ee04b80707c56a" dependencies = [ "libc", + "raw-window-handle", "smithay-client-toolkit", "wayland-backend", ] [[package]] name = "smol_str" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.5.6" @@ -8636,35 +8917,32 @@ dependencies = [ [[package]] name = "softbuffer" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd" +version = "0.4.1" +source = "git+https://github.com/pop-os/softbuffer?tag=cosmic-4.0#6e75b1ad7e98397d37cb187886d05969bc480995" dependencies = [ "as-raw-xcb-connection", "bytemuck", "cfg_aliases 0.2.1", + "cocoa 0.25.0", "core-graphics 0.23.2", "drm", - "fastrand", + "fastrand 2.0.2", "foreign-types 0.5.0", "js-sys", "log", "memmap2 0.9.4", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-quartz-core", + "objc", "raw-window-handle", - "redox_syscall 0.5.3", - "rustix", + "redox_syscall 0.4.1", + "rustix 0.38.32", "tiny-xlib", "wasm-bindgen", "wayland-backend", - "wayland-client 0.31.2", - "wayland-sys 0.31.1", + "wayland-client 0.31.7", + "wayland-sys 0.31.5", "web-sys", "windows-sys 0.52.0", - "x11rb 0.13.0", + "x11rb 0.13.1", ] [[package]] @@ -8757,7 +9035,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" dependencies = [ - "ahash 0.8.11", + "ahash", "atoi", "byteorder", "bytes", @@ -9675,8 +9953,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand", - "rustix", + "fastrand 2.0.2", + "rustix 0.38.32", "windows-sys 0.52.0", ] @@ -9820,13 +10098,14 @@ dependencies = [ [[package]] name = "tiny-xlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4098d49269baa034a8d1eae9bd63e9fa532148d772121dace3bcd6a6c98eb6d" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" dependencies = [ "as-raw-xcb-connection", - "ctor", + "ctor-lite", "libloading 0.8.3", + "pkg-config", "tracing", ] @@ -9859,8 +10138,9 @@ dependencies = [ "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.6", "tokio-macros", + "tracing", "windows-sys 0.48.0", ] @@ -10681,6 +10961,12 @@ dependencies = [ "quote 1.0.36", ] +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "walkdir" version = "2.5.0" @@ -10821,16 +11107,16 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.3" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" dependencies = [ "cc", "downcast-rs", - "rustix", + "rustix 0.38.32", "scoped-tls", "smallvec", - "wayland-sys 0.31.1", + "wayland-sys 0.31.5", ] [[package]] @@ -10850,14 +11136,14 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.2" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" +checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" dependencies = [ "bitflags 2.6.0", - "rustix", + "rustix 0.38.32", "wayland-backend", - "wayland-scanner 0.31.1", + "wayland-scanner 0.31.5", ] [[package]] @@ -10889,8 +11175,8 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71ce5fa868dd13d11a0d04c5e2e65726d0897be8de247c0c5a65886e283231ba" dependencies = [ - "rustix", - "wayland-client 0.31.2", + "rustix 0.38.32", + "wayland-client 0.31.7", "xcursor", ] @@ -10908,40 +11194,42 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.31.2" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" dependencies = [ "bitflags 2.6.0", "wayland-backend", - "wayland-client 0.31.2", - "wayland-scanner 0.31.1", + "wayland-client 0.31.7", + "wayland-scanner 0.31.5", + "wayland-server", ] [[package]] name = "wayland-protocols-plasma" -version = "0.2.0" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" +checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" dependencies = [ "bitflags 2.6.0", "wayland-backend", - "wayland-client 0.31.2", - "wayland-protocols 0.31.2", - "wayland-scanner 0.31.1", + "wayland-client 0.31.7", + "wayland-protocols 0.32.5", + "wayland-scanner 0.31.5", ] [[package]] name = "wayland-protocols-wlr" -version = "0.2.0" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" dependencies = [ "bitflags 2.6.0", "wayland-backend", - "wayland-client 0.31.2", - "wayland-protocols 0.31.2", - "wayland-scanner 0.31.1", + "wayland-client 0.31.7", + "wayland-protocols 0.32.5", + "wayland-scanner 0.31.5", + "wayland-server", ] [[package]] @@ -10957,15 +11245,29 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.1" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ "proc-macro2 1.0.92", - "quick-xml", + "quick-xml 0.36.2", "quote 1.0.36", ] +[[package]] +name = "wayland-server" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89532cc712a2adb119eb4d09694b402576052254d0bb284f82ac1c47fb786ad" +dependencies = [ + "bitflags 2.6.0", + "downcast-rs", + "io-lifetimes 2.0.4", + "rustix 0.38.32", + "wayland-backend", + "wayland-scanner 0.31.5", +] + [[package]] name = "wayland-sys" version = "0.29.5" @@ -10977,9 +11279,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.1" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" dependencies = [ "dlib", "log", @@ -11143,7 +11445,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix", + "rustix 0.38.32", ] [[package]] @@ -11206,13 +11508,14 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "window_clipboard" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d692d46038c433f9daee7ad8757e002a4248c20b0a3fbc991d99521d3bcb6d" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13#a83bf83784276aaa882ef13555295a2ad9edd265" dependencies = [ "clipboard-win 5.3.1", "clipboard_macos", "clipboard_wayland", "clipboard_x11", + "dnd", + "mime 0.1.0", "raw-window-handle", "thiserror", ] @@ -11226,6 +11529,18 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-implement 0.53.0", + "windows-interface 0.53.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.58.0" @@ -11245,19 +11560,40 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", "windows-strings", "windows-targets 0.52.6", ] +[[package]] +name = "windows-implement" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.36", + "syn 2.0.89", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -11269,6 +11605,17 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "windows-interface" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.36", + "syn 2.0.89", +] + [[package]] name = "windows-interface" version = "0.58.0" @@ -11280,6 +11627,15 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -11295,7 +11651,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets 0.52.6", ] @@ -11515,10 +11871,10 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" -version = "0.30.1" -source = "git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d#254d6b3420ce4e674f516f7a2bd440665e05484d" +version = "0.30.5" +source = "git+https://github.com/pop-os/winit.git?tag=iced-xdg-surface-0.13#1cc02bdab141072eaabad639d74b032fd0fcc62e" dependencies = [ - "ahash 0.8.11", + "ahash", "android-activity", "atomic-waker", "bitflags 2.6.0", @@ -11530,7 +11886,7 @@ dependencies = [ "core-foundation 0.9.4", "core-graphics 0.23.2", "cursor-icon", - "dpi 0.1.1 (git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d)", + "dpi 0.1.1 (git+https://github.com/pop-os/winit.git?tag=iced-xdg-surface-0.13)", "js-sys", "libc", "memmap2 0.9.4", @@ -11544,7 +11900,7 @@ dependencies = [ "pin-project", "raw-window-handle", "redox_syscall 0.4.1", - "rustix", + "rustix 0.38.32", "sctk-adwaita", "smithay-client-toolkit", "smol_str", @@ -11553,14 +11909,14 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "wayland-backend", - "wayland-client 0.31.2", - "wayland-protocols 0.31.2", + "wayland-client 0.31.7", + "wayland-protocols 0.32.5", "wayland-protocols-plasma", "web-sys", "web-time", "windows-sys 0.52.0", "x11-dl", - "x11rb 0.13.0", + "x11rb 0.13.1", "xkbcommon-dl", ] @@ -11654,17 +12010,17 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "as-raw-xcb-connection", "gethostname 0.4.3", "libc", "libloading 0.8.3", "once_cell", - "rustix", - "x11rb-protocol 0.13.0", + "rustix 0.38.32", + "x11rb-protocol 0.13.1", ] [[package]] @@ -11678,9 +12034,9 @@ dependencies = [ [[package]] name = "x11rb-protocol" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "x25519-dalek" @@ -11733,6 +12089,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xkbcommon" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e" +dependencies = [ + "libc", + "memmap2 0.8.0", + "xkeysym", +] + [[package]] name = "xkbcommon-dl" version = "0.4.2" @@ -11751,6 +12118,9 @@ name = "xkeysym" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" +dependencies = [ + "bytemuck", +] [[package]] name = "xml-rs" @@ -11778,36 +12148,34 @@ checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" [[package]] name = "zbus" -version = "4.4.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" dependencies = [ "async-broadcast", - "async-executor", - "async-fs", - "async-io", - "async-lock", "async-process", "async-recursion", - "async-task", "async-trait", - "blocking", + "byteorder", + "derivative", "enumflags2", - "event-listener 5.3.1", + "event-listener 2.5.3", "futures-core", "futures-sink", "futures-util", "hex", - "nix 0.29.0", + "nix 0.26.2", + "once_cell", "ordered-stream", "rand", "serde", "serde_repr", "sha1", "static_assertions", + "tokio", "tracing", "uds_windows", - "windows-sys 0.52.0", + "winapi", "xdg-home", "zbus_macros", "zbus_names", @@ -11816,22 +12184,23 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.4.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" dependencies = [ - "proc-macro-crate 3.1.0", + "proc-macro-crate 1.3.1", "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.89", + "regex", + "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zbus_names" -version = "3.0.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" dependencies = [ "serde", "static_assertions", @@ -11938,12 +12307,13 @@ dependencies = [ [[package]] name = "zvariant" -version = "4.2.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" dependencies = [ - "endi", + "byteorder", "enumflags2", + "libc", "serde", "static_assertions", "zvariant_derive", @@ -11951,24 +12321,24 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "4.2.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" dependencies = [ - "proc-macro-crate 3.1.0", + "proc-macro-crate 1.3.1", "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.89", + "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "2.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" dependencies = [ "proc-macro2 1.0.92", "quote 1.0.36", - "syn 2.0.89", + "syn 1.0.109", ] diff --git a/Cargo.toml b/Cargo.toml index 4d9d4ee..4a03afb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,8 @@ members = [ "rust/scenario_runner", ] [workspace.dependencies] -#iced = { version = "0.13.99", features = ["tokio", "lazy", "advanced", "image"] } -iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", features = ["tokio", "lazy", "advanced", "image"] } +#iced = { version = "0.13.99", features = ["tokio", "lazy", "advanced", "image", "winit"] } +iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", features = ["tokio", "lazy", "advanced", "image", "winit"] } #iced_aw = { version = "0.11.99", features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } iced_aw = { git = "https://github.com/project-gauntlet/iced_aw.git", branch = "gauntlet-0.13", default-features = false, features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } #iced_table = "0.13.0" diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index 9bbb3aa..b22e04c 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -28,8 +28,7 @@ tray-icon = { version = "0.15.1", default-features = false } [target.'cfg(target_os = "linux")'.dependencies] iced.workspace = true -# TODO -#iced.features = ["wayland"] +iced.features = ["wayland"] [target.'cfg(not(target_os = "linux"))'.dependencies] iced.workspace = true diff --git a/rust/client/src/ui/hud/mod.rs b/rust/client/src/ui/hud/mod.rs index b1c4887..4266f2f 100644 --- a/rust/client/src/ui/hud/mod.rs +++ b/rust/client/src/ui/hud/mod.rs @@ -1,7 +1,6 @@ use crate::ui::AppMsg; -use iced::advanced::layout::Limits; use iced::window::{Level, Position, Settings}; -use iced::{window, Size, Task}; +use iced::{window, Limits, Size, Task}; use std::convert; use std::time::Duration; @@ -9,11 +8,21 @@ const HUD_WINDOW_WIDTH: f32 = 400.0; const HUD_WINDOW_HEIGHT: f32 = 40.0; pub fn show_hud_window( - // TODO - // #[cfg(target_os = "linux")] - // wayland: bool, + #[cfg(target_os = "linux")] + wayland: bool, ) -> Task { + #[cfg(target_os = "linux")] + if wayland { + open_wayland() + } else { + open_non_wayland() + } + #[cfg(not(target_os = "linux"))] + open_non_wayland() +} + +fn open_non_wayland() -> Task { let settings = Settings { size: Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT), position: Position::Centered, @@ -22,6 +31,7 @@ pub fn show_hud_window( transparent: true, visible: true, level: Level::AlwaysOnTop, + // TODO macos // #[cfg(target_os = "macos")] // platform_specific: iced::window::settings::PlatformSpecific { // activation_policy: window::settings::ActivationPolicy::Accessory, @@ -32,79 +42,51 @@ pub fn show_hud_window( ..Default::default() }; - // #[cfg(target_os = "linux")] - // if wayland { - // let id = window::Id::unique(); - // - // let show_command = iced::wayland::commands::layer_surface::get_layer_surface(layer_shell_settings(id)); - // let close_command = in_2_seconds(AppMsg::CloseHudWindow { id }); - // - // Task::batch([ - // show_command, - // close_command - // ]) - // } else { - // let (id, show_command) = window::spawn(settings); - // let close_command = in_2_seconds(AppMsg::CloseHudWindow { id }); - // - // Task::batch([ - // show_command, - // close_command - // ]) - // } - // - // #[cfg(not(target_os = "linux"))] - // { - // let (id, show_command) = window::spawn(settings); - // let close_command = in_2_seconds(AppMsg::CloseHudWindow { id }); - // - // Task::batch([ - // show_command, - // close_command - // ]) - // } - window::open(settings) .1 .then(|id| in_2_seconds(AppMsg::CloseHudWindow { id })) } +#[cfg(target_os = "linux")] +fn open_wayland() -> Task { + let id = window::Id::unique(); + + iced::platform_specific::shell::commands::layer_surface::get_layer_surface(layer_shell_settings(id)) + .then(|id| in_2_seconds(AppMsg::CloseHudWindow { id })) +} + pub fn close_hud_window( - // #[cfg(target_os = "linux")] - // wayland: bool, + #[cfg(target_os = "linux")] + wayland: bool, id: window::Id ) -> Task { - // TODO - // #[cfg(target_os = "linux")] - // if wayland { - // iced::wayland::commands::layer_surface::destroy_layer_surface(id) - // } else { - // window::close(id) - // } - // - // #[cfg(not(target_os = "linux"))] - // window::close(id) + #[cfg(target_os = "linux")] + if wayland { + iced::platform_specific::shell::commands::layer_surface::destroy_layer_surface(id) + } else { + window::close(id) + } + #[cfg(not(target_os = "linux"))] window::close(id) } -// TODO -// #[cfg(target_os = "linux")] -// fn layer_shell_settings(id: window::Id) -> iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { -// iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { -// id, -// layer: iced::wayland::commands::layer_surface::Layer::Overlay, -// keyboard_interactivity: iced::wayland::commands::layer_surface::KeyboardInteractivity::None, -// pointer_interactivity: false, -// anchor: iced::wayland::commands::layer_surface::Anchor::empty(), -// output: Default::default(), -// namespace: "Gauntlet HUD".to_string(), -// margin: Default::default(), -// exclusive_zone: 0, -// size: Some((Some(HUD_WINDOW_WIDTH as u32), Some(HUD_WINDOW_HEIGHT as u32))), -// size_limits: Limits::new(Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT), Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT)), -// } -// } +#[cfg(target_os = "linux")] +fn layer_shell_settings(id: window::Id) -> iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings { + iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings { + id, + layer: iced::platform_specific::shell::commands::layer_surface::Layer::Overlay, + keyboard_interactivity: iced::platform_specific::shell::commands::layer_surface::KeyboardInteractivity::None, + pointer_interactivity: false, + anchor: iced::platform_specific::shell::commands::layer_surface::Anchor::empty(), + output: Default::default(), + namespace: "Gauntlet HUD".to_string(), + margin: Default::default(), + exclusive_zone: 0, + size: Some((Some(HUD_WINDOW_WIDTH as u32), Some(HUD_WINDOW_HEIGHT as u32))), + size_limits: Limits::new(Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT), Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT)), + } +} fn in_2_seconds(msg: AppMsg) -> Task { Task::perform(async move { diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 57bc428..d01d78f 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -149,7 +149,6 @@ pub enum AppMsg { save_path: String, screenshot: Screenshot }, - Close, ShowBackendError(BackendForFrontendApiError), ClosePluginView(PluginId), OpenPluginView(PluginId, EntrypointId), @@ -195,57 +194,46 @@ pub enum AppMsg { const WINDOW_WIDTH: f32 = 750.0; const WINDOW_HEIGHT: f32 = 450.0; -// #[cfg(target_os = "linux")] -// fn layer_shell_settings() -> iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { -// iced::wayland::runtime::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings { -// id: window::Id::MAIN, -// layer: iced::wayland::commands::layer_surface::Layer::Overlay, -// keyboard_interactivity: iced::wayland::commands::layer_surface::KeyboardInteractivity::Exclusive, -// pointer_interactivity: true, -// anchor: iced::wayland::commands::layer_surface::Anchor::empty(), -// output: Default::default(), -// namespace: "Gauntlet".to_string(), -// margin: Default::default(), -// exclusive_zone: 0, -// size: Some((Some(WINDOW_WIDTH as u32), Some(WINDOW_HEIGHT as u32))), -// size_limits: Limits::new(Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), Size::new(WINDOW_WIDTH, WINDOW_HEIGHT)), -// } -// } +#[cfg(target_os = "linux")] +fn layer_shell_settings(main_window_id: window::Id) -> iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings { + iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings { + id: main_window_id, + layer: iced::platform_specific::shell::commands::layer_surface::Layer::Overlay, + keyboard_interactivity: iced::platform_specific::shell::commands::layer_surface::KeyboardInteractivity::Exclusive, + pointer_interactivity: true, + anchor: iced::platform_specific::shell::commands::layer_surface::Anchor::empty(), + output: Default::default(), + namespace: "Gauntlet".to_string(), + margin: Default::default(), + exclusive_zone: 0, + size: Some((Some(WINDOW_WIDTH as u32), Some(WINDOW_HEIGHT as u32))), + size_limits: Limits::new(Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), Size::new(WINDOW_WIDTH, WINDOW_HEIGHT)), + } +} pub fn run( minimized: bool, frontend_receiver: RequestReceiver, backend_sender: RequestSender, ) { - // #[cfg(target_os = "linux")] - // let wayland = std::env::var("WAYLAND_DISPLAY") - // .or_else(|_| std::env::var("WAYLAND_SOCKET")) - // .is_ok(); - // - // #[cfg(not(target_os = "linux"))] - // let wayland = false; + #[cfg(target_os = "linux")] + let wayland = std::env::var("WAYLAND_DISPLAY") + .or_else(|_| std::env::var("WAYLAND_SOCKET")) + .is_ok(); - let wayland = false; // TODO remove + #[cfg(not(target_os = "linux"))] + let wayland = false; let theme = GauntletComplexTheme::new(); - iced::daemon::("Gauntlet Settings", update, view) + iced::daemon::(title, update, view) .subscription(subscription) - .theme(move |_, _| theme.clone()) + .theme({ + let theme = theme.clone(); + move |_, _| theme.clone() + }) .run_with(move || new(frontend_receiver, backend_sender, wayland, minimized)) - .expect("Unable to start settings application"); - - // #[cfg(target_os = "linux")] - // let result = if wayland { - // AppModel::run_wayland(settings) - // } else { - // AppModel::run(settings) - // }; - // - // #[cfg(not(target_os = "linux"))] - // let result = AppModel::run(settings); - // - // result.expect("Unable to start application") + .expect("Unable to start application") } fn new( @@ -259,46 +247,51 @@ fn new( let global_hotkey_manager = GlobalHotKeyManager::new() .expect("unable to create global hot key manager"); - let mut commands = vec![ - font::load(BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded), - ]; + let (main_window_id, open_window) = if !wayland { + let settings = window::Settings { + size: Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), + position: Position::Centered, + resizable: false, + decorations: false, + transparent: true, + visible: !minimized, + // todo macoss + // #[cfg(target_os = "macos")] + // platform_specific: PlatformSpecific { + // activation_policy: window::settings::ActivationPolicy::Accessory, + // activate_ignoring_other_apps: false, + // ..Default::default() + // }, + ..Default::default() + }; - let settings = window::Settings { - size: Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), - position: Position::Centered, - resizable: false, - decorations: false, - transparent: true, - visible: !minimized, - // #[cfg(target_os = "macos")] - // platform_specific: PlatformSpecific { - // activation_policy: window::settings::ActivationPolicy::Accessory, - // activate_ignoring_other_apps: false, - // ..Default::default() - // }, - ..Default::default() - }; - // #[cfg(target_os = "linux")] - // initial_surface: iced::wayland::settings::InitialSurface::LayerSurface(layer_shell_settings()), - // #[cfg(target_os = "linux")] - // exit_on_close_request: false, - // + let (main_window_id, open_task) = window::open(settings); - let (main_window_id, open_task) = window::open(settings); + let mut commands = vec![]; - commands.push( - open_task.map(|_| AppMsg::Noop), - ); + commands.push( + open_task.map(|_| AppMsg::Noop), + ); - if !wayland { commands.push( window::gain_focus(main_window_id), ); commands.push( window::change_level(main_window_id, Level::AlwaysOnTop), - ) - } + ); + + (main_window_id, Task::batch(commands)) + } else { + let main_window_id = window::Id::unique(); + + (main_window_id, iced::platform_specific::shell::commands::layer_surface::get_layer_surface(layer_shell_settings(main_window_id))) + }; + + let mut commands = vec![ + font::load(BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded), + open_window + ]; let (client_context, global_state) = if cfg!(feature = "scenario_runner") { let gen_in = std::env::var("GAUNTLET_SCREENSHOT_GEN_IN") @@ -419,14 +412,13 @@ fn new( ) } -// TODO -// fn title(state: &AppModel, window: window::Id) -> String { -// if window != window::Id::MAIN { -// "Gauntlet".to_owned() -// } else { -// "Gauntlet HUD".to_owned() -// } -// } +fn title(state: &AppModel, window: window::Id) -> String { + if window == state.main_window_id { + "Gauntlet".to_owned() + } else { + "Gauntlet HUD".to_owned() + } +} fn update(state: &mut AppModel, message: AppMsg) -> Task { match message { @@ -553,10 +545,6 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { AppMsg::IcedEvent(Event::Keyboard(event)) => { match event { keyboard::Event::KeyPressed { key, modifiers, physical_key, text, .. } => { - let Physical::Code(physical_key) = physical_key else { - return Task::none() - }; - tracing::debug!("Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", key, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); match key { Key::Named(Named::ArrowUp) => state.global_state.up(&state.search_results), @@ -601,6 +589,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } }, _ => { + let Physical::Code(physical_key) = physical_key else { + return Task::none() + }; + match &mut state.global_state { GlobalState::MainView { sub_state, search_field_id, focused_search_result, .. } => { match sub_state { @@ -736,21 +728,21 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { AppMsg::IcedEvent(Event::Window(window::Event::Unfocused)) => { state.on_unfocused() } - // #[cfg(target_os = "linux")] - // AppMsg::IcedEvent( - // Event::PlatformSpecific( - // iced::wayland::core::event::PlatformSpecific::Wayland( - // iced::wayland::core::event::wayland::Event::Layer( - // iced::wayland::core::event::wayland::LayerEvent::Unfocused, - // _, - // _ - // ) - // ) - // ) - // ) => { - // // wayland layer shell doesn't have the same unfocused problem as the other platforms - // state.hide_window() - // } + #[cfg(target_os = "linux")] + AppMsg::IcedEvent( + Event::PlatformSpecific( + iced::core::event::PlatformSpecific::Wayland( + iced::core::event::wayland::Event::Layer( + iced::event::wayland::LayerEvent::Unfocused, + _, + _ + ) + ) + ) + ) => { + // wayland layer shell doesn't have the same unfocused problem as the other platforms + state.hide_window() + } AppMsg::IcedEvent(_) => Task::none(), AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::Noop, .. } => Task::none(), AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => state.global_state.back(), @@ -891,22 +883,8 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { }).await .expect("Unable to save screenshot") }, - |_| AppMsg::Close, - ) - } - AppMsg::Close => { - // TODO - // #[cfg(target_os = "linux")] - // if state.wayland { - // iced::wayland::commands::window::close_window(window::Id::MAIN) - // } else { - // window::close(window::Id::MAIN) - // } - // - // #[cfg(not(target_os = "linux"))] - // window::close(window::Id::MAIN) - - window::close(state.main_window_id) + |_| (), + ).then(|_| iced::exit()) } AppMsg::ToggleActionPanel { keyboard } => { match &mut state.global_state { @@ -1107,18 +1085,16 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { state.hud_display = Some(display); show_hud_window( - // TODO - // #[cfg(target_os = "linux")] - // state.wayland, + #[cfg(target_os = "linux")] + state.wayland, ) } AppMsg::CloseHudWindow { id } => { state.hud_display = None; close_hud_window( - // TODO - // #[cfg(target_os = "linux")] - // state.wayland, + #[cfg(target_os = "linux")] + state.wayland, id ) } @@ -1770,27 +1746,23 @@ impl AppModel { fn hide_window(&mut self) -> Task { let mut commands = vec![]; - // #[cfg(target_os = "linux")] - // if self.wayland { - // use iced::wayland::commands::layer_surface::KeyboardInteractivity; - // - // commands.push( - // iced::wayland::commands::layer_surface::destroy_layer_surface(window::Id::MAIN), - // ); - // commands.push( - // iced::wayland::commands::layer_surface::set_keyboard_interactivity(window::Id::MAIN, KeyboardInteractivity::None), - // ); - // } else { - // commands.push( - // window::change_mode(window::Id::MAIN, window::Mode::Hidden) - // ); - // }; - // - // #[cfg(not(target_os = "linux"))] - // commands.push( - // window::change_mode(window::Id::MAIN, window::Mode::Hidden) - // ); + #[cfg(target_os = "linux")] + if self.wayland { + use iced::platform_specific::shell::commands::layer_surface::KeyboardInteractivity; + commands.push( + iced::platform_specific::shell::commands::layer_surface::destroy_layer_surface(self.main_window_id), + ); + commands.push( + iced::platform_specific::shell::commands::layer_surface::set_keyboard_interactivity(self.main_window_id, KeyboardInteractivity::None), + ); + } else { + commands.push( + window::change_mode(self.main_window_id, window::Mode::Hidden) + ); + }; + + #[cfg(not(target_os = "linux"))] commands.push( window::change_mode(self.main_window_id, window::Mode::Hidden) ); @@ -1809,27 +1781,23 @@ impl AppModel { fn show_window(&mut self) -> Task { let mut commands = vec![]; - // #[cfg(target_os = "linux")] - // if self.wayland { - // use iced::wayland::commands::layer_surface::KeyboardInteractivity; - // - // commands.push( - // iced::wayland::commands::layer_surface::get_layer_surface(layer_shell_settings()), - // ); - // commands.push( - // iced::wayland::commands::layer_surface::set_keyboard_interactivity(window::Id::MAIN, KeyboardInteractivity::Exclusive), - // ); - // } else { - // commands.push( - // window::change_mode(window::Id::MAIN, window::Mode::Windowed) - // ); - // }; - // - // #[cfg(not(target_os = "linux"))] - // commands.push( - // window::change_mode(window::Id::MAIN, window::Mode::Windowed) - // ); + #[cfg(target_os = "linux")] + if self.wayland { + use iced::platform_specific::shell::commands::layer_surface::KeyboardInteractivity; + commands.push( + iced::platform_specific::shell::commands::layer_surface::get_layer_surface(layer_shell_settings(self.main_window_id)), + ); + commands.push( + iced::platform_specific::shell::commands::layer_surface::set_keyboard_interactivity(self.main_window_id, KeyboardInteractivity::Exclusive), + ); + } else { + commands.push( + window::change_mode(self.main_window_id, window::Mode::Windowed) + ); + }; + + #[cfg(not(target_os = "linux"))] commands.push( window::change_mode(self.main_window_id, window::Mode::Windowed) ); diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs index b72fe82..36cd117 100644 --- a/rust/client/src/ui/scroll_handle.rs +++ b/rust/client/src/ui/scroll_handle.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; +use iced::id::Id; use iced::Task; -use iced::widget::scrollable; use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; use crate::ui::AppMsg; @@ -10,7 +10,7 @@ pub const ESTIMATED_ACTION_ITEM_HEIGHT: f32 = 38.8; // TODO #[derive(Clone, Debug)] pub struct ScrollHandle { phantom: PhantomData, - pub scrollable_id: scrollable::Id, + pub scrollable_id: Id, pub index: Option, offset: usize, rows_per_view: usize, @@ -21,7 +21,7 @@ impl ScrollHandle { pub fn new(first_focused: bool, item_height: f32, rows_per_view: usize) -> ScrollHandle { ScrollHandle { phantom: PhantomData, - scrollable_id: scrollable::Id::unique(), + scrollable_id: Id::unique(), index: if first_focused { Some(0) } else { None }, offset: 0, rows_per_view, diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index e1ea86c..edeb67e 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -93,6 +93,7 @@ impl container::Catalog for GauntletComplexTheme { let background_color = &panel_theme.background_color; Style { + icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -110,6 +111,7 @@ impl container::Catalog for GauntletComplexTheme { let border_color = &theme.border_color; Style { + icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -126,6 +128,7 @@ impl container::Catalog for GauntletComplexTheme { let border_color = &theme.border_color; Style { + icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -141,6 +144,7 @@ impl container::Catalog for GauntletComplexTheme { let background_color = &theme.background_color; Style { + icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -156,6 +160,7 @@ impl container::Catalog for GauntletComplexTheme { let background_color = &theme.background_color; Style { + icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -172,6 +177,7 @@ impl container::Catalog for GauntletComplexTheme { let background_color = &tooltip_theme.background_color; Style { + icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -228,6 +234,7 @@ impl container::Catalog for GauntletComplexTheme { let background_color = &theme.background_color; Style { + icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { diff --git a/rust/client/src/ui/theme/image.rs b/rust/client/src/ui/theme/image.rs index c83a99b..6cfdf2b 100644 --- a/rust/client/src/ui/theme/image.rs +++ b/rust/client/src/ui/theme/image.rs @@ -6,7 +6,7 @@ pub enum ImageStyle { MainListItemIcon, } -impl<'a, Message: 'a> ThemableWidget<'a, Message> for Image { +impl<'a, Message: 'a> ThemableWidget<'a, Message> for Image<'a, iced::advanced::image::Handle> { type Kind = ImageStyle; fn themed(self, kind: ImageStyle) -> Element<'a, Message> { diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index d2cbfb1..0cdeb13 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -131,7 +131,7 @@ pub struct GauntletComplexTheme { impl Default for GauntletComplexTheme { fn default() -> Self { - unreachable!() + panic!("should not be called") } } @@ -1003,6 +1003,7 @@ impl DefaultStyle for GauntletComplexTheme { application::Appearance { background_color: Color::TRANSPARENT, text_color: theme.text.to_iced(), + icon_color: theme.text.to_iced(), } } } diff --git a/rust/management_client/src/components/shortcut_selector.rs b/rust/management_client/src/components/shortcut_selector.rs index bfe118f..7cc8662 100644 --- a/rust/management_client/src/components/shortcut_selector.rs +++ b/rust/management_client/src/components/shortcut_selector.rs @@ -158,7 +158,9 @@ where renderer, theme, &renderer::Style { + icon_color: renderer_style.icon_color, text_color: renderer_style.text_color, + scale_factor: renderer_style.scale_factor }, layout.children().next().unwrap(), cursor, @@ -178,8 +180,8 @@ where self.content.as_widget().children() } - fn diff(&self, tree: &mut Tree) { - self.content.as_widget().diff(tree); + fn diff(&mut self, tree: &mut Tree) { + self.content.as_widget_mut().diff(tree); } fn on_event( diff --git a/rust/management_client/src/theme.rs b/rust/management_client/src/theme.rs index e1e81c3..f10fc19 100644 --- a/rust/management_client/src/theme.rs +++ b/rust/management_client/src/theme.rs @@ -22,6 +22,7 @@ impl DefaultStyle for GauntletSettingsTheme { Appearance { background_color: BACKGROUND_DARKEST.to_iced(), text_color: TEXT_LIGHTEST.to_iced(), + icon_color: TEXT_LIGHTEST.to_iced(), } } } diff --git a/rust/management_client/src/views/plugins/table.rs b/rust/management_client/src/views/plugins/table.rs index c58971d..98cf211 100644 --- a/rust/management_client/src/views/plugins/table.rs +++ b/rust/management_client/src/views/plugins/table.rs @@ -3,6 +3,7 @@ use std::rc::Rc; use iced::{Alignment, Length, Renderer, Task}; use iced::advanced::text::Shaping; +use iced::id::Id; use iced::widget::{button, checkbox, container, horizontal_space, row, scrollable, Space, text, value}; use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; use iced_table::table; @@ -42,8 +43,8 @@ pub enum PluginTableMsgOut { pub struct PluginTableState { columns: Vec, rows: Vec, - header: scrollable::Id, - body: scrollable::Id, + header: Id, + body: Id, } pub enum PluginTableUpdateResult { @@ -61,8 +62,8 @@ impl PluginTableState { Column::new(ColumnKind::EnableToggle), ], rows: vec![], - header: scrollable::Id::unique(), - body: scrollable::Id::unique(), + header: Id::unique(), + body: Id::unique(), } } From 5cd1c2759e529708bc332c8cbe11d535c478a262 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:39:52 +0100 Subject: [PATCH 198/540] Recreate winit window each time instead of hiding and showing --- rust/client/src/ui/mod.rs | 264 +++++++++++++++++++++----------------- 1 file changed, 144 insertions(+), 120 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index d01d78f..b20a1c3 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -66,7 +66,7 @@ pub struct AppModel { global_hotkey_manager: Arc>, current_hotkey: Arc>>, frontend_receiver: Arc>>, - main_window_id: window::Id, + main_window_id: Option, focused: bool, wayland: bool, #[cfg(any(target_os = "macos", target_os = "windows"))] @@ -194,6 +194,25 @@ pub enum AppMsg { const WINDOW_WIDTH: f32 = 750.0; const WINDOW_HEIGHT: f32 = 450.0; +fn window_settings() -> window::Settings { + window::Settings { + size: Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), + position: Position::Centered, + resizable: false, + decorations: false, + transparent: true, + // todo macoss + // #[cfg(target_os = "macos")] + // platform_specific: PlatformSpecific { + // activation_policy: window::settings::ActivationPolicy::Accessory, + // activate_ignoring_other_apps: false, + // ..Default::default() + // }, + ..Default::default() + } +} + + #[cfg(target_os = "linux")] fn layer_shell_settings(main_window_id: window::Id) -> iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings { iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings { @@ -211,6 +230,34 @@ fn layer_shell_settings(main_window_id: window::Id) -> iced::platform_specific:: } } +fn open_main_window_non_wayland() -> (window::Id, Task) { + let (main_window_id, open_task) = window::open(window_settings()); + + let mut tasks = vec![]; + + tasks.push( + open_task.map(|_| AppMsg::Noop), + ); + + tasks.push( + window::gain_focus(main_window_id), + ); + + tasks.push( + window::change_level(main_window_id, Level::AlwaysOnTop), + ); + + (main_window_id, Task::batch(tasks)) +} + +#[cfg(target_os = "linux")] +fn open_main_window_wayland() -> (window::Id, Task) { + let main_window_id = window::Id::unique(); + + (main_window_id, iced::platform_specific::shell::commands::layer_surface::get_layer_surface(layer_shell_settings(main_window_id))) +} + + pub fn run( minimized: bool, frontend_receiver: RequestReceiver, @@ -247,52 +294,28 @@ fn new( let global_hotkey_manager = GlobalHotKeyManager::new() .expect("unable to create global hot key manager"); - let (main_window_id, open_window) = if !wayland { - let settings = window::Settings { - size: Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), - position: Position::Centered, - resizable: false, - decorations: false, - transparent: true, - visible: !minimized, - // todo macoss - // #[cfg(target_os = "macos")] - // platform_specific: PlatformSpecific { - // activation_policy: window::settings::ActivationPolicy::Accessory, - // activate_ignoring_other_apps: false, - // ..Default::default() - // }, - ..Default::default() + let mut tasks = vec![ + font::load(BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded), + ]; + + let main_window_id = if !minimized { + #[cfg(target_os = "linux")] + let (main_window_id, open_task) = if wayland { + open_main_window_wayland() + } else { + open_main_window_non_wayland() }; - let (main_window_id, open_task) = window::open(settings); + #[cfg(not(target_os = "linux"))] + let (main_window_id, open_task) = open_main_window_non_wayland(); - let mut commands = vec![]; + tasks.push(open_task); - commands.push( - open_task.map(|_| AppMsg::Noop), - ); - - commands.push( - window::gain_focus(main_window_id), - ); - - commands.push( - window::change_level(main_window_id, Level::AlwaysOnTop), - ); - - (main_window_id, Task::batch(commands)) + Some(main_window_id) } else { - let main_window_id = window::Id::unique(); - - (main_window_id, iced::platform_specific::shell::commands::layer_surface::get_layer_surface(layer_shell_settings(main_window_id))) + None }; - let mut commands = vec![ - font::load(BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded), - open_window - ]; - let (client_context, global_state) = if cfg!(feature = "scenario_runner") { let gen_in = std::env::var("GAUNTLET_SCREENSHOT_GEN_IN") .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_IN"); @@ -309,7 +332,7 @@ fn new( let event: ScenarioFrontendEvent = serde_json::from_str(&gen_in) .expect("GAUNTLET_SCREENSHOT_GEN_IN is not valid json"); - commands.push( + tasks.push( Task::perform( async { tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; @@ -342,7 +365,7 @@ fn new( let context = Arc::new(StdRwLock::new(context)); - commands.push(Task::perform(async { }, move |_| AppMsg::ReplaceView { top_level_view, has_children, render_location })); + tasks.push(Task::perform(async { }, move |_| AppMsg::ReplaceView { top_level_view, has_children, render_location })); let state= match render_location { UiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique(), context.clone()), @@ -408,15 +431,22 @@ fn new( loading_bar_state: HashMap::new(), hud_display: None, }, - Task::batch(commands), + Task::batch(tasks), ) } fn title(state: &AppModel, window: window::Id) -> String { - if window == state.main_window_id { - "Gauntlet".to_owned() - } else { - "Gauntlet HUD".to_owned() + match state.main_window_id { + Some(main_window_id) => { + if window == main_window_id { + "Gauntlet".to_owned() + } else { + "Gauntlet HUD".to_owned() + } + } + None => { + "Gauntlet HUD".to_owned() + } } } @@ -851,7 +881,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { fs::create_dir_all(Path::new(&save_path).parent().expect("no parent?")) .expect("unable to create scenario out directories"); - window::screenshot(state.main_window_id) + let window_id = state.main_window_id + .expect("window id should be present when making screenshot"); + + window::screenshot(window_id) .map(move |screenshot| AppMsg::ScreenshotDone { save_path: save_path.clone(), screenshot, @@ -1193,43 +1226,57 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } fn view(state: &AppModel, window: window::Id) -> Element<'_, AppMsg> { - if window != state.main_window_id { - return match &state.hud_display { - Some(hud_display) => { - let hud: Element<_> = text(hud_display.to_string()) - .shaping(Shaping::Advanced) - .into(); - - let hud = container(hud) - .align_x(Horizontal::Center) - .align_y(Vertical::Center) - .height(Length::Fill) - .themed(ContainerStyle::HudInner); - - let hud = container(hud) - .align_x(Horizontal::Center) - .align_y(Vertical::Center) - .height(Length::Fill) - .themed(ContainerStyle::Hud); - - let hud = container(hud) - .height(Length::Fill) - .width(Length::Fill) - .align_x(Horizontal::Center) - .align_y(Vertical::Center) - .class(ContainerStyleInner::Transparent) - .into(); - - hud - } - None => { - horizontal_space() - .into() + match state.main_window_id { + None => { + view_hud(state) + } + Some(main_window_id) => { + if window != main_window_id { + view_hud(state) + } else { + view_main(state) } } } +} +fn view_hud(state: &AppModel) -> Element<'_, AppMsg> { + match &state.hud_display { + Some(hud_display) => { + let hud: Element<_> = text(hud_display.to_string()) + .shaping(Shaping::Advanced) + .into(); + let hud = container(hud) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .height(Length::Fill) + .themed(ContainerStyle::HudInner); + + let hud = container(hud) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .height(Length::Fill) + .themed(ContainerStyle::Hud); + + let hud = container(hud) + .height(Length::Fill) + .width(Length::Fill) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .class(ContainerStyleInner::Transparent) + .into(); + + hud + } + None => { + horizontal_space() + .into() + } + } +} + +fn view_main(state: &AppModel) -> Element<'_, AppMsg> { match &state.global_state { GlobalState::ErrorView { error_view } => { match error_view { @@ -1744,27 +1791,26 @@ impl AppModel { } fn hide_window(&mut self) -> Task { + let Some(main_window_id) = self.main_window_id else { + return Task::none() + }; + let mut commands = vec![]; #[cfg(target_os = "linux")] if self.wayland { - use iced::platform_specific::shell::commands::layer_surface::KeyboardInteractivity; - commands.push( - iced::platform_specific::shell::commands::layer_surface::destroy_layer_surface(self.main_window_id), - ); - commands.push( - iced::platform_specific::shell::commands::layer_surface::set_keyboard_interactivity(self.main_window_id, KeyboardInteractivity::None), + iced::platform_specific::shell::commands::layer_surface::destroy_layer_surface(main_window_id), ); } else { commands.push( - window::change_mode(self.main_window_id, window::Mode::Hidden) + window::close(main_window_id) ); }; #[cfg(not(target_os = "linux"))] commands.push( - window::change_mode(self.main_window_id, window::Mode::Hidden) + window::close(main_window_id) ); match &self.global_state { @@ -1779,34 +1825,22 @@ impl AppModel { } fn show_window(&mut self) -> Task { - let mut commands = vec![]; - #[cfg(target_os = "linux")] - if self.wayland { - use iced::platform_specific::shell::commands::layer_surface::KeyboardInteractivity; - - commands.push( - iced::platform_specific::shell::commands::layer_surface::get_layer_surface(layer_shell_settings(self.main_window_id)), - ); - commands.push( - iced::platform_specific::shell::commands::layer_surface::set_keyboard_interactivity(self.main_window_id, KeyboardInteractivity::Exclusive), - ); + let (main_window_id, open_task) = if self.wayland { + open_main_window_wayland() } else { - commands.push( - window::change_mode(self.main_window_id, window::Mode::Windowed) - ); + open_main_window_non_wayland() }; #[cfg(not(target_os = "linux"))] - commands.push( - window::change_mode(self.main_window_id, window::Mode::Windowed) - ); + let (main_window_id, open_task) = open_main_window_non_wayland(); - commands.push( + self.main_window_id = Some(main_window_id); + + Task::batch([ + open_task, self.reset_window_state() - ); - - Task::batch(commands) + ]) } fn reset_window_state(&mut self) -> Task { @@ -1816,17 +1850,7 @@ impl AppModel { client_context.clear_all_inline_views(); - let mut commands = vec![ - GlobalState::initial(&mut self.global_state, self.client_context.clone()), - ]; - - if !self.wayland { - commands.push( - window::gain_focus(self.main_window_id), - ); - } - - Task::batch(commands) + GlobalState::initial(&mut self.global_state, self.client_context.clone()) } fn open_plugin_view(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Task { From 27eccbebea74659bfebfc1a362102459d44f9b01 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 8 Dec 2024 15:59:16 +0100 Subject: [PATCH 199/540] Migrate back iced_layershell because of performance problems and window transparency on macos not working --- Cargo.lock | 1395 ++++++++--------- Cargo.toml | 10 +- rust/client/Cargo.toml | 7 +- rust/client/src/ui/hud/mod.rs | 32 +- rust/client/src/ui/mod.rs | 140 +- rust/client/src/ui/scroll_handle.rs | 3 +- rust/client/src/ui/theme/container.rs | 7 - rust/client/src/ui/theme/image.rs | 2 +- rust/client/src/ui/theme/mod.rs | 13 +- .../src/components/shortcut_selector.rs | 6 +- rust/management_client/src/theme.rs | 1 - .../src/views/plugins/table.rs | 2 +- 12 files changed, 765 insertions(+), 853 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d4cfe1..e14e26a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,86 +28,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" -[[package]] -name = "accesskit" -version = "0.16.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" - -[[package]] -name = "accesskit_atspi_common" -version = "0.9.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" -dependencies = [ - "accesskit", - "accesskit_consumer", - "atspi-common", - "serde", - "thiserror", - "zvariant", -] - -[[package]] -name = "accesskit_consumer" -version = "0.24.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" -dependencies = [ - "accesskit", - "immutable-chunkmap", -] - -[[package]] -name = "accesskit_macos" -version = "0.17.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" -dependencies = [ - "accesskit", - "accesskit_consumer", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "once_cell", -] - -[[package]] -name = "accesskit_unix" -version = "0.12.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" -dependencies = [ - "accesskit", - "accesskit_atspi_common", - "atspi", - "futures-lite 1.13.0", - "serde", - "tokio", - "tokio-stream", - "zbus", -] - -[[package]] -name = "accesskit_windows" -version = "0.22.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" -dependencies = [ - "accesskit", - "accesskit_consumer", - "paste", - "static_assertions", - "windows 0.54.0", -] - -[[package]] -name = "accesskit_winit" -version = "0.22.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13#956955342dadab7e588e21be726817fca39510f3" -dependencies = [ - "accesskit", - "accesskit_macos", - "accesskit_unix", - "accesskit_windows", - "raw-window-handle", - "winit", -] - [[package]] name = "addr2line" version = "0.21.0" @@ -173,6 +93,17 @@ dependencies = [ "aes", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.14", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -375,7 +306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -429,7 +360,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", "synstructure", ] @@ -441,7 +372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", ] @@ -453,19 +384,21 @@ checksum = "c09c69dffe06d222d072c878c3afe86eee2179806f20503faec97250268b4c24" dependencies = [ "pmutil", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "swc_macros_common", "syn 2.0.89", ] [[package]] name = "async-broadcast" -version = "0.5.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" dependencies = [ - "event-listener 2.5.3", + "event-listener 5.3.1", + "event-listener-strategy", "futures-core", + "pin-project-lite", ] [[package]] @@ -495,23 +428,27 @@ dependencies = [ ] [[package]] -name = "async-io" -version = "1.13.0" +name = "async-executor" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", + "async-task", "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", + "fastrand", + "futures-lite", "slab", - "socket2 0.4.10", - "waker-fn", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", ] [[package]] @@ -520,28 +457,19 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ - "async-lock 3.4.0", + "async-lock", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.5.0", + "futures-lite", "parking", - "polling 3.6.0", - "rustix 0.38.32", + "polling", + "rustix", "slab", "tracing", "windows-sys 0.59.0", ] -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - [[package]] name = "async-lock" version = "3.4.0" @@ -555,19 +483,21 @@ dependencies = [ [[package]] name = "async-process" -version = "1.8.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" dependencies = [ - "async-io 1.13.0", - "async-lock 2.8.0", + "async-channel", + "async-io", + "async-lock", "async-signal", + "async-task", "blocking", "cfg-if", - "event-listener 3.1.0", - "futures-lite 1.13.0", - "rustix 0.38.32", - "windows-sys 0.48.0", + "event-listener 5.3.1", + "futures-lite", + "rustix", + "tracing", ] [[package]] @@ -577,7 +507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -587,13 +517,13 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ - "async-io 2.4.0", - "async-lock 3.4.0", + "async-io", + "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 0.38.32", + "rustix", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -617,7 +547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -634,7 +564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -676,54 +606,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atspi" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6059f350ab6f593ea00727b334265c4dfc7fd442ee32d264794bd9bdc68e87ca" -dependencies = [ - "atspi-common", - "atspi-connection", - "atspi-proxies", -] - -[[package]] -name = "atspi-common" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92af95f966d2431f962bc632c2e68eda7777330158bf640c4af4249349b2cdf5" -dependencies = [ - "enumflags2", - "serde", - "static_assertions", - "zbus", - "zbus_names", - "zvariant", -] - -[[package]] -name = "atspi-connection" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c65e7d70f86d4c0e3b2d585d9bf3f979f0b19d635a336725a88d279f76b939" -dependencies = [ - "atspi-common", - "atspi-proxies", - "futures-lite 1.13.0", - "zbus", -] - -[[package]] -name = "atspi-proxies" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6495661273703e7a229356dcbe8c8f38223d697aacfaf0e13590a9ac9977bb52" -dependencies = [ - "atspi-common", - "serde", - "zbus", -] - [[package]] name = "attohttpc" version = "0.27.0" @@ -794,7 +676,7 @@ dependencies = [ "itoa", "matchit", "memchr", - "mime 0.3.17", + "mime", "percent-encoding", "pin-project-lite", "rustversion", @@ -816,7 +698,7 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "mime 0.3.17", + "mime", "rustversion", "tower-layer", "tower-service", @@ -994,7 +876,7 @@ dependencies = [ "async-channel", "async-task", "futures-io", - "futures-lite 2.5.0", + "futures-lite", "piper", ] @@ -1078,7 +960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -1149,6 +1031,20 @@ dependencies = [ "system-deps", ] +[[package]] +name = "calloop" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix", + "slab", + "thiserror", +] + [[package]] name = "calloop" version = "0.13.0" @@ -1157,20 +1053,57 @@ checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ "bitflags 2.6.0", "log", - "polling 3.6.0", - "rustix 0.38.32", + "polling", + "rustix", "slab", "thiserror", ] +[[package]] +name = "calloop" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ead1e1514bce44c0f40e027899fbc595907fc112635bed21b3b5d975c0a5e7" +dependencies = [ + "bitflags 2.6.0", + "polling", + "rustix", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +dependencies = [ + "calloop 0.12.4", + "rustix", + "wayland-backend", + "wayland-client 0.31.7", +] + [[package]] name = "calloop-wayland-source" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ - "calloop", - "rustix 0.38.32", + "calloop 0.13.0", + "rustix", + "wayland-backend", + "wayland-client 0.31.7", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a7a1dbbe026a55ef47a500b123af5a9a0914520f061d467914cf21be95daf" +dependencies = [ + "calloop 0.14.1", + "rustix", "wayland-backend", "wayland-client 0.31.7", ] @@ -1322,7 +1255,7 @@ checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -1360,6 +1293,7 @@ dependencies = [ "iced", "iced_aw", "iced_fonts", + "iced_layershell", "image 0.25.1", "itertools 0.12.1", "once_cell", @@ -1396,28 +1330,29 @@ dependencies = [ [[package]] name = "clipboard_macos" -version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13#a83bf83784276aaa882ef13555295a2ad9edd265" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" dependencies = [ - "objc", - "objc-foundation", - "objc_id", + "objc2", + "objc2-app-kit", + "objc2-foundation", ] [[package]] name = "clipboard_wayland" version = "0.2.2" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13#a83bf83784276aaa882ef13555295a2ad9edd265" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" dependencies = [ - "dnd", - "mime 0.1.0", "smithay-clipboard", ] [[package]] name = "clipboard_x11" version = "0.4.2" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13#a83bf83784276aaa882ef13555295a2ad9edd265" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" dependencies = [ "thiserror", "x11rb 0.13.1", @@ -1432,22 +1367,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation 0.1.2", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "foreign-types 0.5.0", - "libc", - "objc", -] - [[package]] name = "cocoa" version = "0.26.0" @@ -1456,7 +1375,7 @@ checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ "bitflags 2.6.0", "block", - "cocoa-foundation 0.2.0", + "cocoa-foundation", "core-foundation 0.10.0", "core-graphics 0.24.0", "foreign-types 0.5.0", @@ -1464,20 +1383,6 @@ dependencies = [ "objc", ] -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "libc", - "objc", -] - [[package]] name = "cocoa-foundation" version = "0.2.0" @@ -1715,36 +1620,11 @@ dependencies = [ "libc", ] -[[package]] -name = "cosmic-client-toolkit" -version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-protocols?rev=d218c76#d218c76b58c7a3b20dd5e7943f93fc306a1b81b8" -dependencies = [ - "cosmic-protocols", - "libc", - "smithay-client-toolkit", - "wayland-client 0.31.7", - "wayland-protocols 0.32.5", -] - -[[package]] -name = "cosmic-protocols" -version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-protocols?rev=d218c76#d218c76b58c7a3b20dd5e7943f93fc306a1b81b8" -dependencies = [ - "bitflags 2.6.0", - "wayland-backend", - "wayland-client 0.31.7", - "wayland-protocols 0.32.5", - "wayland-protocols-wlr", - "wayland-scanner 0.31.5", - "wayland-server", -] - [[package]] name = "cosmic-text" version = "0.12.1" -source = "git+https://github.com/pop-os/cosmic-text.git#1f4065c1c3399efad58841082212f7c039b58480" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" dependencies = [ "bitflags 2.6.0", "fontdb", @@ -1754,7 +1634,6 @@ dependencies = [ "rustc-hash 1.1.0", "rustybuzz", "self_cell", - "smol_str", "swash", "sys-locale", "ttf-parser 0.21.1", @@ -1938,10 +1817,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] +[[package]] +name = "dark-light" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a76fa97167fa740dcdbfe18e8895601e1bc36525f09b044e00916e717c03a3c" +dependencies = [ + "dconf_rs", + "detect-desktop-environment", + "dirs 4.0.0", + "objc", + "rust-ini 0.18.0", + "web-sys", + "winreg 0.10.1", + "zbus", +] + [[package]] name = "darling" version = "0.20.10" @@ -1961,7 +1856,7 @@ dependencies = [ "fnv", "ident_case", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "strsim", "syn 2.0.89", ] @@ -1973,7 +1868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -2002,6 +1897,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41b319d1b62ffbd002e057f36bebd1f42b9f97927c9577461d855f3513c4289f" +[[package]] +name = "dconf_rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" + [[package]] name = "debugid" version = "0.8.0" @@ -2041,7 +1942,7 @@ checksum = "3047b312b7451e3190865713a4dd6e1f821aed614ada219766ebc3024a690435" dependencies = [ "once_cell", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -2257,7 +2158,7 @@ dependencies = [ "hyper 0.14.28", "hyper 1.0.0-rc.4", "memmem", - "mime 0.3.17", + "mime", "once_cell", "percent-encoding", "phf 0.10.1", @@ -2358,7 +2259,7 @@ dependencies = [ "log", "pin-project", "serde", - "socket2 0.5.6", + "socket2", "tokio", "trust-dns-proto", "trust-dns-resolver", @@ -2453,7 +2354,7 @@ dependencies = [ "pmutil", "proc-macro-crate 1.3.1", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "regex", "strum", "strum_macros", @@ -2671,17 +2572,6 @@ dependencies = [ "serde", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.36", - "syn 1.0.109", -] - [[package]] name = "derive-new" version = "0.5.9" @@ -2689,7 +2579,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", ] @@ -2710,7 +2600,7 @@ checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" dependencies = [ "darling", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -2732,11 +2622,17 @@ checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case 0.4.0", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "rustc_version 0.4.0", "syn 1.0.109", ] +[[package]] +name = "detect-desktop-environment" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" + [[package]] name = "digest" version = "0.8.1" @@ -2821,7 +2717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -2831,7 +2727,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.3", ] [[package]] @@ -2857,6 +2753,12 @@ dependencies = [ "syn 0.15.44", ] +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "dlv-list" version = "0.5.2" @@ -2866,18 +2768,6 @@ dependencies = [ "const-random", ] -[[package]] -name = "dnd" -version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13#a83bf83784276aaa882ef13555295a2ad9edd265" -dependencies = [ - "bitflags 2.6.0", - "mime 0.1.0", - "raw-window-handle", - "smithay-client-toolkit", - "smithay-clipboard", -] - [[package]] name = "document-features" version = "0.2.10" @@ -2908,7 +2798,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "dpi" version = "0.1.1" -source = "git+https://github.com/pop-os/winit.git?tag=iced-xdg-surface-0.13#1cc02bdab141072eaabad639d74b032fd0fcc62e" +source = "git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d#254d6b3420ce4e674f516f7a2bd440665e05484d" [[package]] name = "dprint-swc-ext" @@ -2928,25 +2818,25 @@ dependencies = [ [[package]] name = "drm" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" +checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" dependencies = [ "bitflags 2.6.0", "bytemuck", "drm-ffi", "drm-fourcc", - "rustix 0.38.32", + "rustix", ] [[package]] name = "drm-ffi" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41334f8405792483e32ad05fbb9c5680ff4e84491883d2947a4757dc54cb2ac6" +checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" dependencies = [ "drm-sys", - "rustix 0.38.32", + "rustix", ] [[package]] @@ -2957,9 +2847,9 @@ checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" [[package]] name = "drm-sys" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d09ff881f92f118b11105ba5e34ff8f4adf27b30dae8f12e28c193af1c83176" +checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" dependencies = [ "libc", "linux-raw-sys 0.6.4", @@ -2998,7 +2888,7 @@ dependencies = [ "lazy_static", "proc-macro-error", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", ] @@ -3109,6 +2999,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enum-as-inner" version = "0.5.1" @@ -3117,7 +3013,7 @@ checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ "heck 0.4.1", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", ] @@ -3138,7 +3034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -3241,17 +3137,6 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "event-listener" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - [[package]] name = "event-listener" version = "5.3.1" @@ -3324,18 +3209,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.9.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fastrand" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fastwebsockets" @@ -3494,7 +3370,7 @@ checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" dependencies = [ "fontconfig-parser", "log", - "memmap2 0.9.4", + "memmap2 0.9.5", "slotmap", "tinyvec", "ttf-parser 0.20.0", @@ -3526,7 +3402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -3559,7 +3435,7 @@ checksum = "a8ef34245e0540c9a3ce7a28340b98d2c12b75da0d446da4e8224923fcaa0c16" dependencies = [ "dirs 5.0.1", "once_cell", - "rust-ini", + "rust-ini 0.20.0", "thiserror", "xdg", ] @@ -3603,7 +3479,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" dependencies = [ - "rustix 0.38.32", + "rustix", "windows-sys 0.52.0", ] @@ -3628,9 +3504,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -3643,9 +3519,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -3653,15 +3529,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -3682,24 +3558,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -3707,38 +3568,41 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ + "fastrand", "futures-core", + "futures-io", + "parking", "pin-project-lite", ] [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -4079,7 +3943,7 @@ dependencies = [ "proc-macro-crate 2.0.0", "proc-macro-error", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -4128,6 +3992,18 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "glyphon" +version = "0.5.0" +source = "git+https://github.com/hecrj/glyphon.git?rev=09712a70df7431e9a3b1ac1bbd4fb634096cb3b4#09712a70df7431e9a3b1ac1bbd4fb634096cb3b4" +dependencies = [ + "cosmic-text", + "etagere", + "lru", + "rustc-hash 2.0.0", + "wgpu", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -4260,7 +4136,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro-error", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -4308,6 +4184,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -4315,7 +4194,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] @@ -4492,7 +4371,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.6", + "socket2", "tokio", "tower-service", "tracing", @@ -4572,34 +4451,21 @@ dependencies = [ [[package]] name = "iced" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" dependencies = [ - "dnd", - "iced_accessibility", "iced_core", "iced_futures", "iced_renderer", "iced_widget", "iced_winit", "image 0.24.9", - "mime 0.1.0", "thiserror", - "window_clipboard", -] - -[[package]] -name = "iced_accessibility" -version = "0.1.0" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" -dependencies = [ - "accesskit", - "accesskit_winit", ] [[package]] name = "iced_aw" version = "0.11.99" -source = "git+https://github.com/project-gauntlet/iced_aw.git?branch=gauntlet-0.13#817cc1c8c4aadd22e300d0edb2adaed2d434d962" +source = "git+https://github.com/project-gauntlet/iced_aw.git?branch=gauntlet-0.13#c5014d7ad426ae14501b289f37ccdb6f6f307127" dependencies = [ "cfg-if", "chrono", @@ -4613,24 +4479,20 @@ dependencies = [ [[package]] name = "iced_core" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" dependencies = [ "bitflags 2.6.0", "bytes", - "cosmic-client-toolkit", - "dnd", + "dark-light", "glam", "log", - "mime 0.1.0", "num-traits", "once_cell", "palette", - "raw-window-handle", "rustc-hash 2.0.0", "smol_str", "thiserror", "web-time", - "window_clipboard", ] [[package]] @@ -4644,7 +4506,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" dependencies = [ "futures", "iced_core", @@ -4655,22 +4517,10 @@ dependencies = [ "wasm-timer", ] -[[package]] -name = "iced_glyphon" -version = "0.6.0" -source = "git+https://github.com/pop-os/glyphon.git?branch=iced-0.13#52b11b2a30e69c9b97c88a737138000c7eb6952a" -dependencies = [ - "cosmic-text", - "etagere", - "lru", - "rustc-hash 2.0.0", - "wgpu", -] - [[package]] name = "iced_graphics" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -4688,10 +4538,41 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "iced_layershell" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#bc47b24351a4a0946b03a1dec7e586ddde408b7d" +dependencies = [ + "futures", + "iced", + "iced_core", + "iced_futures", + "iced_graphics", + "iced_layershell_macros", + "iced_renderer", + "iced_runtime", + "layershellev", + "thiserror", + "tracing", + "window_clipboard", +] + +[[package]] +name = "iced_layershell_macros" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#bc47b24351a4a0946b03a1dec7e586ddde408b7d" +dependencies = [ + "darling", + "manyhow", + "proc-macro2 1.0.92", + "quote 1.0.37", + "syn 2.0.89", +] + [[package]] name = "iced_renderer" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -4703,22 +4584,19 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" dependencies = [ "bytes", - "cosmic-client-toolkit", - "dnd", "iced_core", "iced_futures", "raw-window-handle", "thiserror", - "window_clipboard", ] [[package]] name = "iced_table" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced_table.git?branch=gauntlet-0.13#3e0b0257e3efa34776226aa30566e7b3df659783" +source = "git+https://github.com/project-gauntlet/iced_table.git?branch=gauntlet-0.13#a5a1095db8c3aeca4ae6ad851a349147e579a1df" dependencies = [ "iced_core", "iced_widget", @@ -4727,7 +4605,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" dependencies = [ "bytemuck", "cosmic-text", @@ -4742,39 +4620,27 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" dependencies = [ - "as-raw-xcb-connection", "bitflags 2.6.0", "bytemuck", - "cosmic-client-toolkit", "futures", "glam", + "glyphon", "guillotiere", - "iced_glyphon", "iced_graphics", "log", "once_cell", - "raw-window-handle", "rustc-hash 2.0.0", - "rustix 0.38.32", "thiserror", - "tiny-xlib", - "wayland-backend", - "wayland-client 0.31.7", - "wayland-protocols 0.32.5", - "wayland-sys 0.31.5", "wgpu", - "x11rb 0.13.1", ] [[package]] name = "iced_widget" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" dependencies = [ - "cosmic-client-toolkit", - "dnd", "iced_renderer", "iced_runtime", "num-traits", @@ -4783,34 +4649,25 @@ dependencies = [ "rustc-hash 2.0.0", "thiserror", "unicode-segmentation", - "window_clipboard", ] [[package]] name = "iced_winit" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#42467485932f37cdddafbca0a34e29e6d37dd5e0" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" dependencies = [ - "cosmic-client-toolkit", - "dnd", "iced_futures", "iced_graphics", "iced_runtime", "log", - "raw-window-handle", "rustc-hash 2.0.0", "thiserror", "tracing", "wasm-bindgen-futures", - "wayland-backend", - "wayland-protocols 0.32.5", "web-sys", "winapi", "window_clipboard", "winit", - "xkbcommon", - "xkbcommon-dl", - "xkeysym", ] [[package]] @@ -4929,15 +4786,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" -[[package]] -name = "immutable-chunkmap" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f97096f508d54f8f8ab8957862eee2ccd628847b6217af1a335e1c44dee578" -dependencies = [ - "arrayvec", -] - [[package]] name = "include_dir" version = "0.7.3" @@ -4954,7 +4802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", ] [[package]] @@ -5028,34 +4876,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "io-lifetimes" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" - [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.6", + "socket2", "widestring", "windows-sys 0.48.0", "winreg 0.50.0", @@ -5084,7 +4915,7 @@ checksum = "59a85abdc13717906baccb5a1e435556ce0df215f242892f721dff62bf25288f" dependencies = [ "Inflector", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -5284,6 +5115,27 @@ dependencies = [ "smallvec", ] +[[package]] +name = "layershellev" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#bc47b24351a4a0946b03a1dec7e586ddde408b7d" +dependencies = [ + "bitflags 2.6.0", + "calloop 0.14.1", + "calloop-wayland-source 0.4.0", + "raw-window-handle", + "tempfile", + "thiserror", + "tracing", + "waycrate_xkbkeycode", + "wayland-backend", + "wayland-client 0.31.7", + "wayland-cursor", + "wayland-protocols 0.32.5", + "wayland-protocols-misc", + "wayland-protocols-wlr 0.3.5", +] + [[package]] name = "lazy-regex" version = "2.5.0" @@ -5313,7 +5165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8edfc11b8f56ce85e207e62ea21557cfa09bb24a8f6b04ae181b086ff8611c22" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "regex", "syn 1.0.109", ] @@ -5325,7 +5177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "regex", "syn 2.0.89", ] @@ -5533,15 +5385,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "linux-raw-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "linux-raw-sys" @@ -5647,6 +5493,30 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "darling_core", + "manyhow-macros", + "proc-macro2 1.0.92", + "quote 1.0.37", + "syn 2.0.89", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2 1.0.92", + "quote 1.0.37", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -5730,18 +5600,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.8.0" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" -dependencies = [ - "libc", -] - -[[package]] -name = "memmap2" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" dependencies = [ "libc", ] @@ -5803,14 +5664,6 @@ dependencies = [ "paste", ] -[[package]] -name = "mime" -version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13#a83bf83784276aaa882ef13555295a2ad9edd265" -dependencies = [ - "smithay-clipboard", -] - [[package]] name = "mime" version = "0.3.17" @@ -5823,7 +5676,7 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ - "mime 0.3.17", + "mime", "unicase", ] @@ -5876,7 +5729,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" dependencies = [ - "cocoa 0.26.0", + "cocoa", "crossbeam-channel", "dpi 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "gtk", @@ -6014,6 +5867,19 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset 0.9.1", +] + [[package]] name = "nom" version = "7.1.3" @@ -6111,7 +5977,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -6194,7 +6060,7 @@ checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -6509,7 +6375,7 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed29bb6f7d6ac14023acb332a356f3891265d780e254057c866dbe7a909d2d2d" dependencies = [ - "ahash", + "ahash 0.8.11", "hashbrown 0.15.0", "parking_lot 0.12.1", "stable_deref_trait", @@ -6593,13 +6459,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list 0.3.0", + "hashbrown 0.12.3", +] + [[package]] name = "ordered-multimap" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ - "dlv-list", + "dlv-list 0.5.2", "hashbrown 0.14.3", ] @@ -6655,7 +6531,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2 1.0.92", "proc-macro2-diagnostics", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -6766,7 +6642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8890702dbec0bad9116041ae586f84805b13eecd1d8b1df27c29998a9969d6d" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -6973,7 +6849,7 @@ dependencies = [ "phf_shared 0.10.0", "proc-macro-hack", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", ] @@ -6986,7 +6862,7 @@ dependencies = [ "phf_generator 0.11.2", "phf_shared 0.11.2", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -7030,7 +6906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -7053,7 +6929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.0.2", + "fastrand", "futures-io", ] @@ -7152,7 +7028,7 @@ checksum = "69e940d8d8db30c6f4cc37dab9aab61f4c9cc1e6efb6d18902ab88fa09c03560" dependencies = [ "darling", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -7163,7 +7039,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -7192,22 +7068,6 @@ dependencies = [ "miniz_oxide 0.7.2", ] -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - [[package]] name = "polling" version = "3.6.0" @@ -7218,7 +7078,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 0.38.32", + "rustix", "tracing", "windows-sys 0.52.0", ] @@ -7333,7 +7193,7 @@ checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", "version_check", ] @@ -7345,7 +7205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "version_check", ] @@ -7355,6 +7215,17 @@ version = "0.5.20+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.37", + "smallvec", +] + [[package]] name = "proc-macro2" version = "0.4.30" @@ -7380,7 +7251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", "version_check", "yansi", @@ -7401,7 +7272,7 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -7477,7 +7348,7 @@ dependencies = [ "anyhow", "itertools 0.10.5", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", ] @@ -7490,7 +7361,7 @@ dependencies = [ "anyhow", "itertools 0.12.1", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -7571,9 +7442,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2 1.0.92", ] @@ -7743,6 +7614,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "redox_users" version = "0.4.5" @@ -7824,7 +7704,7 @@ dependencies = [ "ipnet", "js-sys", "log", - "mime 0.3.17", + "mime", "once_cell", "percent-encoding", "pin-project-lite", @@ -7956,11 +7836,11 @@ checksum = "1f1ae91455a4c82892d9513fcfa1ac8faff6c523602d0041536341882714aede" dependencies = [ "basic-toml", "memchr", - "mime 0.3.17", + "mime", "mime_guess", "once_map", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "rinja_parser", "rustc-hash 2.0.0", "serde", @@ -8066,7 +7946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "rust-embed-utils", "shellexpand", "syn 2.0.89", @@ -8083,6 +7963,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap 0.4.3", +] + [[package]] name = "rust-ini" version = "0.20.0" @@ -8090,7 +7980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" dependencies = [ "cfg-if", - "ordered-multimap", + "ordered-multimap 0.7.3", ] [[package]] @@ -8150,28 +8040,14 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.27" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" -dependencies = [ - "bitflags 1.3.2", - "errno 0.3.8", - "io-lifetimes 1.0.11", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags 2.6.0", "errno 0.3.8", "libc", - "linux-raw-sys 0.4.13", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] @@ -8375,14 +8251,14 @@ dependencies = [ [[package]] name = "sctk-adwaita" -version = "0.10.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +checksum = "7555fcb4f753d095d734fdefebb0ad8c98478a21db500492d87c55913d3b0086" dependencies = [ "ab_glyph", "log", - "memmap2 0.9.4", - "smithay-client-toolkit", + "memmap2 0.9.5", + "smithay-client-toolkit 0.18.1", "tiny-skia", ] @@ -8521,7 +8397,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -8544,7 +8420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -8611,7 +8487,7 @@ checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -8763,7 +8639,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" dependencies = [ - "quote 1.0.36", + "quote 1.0.37", ] [[package]] @@ -8847,6 +8723,31 @@ dependencies = [ "version_check", ] +[[package]] +name = "smithay-client-toolkit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +dependencies = [ + "bitflags 2.6.0", + "calloop 0.12.4", + "calloop-wayland-source 0.2.0", + "cursor-icon", + "libc", + "log", + "memmap2 0.9.5", + "rustix", + "thiserror", + "wayland-backend", + "wayland-client 0.31.7", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols 0.31.2", + "wayland-protocols-wlr 0.2.0", + "wayland-scanner 0.31.5", + "xkeysym", +] + [[package]] name = "smithay-client-toolkit" version = "0.19.2" @@ -8854,35 +8755,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ "bitflags 2.6.0", - "bytemuck", - "calloop", - "calloop-wayland-source", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", "cursor-icon", "libc", "log", - "memmap2 0.9.4", - "pkg-config", - "rustix 0.38.32", + "memmap2 0.9.5", + "rustix", "thiserror", "wayland-backend", "wayland-client 0.31.7", "wayland-csd-frame", "wayland-cursor", "wayland-protocols 0.32.5", - "wayland-protocols-wlr", + "wayland-protocols-wlr 0.3.5", "wayland-scanner 0.31.5", - "xkbcommon", "xkeysym", ] [[package]] name = "smithay-clipboard" -version = "0.8.0" -source = "git+https://github.com/pop-os/smithay-clipboard?tag=pop-dnd-5#5a3007def49eb678d1144850c9ee04b80707c56a" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" dependencies = [ "libc", - "raw-window-handle", - "smithay-client-toolkit", + "smithay-client-toolkit 0.19.2", "wayland-backend", ] @@ -8895,16 +8793,6 @@ dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.6" @@ -8917,31 +8805,33 @@ dependencies = [ [[package]] name = "softbuffer" -version = "0.4.1" -source = "git+https://github.com/pop-os/softbuffer?tag=cosmic-4.0#6e75b1ad7e98397d37cb187886d05969bc480995" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "as-raw-xcb-connection", "bytemuck", "cfg_aliases 0.2.1", - "cocoa 0.25.0", - "core-graphics 0.23.2", + "core-graphics 0.24.0", "drm", - "fastrand 2.0.2", + "fastrand", "foreign-types 0.5.0", "js-sys", "log", - "memmap2 0.9.4", - "objc", + "memmap2 0.9.5", + "objc2", + "objc2-foundation", + "objc2-quartz-core", "raw-window-handle", - "redox_syscall 0.4.1", - "rustix 0.38.32", + "redox_syscall 0.5.7", + "rustix", "tiny-xlib", "wasm-bindgen", "wayland-backend", "wayland-client 0.31.7", "wayland-sys 0.31.5", "web-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", "x11rb 0.13.1", ] @@ -9035,7 +8925,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" dependencies = [ - "ahash", + "ahash 0.8.11", "atoi", "byteorder", "bytes", @@ -9076,7 +8966,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "sqlx-core", "sqlx-macros-core", "syn 1.0.109", @@ -9094,7 +8984,7 @@ dependencies = [ "hex", "once_cell", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "serde", "serde_json", "sha2", @@ -9279,7 +9169,7 @@ dependencies = [ "phf_generator 0.10.0", "phf_shared 0.10.0", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", ] [[package]] @@ -9290,7 +9180,7 @@ checksum = "8fa4d4f81d7c05b9161f8de839975d3326328b8ba2831164b465524cc2f55252" dependencies = [ "pmutil", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "swc_macros_common", "syn 2.0.89", ] @@ -9329,7 +9219,7 @@ checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ "heck 0.4.1", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "rustversion", "syn 2.0.89", ] @@ -9428,7 +9318,7 @@ checksum = "e5b5aaca9a0082be4515f0fbbecc191bf5829cd25b5b9c0a2810f6a2bb0d6829" dependencies = [ "pmutil", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "swc_macros_common", "syn 2.0.89", ] @@ -9477,7 +9367,7 @@ checksum = "dcdff076dccca6cc6a0e0b2a2c8acfb066014382bc6df98ec99e755484814384" dependencies = [ "pmutil", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "swc_macros_common", "syn 2.0.89", ] @@ -9560,7 +9450,7 @@ checksum = "f59c4b6ed5d78d3ad9fc7c6f8ab4f85bba99573d31d9a2c0a712077a6b45efd2" dependencies = [ "pmutil", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "swc_macros_common", "syn 2.0.89", ] @@ -9665,7 +9555,7 @@ checksum = "05a95d367e228d52484c53336991fdcf47b6b553ef835d9159db4ba40efb0ee8" dependencies = [ "pmutil", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -9677,7 +9567,7 @@ checksum = "7a273205ccb09b51fabe88c49f3b34c5a4631c4c00a16ae20e03111d6a42e832" dependencies = [ "pmutil", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -9700,7 +9590,7 @@ dependencies = [ "Inflector", "pmutil", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "swc_macros_common", "syn 2.0.89", ] @@ -9723,7 +9613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "unicode-ident", ] @@ -9734,7 +9624,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "unicode-ident", ] @@ -9751,7 +9641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", "unicode-xid 0.2.6", ] @@ -9824,7 +9714,7 @@ dependencies = [ "lru", "lz4_flex", "measure_time", - "memmap2 0.9.4", + "memmap2 0.9.5", "num_cpus", "once_cell", "oneshot", @@ -9948,14 +9838,15 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tempfile" -version = "3.10.1" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", - "fastrand 2.0.2", - "rustix 0.38.32", - "windows-sys 0.52.0", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", ] [[package]] @@ -9992,7 +9883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -10138,9 +10029,8 @@ dependencies = [ "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.6", + "socket2", "tokio-macros", - "tracing", "windows-sys 0.48.0", ] @@ -10161,7 +10051,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -10335,7 +10225,7 @@ dependencies = [ "prettyplease 0.2.19", "proc-macro2 1.0.92", "prost-build 0.12.4", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -10390,7 +10280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -10958,15 +10848,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", ] -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - [[package]] name = "walkdir" version = "2.5.0" @@ -11031,7 +10915,7 @@ dependencies = [ "log", "once_cell", "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", "wasm-bindgen-shared", ] @@ -11054,7 +10938,7 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ - "quote 1.0.36", + "quote 1.0.37", "wasm-bindgen-macro-support", ] @@ -11065,7 +10949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", "wasm-bindgen-backend", "wasm-bindgen-shared", @@ -11105,6 +10989,21 @@ dependencies = [ "web-sys", ] +[[package]] +name = "waycrate_xkbkeycode" +version = "0.13.99" +source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#bc47b24351a4a0946b03a1dec7e586ddde408b7d" +dependencies = [ + "bitflags 2.6.0", + "calloop 0.14.1", + "memmap2 0.9.5", + "smol_str", + "tracing", + "wayland-backend", + "wayland-client 0.31.7", + "xkbcommon-dl", +] + [[package]] name = "wayland-backend" version = "0.3.7" @@ -11113,7 +11012,7 @@ checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.32", + "rustix", "scoped-tls", "smallvec", "wayland-sys 0.31.5", @@ -11141,7 +11040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" dependencies = [ "bitflags 2.6.0", - "rustix 0.38.32", + "rustix", "wayland-backend", "wayland-scanner 0.31.5", ] @@ -11171,11 +11070,11 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.31.1" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ce5fa868dd13d11a0d04c5e2e65726d0897be8de247c0c5a65886e283231ba" +checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" dependencies = [ - "rustix 0.38.32", + "rustix", "wayland-client 0.31.7", "xcursor", ] @@ -11192,6 +11091,18 @@ dependencies = [ "wayland-scanner 0.29.5", ] +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client 0.31.7", + "wayland-scanner 0.31.5", +] + [[package]] name = "wayland-protocols" version = "0.32.5" @@ -11202,14 +11113,13 @@ dependencies = [ "wayland-backend", "wayland-client 0.31.7", "wayland-scanner 0.31.5", - "wayland-server", ] [[package]] -name = "wayland-protocols-plasma" +name = "wayland-protocols-misc" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" +checksum = "da2e42969764e469a115d4bb1c16e9588ef8b75b127ba7a2c9ddf1e140b25ca7" dependencies = [ "bitflags 2.6.0", "wayland-backend", @@ -11218,6 +11128,32 @@ dependencies = [ "wayland-scanner 0.31.5", ] +[[package]] +name = "wayland-protocols-plasma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client 0.31.7", + "wayland-protocols 0.31.2", + "wayland-scanner 0.31.5", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client 0.31.7", + "wayland-protocols 0.31.2", + "wayland-scanner 0.31.5", +] + [[package]] name = "wayland-protocols-wlr" version = "0.3.5" @@ -11229,7 +11165,6 @@ dependencies = [ "wayland-client 0.31.7", "wayland-protocols 0.32.5", "wayland-scanner 0.31.5", - "wayland-server", ] [[package]] @@ -11239,7 +11174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "xml-rs", ] @@ -11251,21 +11186,7 @@ checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ "proc-macro2 1.0.92", "quick-xml 0.36.2", - "quote 1.0.36", -] - -[[package]] -name = "wayland-server" -version = "0.31.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89532cc712a2adb119eb4d09694b402576052254d0bb284f82ac1c47fb786ad" -dependencies = [ - "bitflags 2.6.0", - "downcast-rs", - "io-lifetimes 2.0.4", - "rustix 0.38.32", - "wayland-backend", - "wayland-scanner 0.31.5", + "quote 1.0.37", ] [[package]] @@ -11445,7 +11366,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.32", + "rustix", ] [[package]] @@ -11508,14 +11429,13 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "window_clipboard" version = "0.4.1" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13#a83bf83784276aaa882ef13555295a2ad9edd265" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d692d46038c433f9daee7ad8757e002a4248c20b0a3fbc991d99521d3bcb6d" dependencies = [ "clipboard-win 5.3.1", "clipboard_macos", "clipboard_wayland", "clipboard_x11", - "dnd", - "mime 0.1.0", "raw-window-handle", "thiserror", ] @@ -11529,18 +11449,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-implement 0.53.0", - "windows-interface 0.53.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.58.0" @@ -11560,40 +11468,19 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", + "windows-implement", + "windows-interface", + "windows-result", "windows-strings", "windows-targets 0.52.6", ] -[[package]] -name = "windows-implement" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" -dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.36", - "syn 2.0.89", -] - [[package]] name = "windows-implement" version = "0.58.0" @@ -11601,18 +11488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", - "syn 2.0.89", -] - -[[package]] -name = "windows-interface" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" -dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -11623,19 +11499,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.2.0" @@ -11651,7 +11518,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result 0.2.0", + "windows-result", "windows-targets 0.52.6", ] @@ -11871,25 +11738,25 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" -version = "0.30.5" -source = "git+https://github.com/pop-os/winit.git?tag=iced-xdg-surface-0.13#1cc02bdab141072eaabad639d74b032fd0fcc62e" +version = "0.30.1" +source = "git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d#254d6b3420ce4e674f516f7a2bd440665e05484d" dependencies = [ - "ahash", + "ahash 0.8.11", "android-activity", "atomic-waker", "bitflags 2.6.0", "block2", "bytemuck", - "calloop", + "calloop 0.12.4", "cfg_aliases 0.2.1", "concurrent-queue", "core-foundation 0.9.4", "core-graphics 0.23.2", "cursor-icon", - "dpi 0.1.1 (git+https://github.com/pop-os/winit.git?tag=iced-xdg-surface-0.13)", + "dpi 0.1.1 (git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d)", "js-sys", "libc", - "memmap2 0.9.4", + "memmap2 0.9.5", "ndk", "objc2", "objc2-app-kit", @@ -11900,9 +11767,9 @@ dependencies = [ "pin-project", "raw-window-handle", "redox_syscall 0.4.1", - "rustix 0.38.32", + "rustix", "sctk-adwaita", - "smithay-client-toolkit", + "smithay-client-toolkit 0.18.1", "smol_str", "tracing", "unicode-segmentation", @@ -11910,7 +11777,7 @@ dependencies = [ "wasm-bindgen-futures", "wayland-backend", "wayland-client 0.31.7", - "wayland-protocols 0.32.5", + "wayland-protocols 0.31.2", "wayland-protocols-plasma", "web-sys", "web-time", @@ -12019,7 +11886,7 @@ dependencies = [ "libc", "libloading 0.8.3", "once_cell", - "rustix 0.38.32", + "rustix", "x11rb-protocol 0.13.1", ] @@ -12089,17 +11956,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "xkbcommon" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e" -dependencies = [ - "libc", - "memmap2 0.8.0", - "xkeysym", -] - [[package]] name = "xkbcommon-dl" version = "0.4.2" @@ -12118,9 +11974,6 @@ name = "xkeysym" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" -dependencies = [ - "bytemuck", -] [[package]] name = "xml-rs" @@ -12148,34 +12001,36 @@ checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" [[package]] name = "zbus" -version = "3.15.2" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" dependencies = [ "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", "async-process", "async-recursion", + "async-task", "async-trait", - "byteorder", - "derivative", + "blocking", "enumflags2", - "event-listener 2.5.3", + "event-listener 5.3.1", "futures-core", "futures-sink", "futures-util", "hex", - "nix 0.26.2", - "once_cell", + "nix 0.29.0", "ordered-stream", "rand", "serde", "serde_repr", "sha1", "static_assertions", - "tokio", "tracing", "uds_windows", - "winapi", + "windows-sys 0.52.0", "xdg-home", "zbus_macros", "zbus_names", @@ -12184,23 +12039,22 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.15.2" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.1.0", "proc-macro2 1.0.92", - "quote 1.0.36", - "regex", - "syn 1.0.109", + "quote 1.0.37", + "syn 2.0.89", "zvariant_utils", ] [[package]] name = "zbus_names" -version = "2.6.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", @@ -12229,7 +12083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -12249,7 +12103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", + "quote 1.0.37", "syn 2.0.89", ] @@ -12307,13 +12161,12 @@ dependencies = [ [[package]] name = "zvariant" -version = "3.15.2" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" dependencies = [ - "byteorder", + "endi", "enumflags2", - "libc", "serde", "static_assertions", "zvariant_derive", @@ -12321,24 +12174,24 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "3.15.2" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.1.0", "proc-macro2 1.0.92", - "quote 1.0.36", - "syn 1.0.109", + "quote 1.0.37", + "syn 2.0.89", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "1.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2 1.0.92", - "quote 1.0.36", - "syn 1.0.109", + "quote 1.0.37", + "syn 2.0.89", ] diff --git a/Cargo.toml b/Cargo.toml index 4a03afb..e1f430d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,14 +16,16 @@ members = [ "rust/scenario_runner", ] [workspace.dependencies] -#iced = { version = "0.13.99", features = ["tokio", "lazy", "advanced", "image", "winit"] } -iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", features = ["tokio", "lazy", "advanced", "image", "winit"] } +#iced = { version = "0.13.99", features = ["tokio", "lazy", "advanced", "image"] } +iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", features = ["tokio", "lazy", "advanced", "image"] } #iced_aw = { version = "0.11.99", features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } iced_aw = { git = "https://github.com/project-gauntlet/iced_aw.git", branch = "gauntlet-0.13", default-features = false, features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } -#iced_table = "0.13.0" +#iced_table = "0.13.99" iced_table = { git = "https://github.com/project-gauntlet/iced_table.git", branch = "gauntlet-0.13" } -#iced_fonts = { version = "0.1.1", features = ["bootstrap"] } +#iced_fonts = { version = "0.1.99", features = ["bootstrap"] } iced_fonts = { git = "https://github.com/project-gauntlet/iced_fonts.git", branch = "gauntlet-0.13", features = ["bootstrap"] } +#iced_layershell = "0.13.99" +iced_layershell = { git = "https://github.com/project-gauntlet/exwlshelleventloop.git", branch = "gauntlet-0.13" } [dependencies] cli = { path = "rust/cli" } diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index b22e04c..3e33a88 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" tokio = "1.28.1" anyhow = { version = "1", features = ["backtrace"] } thiserror = "1" +iced.workspace = true iced_aw.workspace = true iced_fonts.workspace = true tracing = "0.1" @@ -27,11 +28,7 @@ global-hotkey = "0.4.2" tray-icon = { version = "0.15.1", default-features = false } [target.'cfg(target_os = "linux")'.dependencies] -iced.workspace = true -iced.features = ["wayland"] - -[target.'cfg(not(target_os = "linux"))'.dependencies] -iced.workspace = true +iced_layershell.workspace = true [build-dependencies] component_model = { path = "../component_model" } diff --git a/rust/client/src/ui/hud/mod.rs b/rust/client/src/ui/hud/mod.rs index 4266f2f..91ae433 100644 --- a/rust/client/src/ui/hud/mod.rs +++ b/rust/client/src/ui/hud/mod.rs @@ -1,6 +1,6 @@ -use crate::ui::AppMsg; +use crate::ui::{layer_shell, AppMsg}; use iced::window::{Level, Position, Settings}; -use iced::{window, Limits, Size, Task}; +use iced::{window, Size, Task}; use std::convert; use std::time::Duration; @@ -50,9 +50,10 @@ fn open_non_wayland() -> Task { #[cfg(target_os = "linux")] fn open_wayland() -> Task { let id = window::Id::unique(); + let settings = layer_shell_settings(); - iced::platform_specific::shell::commands::layer_surface::get_layer_surface(layer_shell_settings(id)) - .then(|id| in_2_seconds(AppMsg::CloseHudWindow { id })) + Task::done(AppMsg::LayerShell(layer_shell::LayerShellAppMsg::NewLayerShell { id, settings })) + .then(move |_| in_2_seconds(AppMsg::CloseHudWindow { id })) } pub fn close_hud_window( @@ -62,7 +63,7 @@ pub fn close_hud_window( ) -> Task { #[cfg(target_os = "linux")] if wayland { - iced::platform_specific::shell::commands::layer_surface::destroy_layer_surface(id) + Task::done(AppMsg::LayerShell(layer_shell::LayerShellAppMsg::RemoveWindow(id))) } else { window::close(id) } @@ -72,19 +73,16 @@ pub fn close_hud_window( } #[cfg(target_os = "linux")] -fn layer_shell_settings(id: window::Id) -> iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings { - iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings { - id, - layer: iced::platform_specific::shell::commands::layer_surface::Layer::Overlay, - keyboard_interactivity: iced::platform_specific::shell::commands::layer_surface::KeyboardInteractivity::None, - pointer_interactivity: false, - anchor: iced::platform_specific::shell::commands::layer_surface::Anchor::empty(), - output: Default::default(), - namespace: "Gauntlet HUD".to_string(), +fn layer_shell_settings() -> iced_layershell::reexport::NewLayerShellSettings { + iced_layershell::reexport::NewLayerShellSettings { + layer: iced_layershell::reexport::Layer::Overlay, + keyboard_interactivity: iced_layershell::reexport::KeyboardInteractivity::None, + use_last_output: false, + events_transparent: true, + anchor: iced_layershell::reexport::Anchor::empty(), margin: Default::default(), - exclusive_zone: 0, - size: Some((Some(HUD_WINDOW_WIDTH as u32), Some(HUD_WINDOW_HEIGHT as u32))), - size_limits: Limits::new(Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT), Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT)), + exclusive_zone: Some(0), + size: Some((HUD_WINDOW_WIDTH as u32, HUD_WINDOW_HEIGHT as u32)), } } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index b20a1c3..36028d0 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -83,6 +83,12 @@ pub struct AppModel { hud_display: Option } +#[cfg(target_os = "linux")] +mod layer_shell { + #[iced_layershell::to_layer_message(multi)] + #[derive(Debug, Clone)] + pub enum LayerShellAppMsg {} +} #[derive(Debug, Clone)] pub enum AppMsg { @@ -189,6 +195,19 @@ pub enum AppMsg { FocusPluginViewSearchBar { widget_id: UiWidgetId }, + #[cfg(target_os = "linux")] + LayerShell(layer_shell::LayerShellAppMsg) +} + +#[cfg(target_os = "linux")] +impl TryInto for AppMsg { + type Error = Self; + fn try_into(self) -> Result { + match self { + Self::LayerShell(msg) => msg.try_into().map_err(|msg| Self::LayerShell(msg)), + _ => Err(self) + } + } } const WINDOW_WIDTH: f32 = 750.0; @@ -214,19 +233,16 @@ fn window_settings() -> window::Settings { #[cfg(target_os = "linux")] -fn layer_shell_settings(main_window_id: window::Id) -> iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings { - iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings { - id: main_window_id, - layer: iced::platform_specific::shell::commands::layer_surface::Layer::Overlay, - keyboard_interactivity: iced::platform_specific::shell::commands::layer_surface::KeyboardInteractivity::Exclusive, - pointer_interactivity: true, - anchor: iced::platform_specific::shell::commands::layer_surface::Anchor::empty(), - output: Default::default(), - namespace: "Gauntlet".to_string(), +fn layer_shell_settings() -> iced_layershell::reexport::NewLayerShellSettings { + iced_layershell::reexport::NewLayerShellSettings { + layer: iced_layershell::reexport::Layer::Overlay, + keyboard_interactivity: iced_layershell::reexport::KeyboardInteractivity::Exclusive, + events_transparent: false, + anchor: iced_layershell::reexport::Anchor::empty(), margin: Default::default(), - exclusive_zone: 0, - size: Some((Some(WINDOW_WIDTH as u32), Some(WINDOW_HEIGHT as u32))), - size_limits: Limits::new(Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), Size::new(WINDOW_WIDTH, WINDOW_HEIGHT)), + exclusive_zone: Some(0), + size: Some((WINDOW_WIDTH as u32, WINDOW_HEIGHT as u32)), + use_last_output: false, } } @@ -252,9 +268,10 @@ fn open_main_window_non_wayland() -> (window::Id, Task) { #[cfg(target_os = "linux")] fn open_main_window_wayland() -> (window::Id, Task) { - let main_window_id = window::Id::unique(); + let id = window::Id::unique(); + let settings = layer_shell_settings(); - (main_window_id, iced::platform_specific::shell::commands::layer_surface::get_layer_surface(layer_shell_settings(main_window_id))) + (id, Task::done(AppMsg::LayerShell(layer_shell::LayerShellAppMsg::NewLayerShell { id, settings }))) } @@ -263,15 +280,33 @@ pub fn run( frontend_receiver: RequestReceiver, backend_sender: RequestSender, ) { + let theme = GauntletComplexTheme::new(); + #[cfg(target_os = "linux")] - let wayland = std::env::var("WAYLAND_DISPLAY") - .or_else(|_| std::env::var("WAYLAND_SOCKET")) - .is_ok(); + let result = { + let wayland = std::env::var("WAYLAND_DISPLAY") + .or_else(|_| std::env::var("WAYLAND_SOCKET")) + .is_ok(); + + if wayland { + run_wayland(minimized, frontend_receiver, backend_sender, theme) + } else { + run_non_wayland(minimized, frontend_receiver, backend_sender, theme) + } + }; #[cfg(not(target_os = "linux"))] - let wayland = false; + let result = run_non_wayland(minimized, frontend_receiver, backend_sender, theme); - let theme = GauntletComplexTheme::new(); + result.expect("Unable to start application") +} + +fn run_non_wayland( + minimized: bool, + frontend_receiver: RequestReceiver, + backend_sender: RequestSender, + theme: GauntletComplexTheme, +) -> anyhow::Result<()> { iced::daemon::(title, update, view) .subscription(subscription) @@ -279,8 +314,38 @@ pub fn run( let theme = theme.clone(); move |_, _| theme.clone() }) - .run_with(move || new(frontend_receiver, backend_sender, wayland, minimized)) - .expect("Unable to start application") + .run_with(move || new(frontend_receiver, backend_sender, false, minimized))?; + + Ok(()) +} + +#[cfg(target_os = "linux")] +fn run_wayland( + minimized: bool, + frontend_receiver: RequestReceiver, + backend_sender: RequestSender, + theme: GauntletComplexTheme, +) -> anyhow::Result<()> { + iced_layershell::build_pattern::daemon("Gauntlet", update, view, wayland_remove_id_info) + .layer_settings(iced_layershell::settings::LayerShellSettings { + start_mode: iced_layershell::settings::StartMode::Background, + events_transparent: true, + keyboard_interactivity: iced_layershell::reexport::KeyboardInteractivity::None, + size: Some((0, 0)), + ..Default::default() + }) + .subscription(subscription) + .theme({ + let theme = theme.clone(); + move |_| theme.clone() + }) + .run_with(move || new(frontend_receiver, backend_sender, true, minimized))?; + + Ok(()) +} + +#[cfg(target_os = "linux")] +fn wayland_remove_id_info(_state: &mut AppModel, _id: window::Id) { } fn new( @@ -753,25 +818,18 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } AppMsg::IcedEvent(Event::Window(window::Event::Focused)) => { - state.on_focused() + if state.wayland { + Task::none() + } else { + state.on_focused() + } } AppMsg::IcedEvent(Event::Window(window::Event::Unfocused)) => { - state.on_unfocused() - } - #[cfg(target_os = "linux")] - AppMsg::IcedEvent( - Event::PlatformSpecific( - iced::core::event::PlatformSpecific::Wayland( - iced::core::event::wayland::Event::Layer( - iced::event::wayland::LayerEvent::Unfocused, - _, - _ - ) - ) - ) - ) => { - // wayland layer shell doesn't have the same unfocused problem as the other platforms - state.hide_window() + if state.wayland { + state.hide_window() + } else { + state.on_unfocused() + } } AppMsg::IcedEvent(_) => Task::none(), AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::Noop, .. } => Task::none(), @@ -1222,6 +1280,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { client_context.focus_search_bar(widget_id) } + AppMsg::LayerShell(_) => { + // handled by library + Task::none() + } } } @@ -1800,7 +1862,7 @@ impl AppModel { #[cfg(target_os = "linux")] if self.wayland { commands.push( - iced::platform_specific::shell::commands::layer_surface::destroy_layer_surface(main_window_id), + Task::done(AppMsg::LayerShell(layer_shell::LayerShellAppMsg::RemoveWindow(main_window_id))) ); } else { commands.push( diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs index 36cd117..5e552ea 100644 --- a/rust/client/src/ui/scroll_handle.rs +++ b/rust/client/src/ui/scroll_handle.rs @@ -1,7 +1,6 @@ use std::marker::PhantomData; -use iced::id::Id; use iced::Task; -use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; +use iced::widget::scrollable::{scroll_to, AbsoluteOffset, Id}; use crate::ui::AppMsg; pub const ESTIMATED_MAIN_LIST_ITEM_HEIGHT: f32 = 38.8; diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index edeb67e..e1ea86c 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -93,7 +93,6 @@ impl container::Catalog for GauntletComplexTheme { let background_color = &panel_theme.background_color; Style { - icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -111,7 +110,6 @@ impl container::Catalog for GauntletComplexTheme { let border_color = &theme.border_color; Style { - icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -128,7 +126,6 @@ impl container::Catalog for GauntletComplexTheme { let border_color = &theme.border_color; Style { - icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -144,7 +141,6 @@ impl container::Catalog for GauntletComplexTheme { let background_color = &theme.background_color; Style { - icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -160,7 +156,6 @@ impl container::Catalog for GauntletComplexTheme { let background_color = &theme.background_color; Style { - icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -177,7 +172,6 @@ impl container::Catalog for GauntletComplexTheme { let background_color = &tooltip_theme.background_color; Style { - icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { @@ -234,7 +228,6 @@ impl container::Catalog for GauntletComplexTheme { let background_color = &theme.background_color; Style { - icon_color: None, text_color: None, background: Some(background_color.to_iced().into()), border: Border { diff --git a/rust/client/src/ui/theme/image.rs b/rust/client/src/ui/theme/image.rs index 6cfdf2b..c83a99b 100644 --- a/rust/client/src/ui/theme/image.rs +++ b/rust/client/src/ui/theme/image.rs @@ -6,7 +6,7 @@ pub enum ImageStyle { MainListItemIcon, } -impl<'a, Message: 'a> ThemableWidget<'a, Message> for Image<'a, iced::advanced::image::Handle> { +impl<'a, Message: 'a> ThemableWidget<'a, Message> for Image { type Kind = ImageStyle; fn themed(self, kind: ImageStyle) -> Element<'a, Message> { diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 0cdeb13..474ecf8 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -1003,7 +1003,18 @@ impl DefaultStyle for GauntletComplexTheme { application::Appearance { background_color: Color::TRANSPARENT, text_color: theme.text.to_iced(), - icon_color: theme.text.to_iced(), + } + } +} + +#[cfg(target_os = "linux")] +impl iced_layershell::DefaultStyle for GauntletComplexTheme { + fn default_style(&self) -> iced_layershell::Appearance { + let theme = get_theme(); + + iced_layershell::Appearance { + background_color: Color::TRANSPARENT, + text_color: theme.text.to_iced(), } } } diff --git a/rust/management_client/src/components/shortcut_selector.rs b/rust/management_client/src/components/shortcut_selector.rs index 7cc8662..bfe118f 100644 --- a/rust/management_client/src/components/shortcut_selector.rs +++ b/rust/management_client/src/components/shortcut_selector.rs @@ -158,9 +158,7 @@ where renderer, theme, &renderer::Style { - icon_color: renderer_style.icon_color, text_color: renderer_style.text_color, - scale_factor: renderer_style.scale_factor }, layout.children().next().unwrap(), cursor, @@ -180,8 +178,8 @@ where self.content.as_widget().children() } - fn diff(&mut self, tree: &mut Tree) { - self.content.as_widget_mut().diff(tree); + fn diff(&self, tree: &mut Tree) { + self.content.as_widget().diff(tree); } fn on_event( diff --git a/rust/management_client/src/theme.rs b/rust/management_client/src/theme.rs index f10fc19..e1e81c3 100644 --- a/rust/management_client/src/theme.rs +++ b/rust/management_client/src/theme.rs @@ -22,7 +22,6 @@ impl DefaultStyle for GauntletSettingsTheme { Appearance { background_color: BACKGROUND_DARKEST.to_iced(), text_color: TEXT_LIGHTEST.to_iced(), - icon_color: TEXT_LIGHTEST.to_iced(), } } } diff --git a/rust/management_client/src/views/plugins/table.rs b/rust/management_client/src/views/plugins/table.rs index 98cf211..a524e7c 100644 --- a/rust/management_client/src/views/plugins/table.rs +++ b/rust/management_client/src/views/plugins/table.rs @@ -3,8 +3,8 @@ use std::rc::Rc; use iced::{Alignment, Length, Renderer, Task}; use iced::advanced::text::Shaping; -use iced::id::Id; use iced::widget::{button, checkbox, container, horizontal_space, row, scrollable, Space, text, value}; +use iced::widget::scrollable::Id; use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; use iced_table::table; From 771d837eb0be74644ad66bc1980ff96a95fe5e96 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 8 Dec 2024 22:01:49 +0100 Subject: [PATCH 200/540] Use NSPanel with Accessory activation_policy on MacOS for better window behavior --- Cargo.lock | 28 ++++++++++++++-------------- rust/client/src/ui/hud/mod.rs | 18 ++++++++---------- rust/client/src/ui/mod.rs | 25 ++++++++++++++++--------- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e14e26a..d3e9d07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2798,7 +2798,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "dpi" version = "0.1.1" -source = "git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d#254d6b3420ce4e674f516f7a2bd440665e05484d" +source = "git+https://github.com/project-gauntlet/winit.git?rev=0f1b21e4c12ce0b5b3b8f2c8598f8163c76eff10#0f1b21e4c12ce0b5b3b8f2c8598f8163c76eff10" [[package]] name = "dprint-swc-ext" @@ -4451,7 +4451,7 @@ dependencies = [ [[package]] name = "iced" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" dependencies = [ "iced_core", "iced_futures", @@ -4479,7 +4479,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" dependencies = [ "bitflags 2.6.0", "bytes", @@ -4506,7 +4506,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" dependencies = [ "futures", "iced_core", @@ -4520,7 +4520,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -4572,7 +4572,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -4584,7 +4584,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" dependencies = [ "bytes", "iced_core", @@ -4605,7 +4605,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" dependencies = [ "bytemuck", "cosmic-text", @@ -4620,7 +4620,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -4639,7 +4639,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" dependencies = [ "iced_renderer", "iced_runtime", @@ -4654,7 +4654,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#247d30fb12e544b683d804b3734a877fce264560" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" dependencies = [ "iced_futures", "iced_graphics", @@ -11738,8 +11738,8 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" -version = "0.30.1" -source = "git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d#254d6b3420ce4e674f516f7a2bd440665e05484d" +version = "0.30.99" +source = "git+https://github.com/project-gauntlet/winit.git?rev=0f1b21e4c12ce0b5b3b8f2c8598f8163c76eff10#0f1b21e4c12ce0b5b3b8f2c8598f8163c76eff10" dependencies = [ "ahash 0.8.11", "android-activity", @@ -11753,7 +11753,7 @@ dependencies = [ "core-foundation 0.9.4", "core-graphics 0.23.2", "cursor-icon", - "dpi 0.1.1 (git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d)", + "dpi 0.1.1 (git+https://github.com/project-gauntlet/winit.git?rev=0f1b21e4c12ce0b5b3b8f2c8598f8163c76eff10)", "js-sys", "libc", "memmap2 0.9.5", diff --git a/rust/client/src/ui/hud/mod.rs b/rust/client/src/ui/hud/mod.rs index 91ae433..a3006aa 100644 --- a/rust/client/src/ui/hud/mod.rs +++ b/rust/client/src/ui/hud/mod.rs @@ -1,8 +1,8 @@ -use crate::ui::{layer_shell, AppMsg}; use iced::window::{Level, Position, Settings}; use iced::{window, Size, Task}; use std::convert; use std::time::Duration; +use crate::ui::AppMsg; const HUD_WINDOW_WIDTH: f32 = 400.0; const HUD_WINDOW_HEIGHT: f32 = 40.0; @@ -31,13 +31,11 @@ fn open_non_wayland() -> Task { transparent: true, visible: true, level: Level::AlwaysOnTop, - // TODO macos - // #[cfg(target_os = "macos")] - // platform_specific: iced::window::settings::PlatformSpecific { - // activation_policy: window::settings::ActivationPolicy::Accessory, - // activate_ignoring_other_apps: false, - // ..Default::default() - // }, + #[cfg(target_os = "macos")] + platform_specific: window::settings::PlatformSpecific { + window_kind: window::settings::WindowKind::Popup, + ..Default::default() + }, exit_on_close_request: false, ..Default::default() }; @@ -52,7 +50,7 @@ fn open_wayland() -> Task { let id = window::Id::unique(); let settings = layer_shell_settings(); - Task::done(AppMsg::LayerShell(layer_shell::LayerShellAppMsg::NewLayerShell { id, settings })) + Task::done(AppMsg::LayerShell(crate::ui::layer_shell::LayerShellAppMsg::NewLayerShell { id, settings })) .then(move |_| in_2_seconds(AppMsg::CloseHudWindow { id })) } @@ -63,7 +61,7 @@ pub fn close_hud_window( ) -> Task { #[cfg(target_os = "linux")] if wayland { - Task::done(AppMsg::LayerShell(layer_shell::LayerShellAppMsg::RemoveWindow(id))) + Task::done(AppMsg::LayerShell(crate::ui::layer_shell::LayerShellAppMsg::RemoveWindow(id))) } else { window::close(id) } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 36028d0..2a8eecd 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -11,7 +11,6 @@ use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; use iced::widget::text::Shaping; use iced::widget::text_input::focus; use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, Space}; -use iced::window::settings::PlatformSpecific; use iced::window::{Level, Position, Screenshot}; use iced::{event, executor, font, futures, keyboard, stream, window, Alignment, Event, Font, Length, Padding, Pixels, Renderer, Settings, Size, Subscription, Task}; use std::collections::HashMap; @@ -220,13 +219,11 @@ fn window_settings() -> window::Settings { resizable: false, decorations: false, transparent: true, - // todo macoss - // #[cfg(target_os = "macos")] - // platform_specific: PlatformSpecific { - // activation_policy: window::settings::ActivationPolicy::Accessory, - // activate_ignoring_other_apps: false, - // ..Default::default() - // }, + #[cfg(target_os = "macos")] + platform_specific: window::settings::PlatformSpecific { + window_kind: window::settings::WindowKind::Popup, + ..Default::default() + }, ..Default::default() } } @@ -309,6 +306,14 @@ fn run_non_wayland( ) -> anyhow::Result<()> { iced::daemon::(title, update, view) + .settings(Settings { + #[cfg(target_os = "macos")] + platform_specific: iced::settings::PlatformSpecific { + activation_policy: iced::settings::ActivationPolicy::Accessory, + activate_ignoring_other_apps: true, + }, + ..Default::default() + }) .subscription(subscription) .theme({ let theme = theme.clone(); @@ -1280,6 +1285,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { client_context.focus_search_bar(widget_id) } + #[cfg(target_os = "linux")] AppMsg::LayerShell(_) => { // handled by library Task::none() @@ -1845,7 +1851,6 @@ impl AppModel { fn on_unfocused(&mut self) -> Task { // for some reason (on both macOS and linux x11) duplicate Unfocused fires right before Focus event if self.focused { - self.focused = false; self.hide_window() } else { Task::none() @@ -1857,6 +1862,8 @@ impl AppModel { return Task::none() }; + self.focused = false; + let mut commands = vec![]; #[cfg(target_os = "linux")] From 82ef93a626334a049abac157419dc3f8b32b10b6 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 10 Dec 2024 18:56:22 +0100 Subject: [PATCH 201/540] Fix occasional deadlock when spamming keys in UI --- rust/client/src/ui/mod.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 2a8eecd..97c1acb 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -2138,8 +2138,6 @@ async fn request_loop( let (request_data, responder) = frontend_receiver.recv().await; let app_msgs = { - let mut client_context = client_context.write().expect("lock is poisoned"); - match request_data { UiRequestData::ReplaceView { plugin_id, @@ -2155,15 +2153,19 @@ async fn request_loop( } => { let has_children = container.content.is_some(); - let message = client_context.replace_view( - render_location, - container, - images, - &plugin_id, - &plugin_name, - &entrypoint_id, - &entrypoint_name - ); + let message = { + let mut client_context = client_context.write().expect("lock is poisoned"); + + client_context.replace_view( + render_location, + container, + images, + &plugin_id, + &plugin_name, + &entrypoint_id, + &entrypoint_name + ) + }; responder.respond(UiResponseData::Nothing); @@ -2177,7 +2179,11 @@ async fn request_loop( ] } UiRequestData::ClearInlineView { plugin_id } => { - client_context.clear_inline_view(&plugin_id); + { + let mut client_context = client_context.write().expect("lock is poisoned"); + + client_context.clear_inline_view(&plugin_id); + } responder.respond(UiResponseData::Nothing); From 8150bfd03f9de373a184bef89e4dbf43346ef7fa Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:37:23 +0100 Subject: [PATCH 202/540] Slightly refactor plugin action messages to better group result messages --- rust/client/src/ui/mod.rs | 179 ++++++++++++++++---------------- rust/client/src/ui/state/mod.rs | 14 +-- 2 files changed, 96 insertions(+), 97 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 97c1acb..85b777f 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -106,6 +106,12 @@ pub enum AppMsg { entrypoint_id: EntrypointId, action_index: Option }, + RunSearchItemAction(SearchResult, Option), + RunPluginAction { + render_location: UiRenderLocation, + plugin_id: PluginId, + widget_id: UiWidgetId + }, PromptChanged(String), PromptSubmit, UpdateSearchResults, @@ -146,7 +152,6 @@ pub enum AppMsg { entrypoint_id: EntrypointId, render_location: UiRenderLocation }, - RunSearchItemAction(SearchResult, Option), Screenshot { save_path: String }, @@ -168,15 +173,14 @@ pub enum AppMsg { }, OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus, OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result: SearchResult }, - OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result: SearchResult, widget_id: UiWidgetId }, - OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id: UiWidgetId }, - OnPrimaryActionPluginViewNoPanelKeyboardWithFocus { widget_id: UiWidgetId }, - OnPrimaryActionPluginViewAnyPanelKeyboardWithFocus { widget_id: UiWidgetId }, - OnAnyActionPluginViewAnyPanel { widget_id: UiWidgetId }, OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result: SearchResult }, OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus, - OnSecondaryActionPluginViewNoPanelKeyboardWithFocus { widget_id: UiWidgetId }, - OnAnyActionMainViewAnyPanelMouse { widget_id: UiWidgetId }, + OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { search_result: SearchResult, widget_id: UiWidgetId }, + OnAnyActionMainViewInlineViewPanelKeyboardWithFocus { widget_id: UiWidgetId }, + OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id: UiWidgetId }, + OnAnyActionPluginViewAnyPanelKeyboardWithFocus { widget_id: UiWidgetId }, + OnAnyActionPluginViewAnyPanel { widget_id: UiWidgetId }, + OnAnyActionMainViewSearchResultPanelMouse { widget_id: UiWidgetId }, OnPrimaryActionMainViewActionPanelMouse { widget_id: UiWidgetId }, ResetMainViewState, OnAnyActionMainViewNoPanelKeyboardAtIndex { index: usize }, @@ -559,6 +563,48 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { state.run_generated_command(plugin_id, entrypoint_id, action_index), ]) } + AppMsg::RunPluginAction { render_location, plugin_id, widget_id } => { + let widget_event = ComponentWidgetEvent::RunAction { + widget_id, + }; + + Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + } + AppMsg::RunSearchItemAction(search_result, action_index) => { + match search_result.entrypoint_type { + SearchResultEntrypointType::Command => { + match action_index { + None => { + Task::done(AppMsg::RunCommand { + entrypoint_id: search_result.entrypoint_id.clone(), + plugin_id: search_result.plugin_id.clone() + }) + } + Some(_) => Task::none() + } + }, + SearchResultEntrypointType::View => { + match action_index { + None => { + Task::done(AppMsg::OpenView { + plugin_id: search_result.plugin_id.clone(), + plugin_name: search_result.plugin_name.clone(), + entrypoint_id: search_result.entrypoint_id.clone(), + entrypoint_name: search_result.entrypoint_name.clone(), + }) + } + Some(_) => Task::none() + } + }, + SearchResultEntrypointType::GeneratedCommand => { + Task::done(AppMsg::RunGeneratedCommandEvent { + entrypoint_id: search_result.entrypoint_id.clone(), + plugin_id: search_result.plugin_id.clone(), + action_index, + }) + }, + } + } AppMsg::PromptChanged(mut new_prompt) => { if cfg!(feature = "scenario_runner") { Task::none() @@ -903,41 +949,6 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } - AppMsg::RunSearchItemAction(search_result, action_index) => { - match search_result.entrypoint_type { - SearchResultEntrypointType::Command => { - match action_index { - None => { - Task::done(AppMsg::RunCommand { - entrypoint_id: search_result.entrypoint_id.clone(), - plugin_id: search_result.plugin_id.clone() - }) - } - Some(_) => Task::none() - } - }, - SearchResultEntrypointType::View => { - match action_index { - None => { - Task::done(AppMsg::OpenView { - plugin_id: search_result.plugin_id.clone(), - plugin_name: search_result.plugin_name.clone(), - entrypoint_id: search_result.entrypoint_id.clone(), - entrypoint_name: search_result.entrypoint_name.clone(), - }) - } - Some(_) => Task::none() - } - }, - SearchResultEntrypointType::GeneratedCommand => { - Task::done(AppMsg::RunGeneratedCommandEvent { - entrypoint_id: search_result.entrypoint_id.clone(), - plugin_id: search_result.plugin_id.clone(), - action_index, - }) - }, - } - } AppMsg::Screenshot { save_path } => { println!("Creating screenshot at: {}", save_path); @@ -1031,7 +1042,13 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result } => { Task::done(AppMsg::RunSearchItemAction(search_result, None)) } - AppMsg::OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id } => { + AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus => { + Task::done(AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index: 1 }) + } + AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result } => { + Task::done(AppMsg::RunSearchItemAction(search_result, Some(0))) + } + AppMsg::OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id } => { let run_action_command = if widget_id == 0 { Task::done(AppMsg::RunSearchItemAction(search_result, None)) } else { @@ -1043,42 +1060,38 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::done(AppMsg::ResetMainViewState) ]) } - AppMsg::OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id } => { + AppMsg::OnAnyActionMainViewInlineViewPanelKeyboardWithFocus { widget_id } => { let client_context = state.client_context.read().expect("lock is poisoned"); match client_context.get_first_inline_view_container() { Some(container) => { let plugin_id = container.get_plugin_id(); - let widget_event = ComponentWidgetEvent::RunAction { - widget_id, - }; - let render_location = UiRenderLocation::InlineView; - Task::batch([ Task::done(AppMsg::ToggleActionPanel { keyboard: true }), - Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + Task::done(AppMsg::RunPluginAction { + render_location: UiRenderLocation::InlineView, + plugin_id, + widget_id, + }) ]) } None => Task::none() } } - AppMsg::OnPrimaryActionPluginViewNoPanelKeyboardWithFocus { widget_id } => { - Task::perform(async {}, move |_| AppMsg::OnAnyActionPluginViewAnyPanel { widget_id }) + AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id } => { + Task::done(AppMsg::OnAnyActionPluginViewAnyPanel { widget_id }) } - AppMsg::OnPrimaryActionPluginViewAnyPanelKeyboardWithFocus { widget_id } => { + AppMsg::OnAnyActionPluginViewAnyPanelKeyboardWithFocus { widget_id } => { let client_context = state.client_context.read().expect("lock is poisoned"); - let plugin_id = client_context.get_view_plugin_id(); - - let widget_event = ComponentWidgetEvent::RunAction { - widget_id, - }; - let render_location = UiRenderLocation::View; - Task::batch([ Task::done(AppMsg::ToggleActionPanel { keyboard: true }), - Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + Task::done(AppMsg::RunPluginAction { + render_location: UiRenderLocation::View, + plugin_id: client_context.get_view_plugin_id(), + widget_id, + }) ]) } AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id: _ } => { @@ -1096,15 +1109,6 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { GlobalState::ErrorView { .. } => Task::none() } } - AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus => { - Task::done(AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index: 1 }) - } - AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result } => { - Task::done(AppMsg::RunSearchItemAction(search_result, Some(0))) - } - AppMsg::OnSecondaryActionPluginViewNoPanelKeyboardWithFocus { widget_id } => { - Task::done(AppMsg::OnAnyActionPluginViewAnyPanel { widget_id }) - } AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index } => { let client_context = state.client_context.read().expect("lock is poisoned"); @@ -1116,12 +1120,11 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Some(widget_id) => { let widget_id = *widget_id; - let widget_event = ComponentWidgetEvent::RunAction { + Task::done(AppMsg::RunPluginAction { + render_location: UiRenderLocation::InlineView, + plugin_id, widget_id, - }; - let render_location = UiRenderLocation::InlineView; - - Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + }) } None => Task::none() } @@ -1129,7 +1132,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } } - AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id } => { + AppMsg::OnAnyActionMainViewSearchResultPanelMouse { widget_id } => { match &mut state.global_state { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { @@ -1137,13 +1140,13 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { MainViewState::SearchResultActionPanel { .. } => { if let Some(search_result) = focused_search_result.get(&state.search_results) { let search_result = search_result.clone(); - Task::done(AppMsg::OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id }) + Task::done(AppMsg::OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id }) } else { Task::none() } } MainViewState::InlineViewActionPanel { .. } => { - Task::done(AppMsg::OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id }) + Task::none() } } } @@ -1154,15 +1157,11 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { AppMsg::OnAnyActionPluginViewAnyPanel { widget_id } => { let client_context = state.client_context.read().expect("lock is poisoned"); - let plugin_id = client_context.get_view_plugin_id(); - - let widget_event = ComponentWidgetEvent::RunAction { + Task::done(AppMsg::RunPluginAction { + render_location: UiRenderLocation::View, + plugin_id: client_context.get_view_plugin_id(), widget_id, - }; - - let render_location = UiRenderLocation::View; - - Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + }) } AppMsg::OpenPluginView(plugin_id, entrypoint_id) => { state.open_plugin_view(plugin_id, entrypoint_id) @@ -1724,7 +1723,7 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { "", || AppMsg::ToggleActionPanel { keyboard: false }, |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, - |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, + |widget_id| AppMsg::Noop, || AppMsg::Noop, ) } @@ -1741,7 +1740,7 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { "", || AppMsg::ToggleActionPanel { keyboard: false }, |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, - |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, + |widget_id| AppMsg::OnAnyActionMainViewSearchResultPanelMouse { widget_id }, || AppMsg::Noop, ) } @@ -1758,7 +1757,7 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { "", || AppMsg::ToggleActionPanel { keyboard: false }, |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, - |widget_id| AppMsg::OnAnyActionMainViewAnyPanelMouse { widget_id }, + |widget_id| AppMsg::OnAnyActionMainViewInlineViewPanelKeyboardWithFocus { widget_id }, || AppMsg::Noop, ) } diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 79c1e2a..8a69809 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -156,7 +156,7 @@ impl Focus for GlobalState { Some(widget_id) => { if let Some(search_result) = focused_search_result.get(&focus_list) { let search_result = search_result.clone(); - Task::done(AppMsg::OnPrimaryActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id }) + Task::done(AppMsg::OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id }) } else { Task::none() } @@ -167,7 +167,7 @@ impl Focus for GlobalState { match focused_action_item.index { None => Task::none(), Some(widget_id) => { - Task::perform(async {}, move |_| AppMsg::OnPrimaryActionMainViewInlineViewPanelKeyboardWithFocus { widget_id }) + Task::done(AppMsg::OnAnyActionMainViewInlineViewPanelKeyboardWithFocus { widget_id }) } } } @@ -182,7 +182,7 @@ impl Focus for GlobalState { PluginViewState::None => { if let Some(widget_id) = action_ids.get(0) { let widget_id = *widget_id; - Task::perform(async {}, move |_| AppMsg::OnPrimaryActionPluginViewNoPanelKeyboardWithFocus { widget_id }) + Task::done(AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id }) } else { Task::none() } @@ -190,7 +190,7 @@ impl Focus for GlobalState { PluginViewState::ActionPanel { focused_action_item, .. } => { if let Some(widget_id) = focused_action_item.get(&action_ids) { let widget_id = *widget_id; - Task::perform(async {}, move |_| AppMsg::OnPrimaryActionPluginViewAnyPanelKeyboardWithFocus { widget_id }) + Task::done(AppMsg::OnAnyActionPluginViewAnyPanelKeyboardWithFocus { widget_id }) } else { Task::none() } @@ -228,7 +228,7 @@ impl Focus for GlobalState { PluginViewState::None => { if let Some(widget_id) = action_ids.get(1) { let widget_id = *widget_id; - Task::perform(async {}, move |_| AppMsg::OnSecondaryActionPluginViewNoPanelKeyboardWithFocus { widget_id }) + Task::done(AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id }) } else { Task::none() } @@ -289,12 +289,12 @@ impl Focus for GlobalState { } } PluginViewState::ActionPanel { .. } => { - Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) + Task::done(AppMsg::ToggleActionPanel { keyboard: true }) } } } GlobalState::ErrorView { .. } => { - Task::perform(async {}, |_| AppMsg::HideWindow) + Task::done(AppMsg::HideWindow) } } } From 17648299ef13c9620028648ebe51a98c12789961 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:49:03 +0100 Subject: [PATCH 203/540] Fix window transparency not working on macOS --- Cargo.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3e9d07..cc263dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2798,7 +2798,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "dpi" version = "0.1.1" -source = "git+https://github.com/project-gauntlet/winit.git?rev=0f1b21e4c12ce0b5b3b8f2c8598f8163c76eff10#0f1b21e4c12ce0b5b3b8f2c8598f8163c76eff10" +source = "git+https://github.com/project-gauntlet/winit.git?rev=ddf41619ffb9e1fc9fc157ca6ea99b81e62b0e5a#ddf41619ffb9e1fc9fc157ca6ea99b81e62b0e5a" [[package]] name = "dprint-swc-ext" @@ -4451,7 +4451,7 @@ dependencies = [ [[package]] name = "iced" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" dependencies = [ "iced_core", "iced_futures", @@ -4479,7 +4479,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" dependencies = [ "bitflags 2.6.0", "bytes", @@ -4506,7 +4506,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" dependencies = [ "futures", "iced_core", @@ -4520,7 +4520,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -4572,7 +4572,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -4584,7 +4584,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" dependencies = [ "bytes", "iced_core", @@ -4605,7 +4605,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" dependencies = [ "bytemuck", "cosmic-text", @@ -4620,7 +4620,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -4639,7 +4639,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" dependencies = [ "iced_renderer", "iced_runtime", @@ -4654,7 +4654,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#7c26aa9e30be731e2f18879a91c12c20b1f4a0ca" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" dependencies = [ "iced_futures", "iced_graphics", @@ -11739,7 +11739,7 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" version = "0.30.99" -source = "git+https://github.com/project-gauntlet/winit.git?rev=0f1b21e4c12ce0b5b3b8f2c8598f8163c76eff10#0f1b21e4c12ce0b5b3b8f2c8598f8163c76eff10" +source = "git+https://github.com/project-gauntlet/winit.git?rev=ddf41619ffb9e1fc9fc157ca6ea99b81e62b0e5a#ddf41619ffb9e1fc9fc157ca6ea99b81e62b0e5a" dependencies = [ "ahash 0.8.11", "android-activity", @@ -11753,7 +11753,7 @@ dependencies = [ "core-foundation 0.9.4", "core-graphics 0.23.2", "cursor-icon", - "dpi 0.1.1 (git+https://github.com/project-gauntlet/winit.git?rev=0f1b21e4c12ce0b5b3b8f2c8598f8163c76eff10)", + "dpi 0.1.1 (git+https://github.com/project-gauntlet/winit.git?rev=ddf41619ffb9e1fc9fc157ca6ea99b81e62b0e5a)", "js-sys", "libc", "memmap2 0.9.5", From 1f2fe341d621dbe104f6a5a2ada7dba2eeaacbf5 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:38:42 +0100 Subject: [PATCH 204/540] Fix window flashing and not appearing after hud window is shown --- rust/client/src/ui/mod.rs | 42 +++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 85b777f..f3b3c2f 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -121,7 +121,7 @@ pub enum AppMsg { has_children: bool, render_location: UiRenderLocation, }, - IcedEvent(Event), + IcedEvent(window::Id, Event), WidgetEvent { plugin_id: PluginId, render_location: UiRenderLocation, @@ -688,7 +688,15 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } } - AppMsg::IcedEvent(Event::Keyboard(event)) => { + AppMsg::IcedEvent(window_id, Event::Keyboard(event)) => { + let Some(main_window_id) = state.main_window_id else { + return Task::none() + }; + + if window_id != main_window_id { + return Task::none() + } + match event { keyboard::Event::KeyPressed { key, modifiers, physical_key, text, .. } => { tracing::debug!("Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", key, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); @@ -868,21 +876,33 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { _ => Task::none() } } - AppMsg::IcedEvent(Event::Window(window::Event::Focused)) => { - if state.wayland { - Task::none() - } else { - state.on_focused() + AppMsg::IcedEvent(window_id, Event::Window(window::Event::Focused)) => { + let Some(main_window_id) = state.main_window_id else { + return Task::none() + }; + + if window_id != main_window_id { + return Task::none() } + + state.on_focused() } - AppMsg::IcedEvent(Event::Window(window::Event::Unfocused)) => { + AppMsg::IcedEvent(window_id, Event::Window(window::Event::Unfocused)) => { + let Some(main_window_id) = state.main_window_id else { + return Task::none() + }; + + if window_id != main_window_id { + return Task::none() + } + if state.wayland { state.hide_window() } else { state.on_unfocused() } } - AppMsg::IcedEvent(_) => Task::none(), + AppMsg::IcedEvent(_, _) => Task::none(), AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::Noop, .. } => Task::none(), AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => state.global_state.back(), AppMsg::WidgetEvent { widget_event, plugin_id, render_location } => { @@ -1804,9 +1824,9 @@ fn subscription(state: &AppModel) -> Subscription { struct GlobalShortcutListener; let events_subscription = event::listen_with(|event, status, window_id| match status { - event::Status::Ignored => Some(AppMsg::IcedEvent(event)), + event::Status::Ignored => Some(AppMsg::IcedEvent(window_id, event)), event::Status::Captured => match &event { - Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Named(Named::Escape), .. }) => Some(AppMsg::IcedEvent(event)), + Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Named(Named::Escape), .. }) => Some(AppMsg::IcedEvent(window_id, event)), _ => None } }); From 06ecad922dda467891a1c23340b9ed789ecea485 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:40:00 +0100 Subject: [PATCH 205/540] Fix hud window not disappearing properly when multiple hud windows were present at the same time --- rust/client/src/ui/hud/mod.rs | 26 ++++++-------------------- rust/client/src/ui/mod.rs | 14 +------------- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/rust/client/src/ui/hud/mod.rs b/rust/client/src/ui/hud/mod.rs index a3006aa..940d80f 100644 --- a/rust/client/src/ui/hud/mod.rs +++ b/rust/client/src/ui/hud/mod.rs @@ -42,7 +42,8 @@ fn open_non_wayland() -> Task { window::open(settings) .1 - .then(|id| in_2_seconds(AppMsg::CloseHudWindow { id })) + .then(|id| sleep_for_2_seconds(id)) + .then(|id| window::close(id)) } #[cfg(target_os = "linux")] @@ -51,23 +52,8 @@ fn open_wayland() -> Task { let settings = layer_shell_settings(); Task::done(AppMsg::LayerShell(crate::ui::layer_shell::LayerShellAppMsg::NewLayerShell { id, settings })) - .then(move |_| in_2_seconds(AppMsg::CloseHudWindow { id })) -} - -pub fn close_hud_window( - #[cfg(target_os = "linux")] - wayland: bool, - id: window::Id -) -> Task { - #[cfg(target_os = "linux")] - if wayland { - Task::done(AppMsg::LayerShell(crate::ui::layer_shell::LayerShellAppMsg::RemoveWindow(id))) - } else { - window::close(id) - } - - #[cfg(not(target_os = "linux"))] - window::close(id) + .then(move |_| sleep_for_2_seconds(id)) + .then(|id| Task::done(AppMsg::LayerShell(crate::ui::layer_shell::LayerShellAppMsg::RemoveWindow(id)))) } #[cfg(target_os = "linux")] @@ -84,10 +70,10 @@ fn layer_shell_settings() -> iced_layershell::reexport::NewLayerShellSettings { } } -fn in_2_seconds(msg: AppMsg) -> Task { +fn sleep_for_2_seconds(id: window::Id) -> Task { Task::perform(async move { tokio::time::sleep(Duration::from_secs(2)).await; - msg + id }, convert::identity) } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index f3b3c2f..b1e969e 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -53,7 +53,7 @@ mod grid_navigation; use crate::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; use crate::ui::custom_widgets::loading_bar::LoadingBar; -use crate::ui::hud::{close_hud_window, show_hud_window}; +use crate::ui::hud::show_hud_window; use crate::ui::scroll_handle::ScrollHandle; use crate::ui::state::{ErrorViewData, Focus, GlobalState, LoadingBarState, MainViewState, PluginViewData, PluginViewState}; use crate::ui::widget_container::PluginWidgetContainer; @@ -168,9 +168,6 @@ pub enum AppMsg { ShowHud { display: String }, - CloseHudWindow { - id: window::Id - }, OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus, OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result: SearchResult }, OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result: SearchResult }, @@ -1204,15 +1201,6 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { state.wayland, ) } - AppMsg::CloseHudWindow { id } => { - state.hud_display = None; - - close_hud_window( - #[cfg(target_os = "linux")] - state.wayland, - id - ) - } AppMsg::ResetMainViewState => { match &mut state.global_state { GlobalState::MainView { sub_state, .. } => { From 5b769c8e7db0fdfd3f7c1ce0321910891939b4f2 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Tue, 26 Nov 2024 20:12:21 +0100 Subject: [PATCH 206/540] Deno update to 2.1.1 --- Cargo.toml | 3 - bundled_plugins/gauntlet/package.json | 2 +- bundled_plugins/gauntlet/src/applications.ts | 109 ++---- bundled_plugins/gauntlet/src/calculator.tsx | 11 +- bundled_plugins/gauntlet/src/settings.tsx | 10 +- bundled_plugins/gauntlet/tsconfig.json | 2 +- dev_plugin/package.json | 2 +- js/api/.gitignore | 1 + js/api/package.json | 9 +- js/api/rollup.config.ts | 24 ++ js/api/src/helpers.ts | 35 +- js/api/src/hooks.ts | 2 +- js/api_build/package.json | 4 +- js/bridge_build/.gitignore | 2 + js/bridge_build/package.json | 14 + js/bridge_build/src/index.ts | 212 ++++++++++ js/bridge_build/tsconfig.json | 14 + js/build/package.json | 4 +- js/core/package.json | 11 +- js/core/rollup.config.ts | 15 +- js/core/src/command-generator.ts | 24 +- js/core/src/{init.tsx => core.tsx} | 76 ++-- js/core/src/init.ts | 10 + js/core/src/internal-all.ts | 5 + js/core/src/internal-linux.ts | 5 + js/core/src/internal-macos.ts | 12 + js/core/src/search-index.ts | 7 +- js/core/typings/index.d.ts | 25 +- js/deno/generator/index.ts | 4 +- js/deno/package.json | 4 +- js/react/package.json | 6 +- js/react/rollup.config.ts | 11 +- js/react_renderer/package.json | 7 +- js/react_renderer/rollup.config.ts | 11 +- js/react_renderer/src/renderer.ts | 60 +-- js/scenario_runner_cli/package.json | 4 +- js/typings/index.d.ts | 151 ++++++-- package-lock.json | 361 ++++++++++++------ package.json | 1 + rust/server/Cargo.toml | 10 +- rust/server/src/plugins/js/assets.rs | 18 +- rust/server/src/plugins/js/clipboard.rs | 24 +- .../src/plugins/js/command_generators.rs | 7 +- rust/server/src/plugins/js/environment.rs | 20 +- rust/server/src/plugins/js/logs.rs | 22 +- rust/server/src/plugins/js/mod.rs | 355 ++++++++++------- rust/server/src/plugins/js/permissions.rs | 154 ++++---- .../src/plugins/js/plugins/applications.rs | 53 +-- rust/server/src/plugins/js/plugins/numbat.rs | 7 +- .../server/src/plugins/js/plugins/settings.rs | 6 +- rust/server/src/plugins/js/preferences.rs | 20 +- rust/server/src/plugins/js/search.rs | 6 +- rust/server/src/plugins/js/ui.rs | 51 +-- scenarios/plugins/docs_detail/package.json | 2 +- scenarios/plugins/docs_form/package.json | 2 +- scenarios/plugins/docs_grid/package.json | 2 +- .../docs_inline_separators/package.json | 2 +- .../docs_inline_three_sections/package.json | 2 +- .../docs_inline_two_sections/package.json | 2 +- scenarios/plugins/docs_list/package.json | 2 +- tools | 2 +- 61 files changed, 1339 insertions(+), 700 deletions(-) create mode 100644 js/api/rollup.config.ts create mode 100644 js/bridge_build/.gitignore create mode 100644 js/bridge_build/package.json create mode 100644 js/bridge_build/src/index.ts create mode 100644 js/bridge_build/tsconfig.json rename js/core/src/{init.tsx => core.tsx} (72%) create mode 100644 js/core/src/init.ts create mode 100644 js/core/src/internal-all.ts create mode 100644 js/core/src/internal-linux.ts create mode 100644 js/core/src/internal-macos.ts diff --git a/Cargo.toml b/Cargo.toml index e1f430d..e555f27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,3 @@ cli = { path = "rust/cli" } [features] release = ["cli/release"] scenario_runner = ["cli/scenario_runner"] - -[profile.release] -opt-level = 1 # something at opt-level 2 breaks deno \ No newline at end of file diff --git a/bundled_plugins/gauntlet/package.json b/bundled_plugins/gauntlet/package.json index 58d1505..66a4080 100644 --- a/bundled_plugins/gauntlet/package.json +++ b/bundled_plugins/gauntlet/package.json @@ -14,6 +14,6 @@ "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../tools", "@types/react": "^18.2.14", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/bundled_plugins/gauntlet/src/applications.ts b/bundled_plugins/gauntlet/src/applications.ts index 2b198e6..f4b6f03 100644 --- a/bundled_plugins/gauntlet/src/applications.ts +++ b/bundled_plugins/gauntlet/src/applications.ts @@ -1,78 +1,35 @@ import { GeneratedCommand, GeneratorProps } from "@project-gauntlet/api/helpers"; import { walk, WalkOptions } from "@std/fs/walk"; import { debounce } from "@std/async/debounce"; - -// @ts-expect-error -const denoCore: DenoCore = Deno[Deno.internal].core; -const InternalApi: InternalApi = denoCore.ops; - -type LinuxDesktopApplicationData = { - name: string - icon: ArrayBuffer | undefined, -} - -type MacOSDesktopApplicationData = { - name: string - path: string, - icon: ArrayBuffer | undefined, -} - -type DesktopPathAction = DesktopPathActionAdd | DesktopPathActionRemove - -type DesktopPathActionAdd = { - type: "add", - id: string, - data: DATA -} - -type DesktopPathActionRemove = { - type: "remove" - id: string -} - -type MacOSDesktopSettingsPre13Data = { - name: string - path: string, - icon: ArrayBuffer | undefined, -} - -type MacOSDesktopSettings13AndPostData = { - name: string - preferences_id: string - icon: ArrayBuffer | undefined, -} - -interface InternalApi { - current_os(): string - - linux_open_application(desktop_id: string): void - linux_application_dirs(): string[] - linux_app_from_path(path: string): Promise> - - macos_major_version(): number - - macos_settings_pre_13(): MacOSDesktopSettingsPre13Data[] - macos_settings_13_and_post(): MacOSDesktopSettings13AndPostData[] - macos_open_setting_13_and_post(preferences_id: String): void - macos_open_setting_pre_13(setting_path: String): void - - macos_system_applications(): string[] - macos_application_dirs(): string[] - macos_app_from_path(path: string): Promise> - macos_app_from_arbitrary_path(path: string): Promise> - macos_open_application(app_path: String): void -} +import { current_os } from "gauntlet:bridge/internal-all"; +import { + linux_app_from_path, + linux_application_dirs, + linux_open_application, +} from "gauntlet:bridge/internal-linux"; +import { + macos_app_from_arbitrary_path, + macos_app_from_path, + macos_application_dirs, + macos_major_version, + macos_open_application, + macos_open_setting_13_and_post, + macos_open_setting_pre_13, + macos_settings_13_and_post, + macos_settings_pre_13, + macos_system_applications +} from "gauntlet:bridge/internal-macos"; export default async function Applications({ add, remove }: GeneratorProps): Promise void)> { - switch (InternalApi.current_os()) { + switch (current_os()) { case "linux": { return await genericGenerator( - InternalApi.linux_application_dirs(), - path => InternalApi.linux_app_from_path(path), + linux_application_dirs(), + path => linux_app_from_path(path), (id, data) => ({ name: data.name, fn: () => { - InternalApi.linux_open_application(id) + linux_open_application(id) }, icon: data.icon, // TODO lazy icons }), @@ -81,32 +38,32 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro ); } case "macos": { - const majorVersion = InternalApi.macos_major_version(); + const majorVersion = macos_major_version(); if (majorVersion >= 13) { - for (const setting of InternalApi.macos_settings_13_and_post()) { + for (const setting of macos_settings_13_and_post()) { add(`settings:${setting.preferences_id}`, { name: setting.name, fn: () => { - InternalApi.macos_open_setting_13_and_post(setting.preferences_id) + macos_open_setting_13_and_post(setting.preferences_id) }, icon: setting.icon, }) } } else { - for (const setting of InternalApi.macos_settings_pre_13()) { + for (const setting of macos_settings_pre_13()) { add(`settings:${setting.path}`, { name: setting.name, fn: () => { - InternalApi.macos_open_setting_pre_13(setting.path) + macos_open_setting_pre_13(setting.path) }, icon: setting.icon, }) } } - for (const path of InternalApi.macos_system_applications()) { - const app = await InternalApi.macos_app_from_path(path) + for (const path of macos_system_applications()) { + const app = await macos_app_from_path(path) if (app) { switch (app.type) { case "add": { @@ -114,7 +71,7 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro add(data.path, { name: data.name, fn: () => { - InternalApi.macos_open_application(data.path) + macos_open_application(data.path) }, icon: data.icon, }) @@ -127,12 +84,12 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro } return await genericGenerator( - InternalApi.macos_application_dirs(), - path => InternalApi.macos_app_from_arbitrary_path(path), + macos_application_dirs(), + path => macos_app_from_arbitrary_path(path), (_id, data) => ({ name: data.name, fn: () => { - InternalApi.macos_open_application(data.path) + macos_open_application(data.path) }, icon: data.icon, }), diff --git a/bundled_plugins/gauntlet/src/calculator.tsx b/bundled_plugins/gauntlet/src/calculator.tsx index 3a2ad06..f7588cd 100644 --- a/bundled_plugins/gauntlet/src/calculator.tsx +++ b/bundled_plugins/gauntlet/src/calculator.tsx @@ -1,14 +1,7 @@ import { Action, ActionPanel, Content, Icons, Inline } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; import { Clipboard, showHud } from "@project-gauntlet/api/helpers"; - -// @ts-expect-error -const denoCore: DenoCore = Deno[Deno.internal].core; -const InternalApi: InternalApi = denoCore.ops; - -interface InternalApi { - run_numbat(input: string): { left: string, right: string } -} +import { run_numbat } from "gauntlet:bridge/internal-all"; export default function Calculator(props: { text: string }): ReactNode | undefined { const text = props.text; @@ -20,7 +13,7 @@ export default function Calculator(props: { text: string }): ReactNode | undefin let result; try { - result = InternalApi.run_numbat(text); + result = run_numbat(text); } catch (e) { // this view is executed on every key press in main search bar // when numbat run fails it means expression is not valid so we return here and do not show inline view diff --git a/bundled_plugins/gauntlet/src/settings.tsx b/bundled_plugins/gauntlet/src/settings.tsx index 9df14d6..7d58d8e 100644 --- a/bundled_plugins/gauntlet/src/settings.tsx +++ b/bundled_plugins/gauntlet/src/settings.tsx @@ -1,11 +1,5 @@ -// @ts-expect-error -const denoCore: DenoCore = Deno[Deno.internal].core; -const InternalApi: InternalApi = denoCore.ops; - -interface InternalApi { - open_settings(): void -} +import { open_settings } from "gauntlet:bridge/internal-all"; export default function Settings(): void { - InternalApi.open_settings() + open_settings() } diff --git a/bundled_plugins/gauntlet/tsconfig.json b/bundled_plugins/gauntlet/tsconfig.json index cbe7961..7f14964 100644 --- a/bundled_plugins/gauntlet/tsconfig.json +++ b/bundled_plugins/gauntlet/tsconfig.json @@ -6,7 +6,7 @@ "target": "ES2022", "moduleResolution": "bundler", "jsx": "react-jsx", - "types": ["@project-gauntlet/deno"] + "types": ["@project-gauntlet/typings", "@project-gauntlet/deno"] }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/dev_plugin/package.json b/dev_plugin/package.json index ed9574f..617e170 100644 --- a/dev_plugin/package.json +++ b/dev_plugin/package.json @@ -14,6 +14,6 @@ "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../js/deno", "@project-gauntlet/tools": "file:../tools", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/js/api/.gitignore b/js/api/.gitignore index cf0ba43..44d689f 100644 --- a/js/api/.gitignore +++ b/js/api/.gitignore @@ -1,2 +1,3 @@ dist disttypes +node_modules \ No newline at end of file diff --git a/js/api/package.json b/js/api/package.json index 55547d7..e0c76bb 100644 --- a/js/api/package.json +++ b/js/api/package.json @@ -22,11 +22,16 @@ "disttypes" ], "scripts": { - "build": "tsc" + "build": "tsc && rollup --config rollup.config.ts --configPlugin typescript" }, "devDependencies": { "@project-gauntlet/typings": "*", - "typescript": "^5.3.3" + "@rollup/plugin-alias": "^5.1.1", + "@types/react": "^18.2.35", + "rollup": "^4.27.4", + "tslib": "^2.6.2", + "typescript": "^5.7.2", + "glob": "^11.0.0" }, "publishConfig": { "access": "public" diff --git a/js/api/rollup.config.ts b/js/api/rollup.config.ts new file mode 100644 index 0000000..6bdeb10 --- /dev/null +++ b/js/api/rollup.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "rollup"; +import alias from '@rollup/plugin-alias'; +import { globSync } from "glob"; + +export default defineConfig({ + input: globSync('dist/**/*', { nodir: true }), + output: [ + { + dir: 'dist', + format: 'esm', + sourcemap: 'inline', + preserveModules: true, + } + ], + external: [/^ext:.+/], + plugins: [ + alias({ + entries: [ + { find: 'react/jsx-runtime', replacement: 'ext:gauntlet/react-jsx-runtime.js' }, + { find: 'react', replacement: 'ext:gauntlet/react.js' }, + ] + }), + ] +}) diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index b832bc2..3e34339 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -1,9 +1,16 @@ // @ts-ignore TODO how to add declaration for this? -import { getAssetData, getAssetDataSync, getPluginPreferences, getEntrypointPreferences, showHudWindow } from "gauntlet:renderer"; - -// @ts-expect-error does typescript support such symbol declarations? -const denoCore: DenoCore = Deno[Deno.internal].core; -const InternalApi = denoCore.ops; +import { getAssetData, getAssetDataSync, getPluginPreferences, getEntrypointPreferences, showHudWindow } from "ext:gauntlet/renderer.js"; +import { + clipboard_clear, + clipboard_read, + clipboard_read_text, + clipboard_write, + clipboard_write_text, + environment_gauntlet_version, + environment_is_development, + environment_plugin_cache_dir, + environment_plugin_data_dir +} from "ext:core/ops"; export function assetDataSync(path: string): ArrayBuffer { return getAssetDataSync(path) @@ -45,7 +52,7 @@ export type GeneratorProps = { export const Clipboard: Clipboard = { read: async function (): Promise<{ "text/plain"?: string | undefined; "image/png"?: Blob | undefined; }> { - const data = await InternalApi.clipboard_read(); + const data = await clipboard_read(); const result: { "text/plain"?: string; "image/png"?: Blob; } = {}; @@ -60,21 +67,21 @@ export const Clipboard: Clipboard = { return result }, readText: async function (): Promise { - return await InternalApi.clipboard_read_text() + return await clipboard_read_text() }, write: async function (data: { "text/plain"?: string | undefined; "image/png"?: Blob | undefined; }): Promise { const text_data = data["text/plain"]; const png_data = data["image/png"]; - return await InternalApi.clipboard_write({ + return await clipboard_write({ text_data: text_data, png_data: png_data != undefined ? Array.from(new Uint8Array(png_data as any)) : undefined, // TODO arraybuffer? fix when migrating to deno's op2 }) }, writeText: async function (data: string): Promise { - return await InternalApi.clipboard_write_text(data) + return await clipboard_write_text(data) }, clear: async function (): Promise { - await InternalApi.clipboard_clear() + await clipboard_clear() } } @@ -88,16 +95,16 @@ export interface Clipboard { export const Environment: Environment = { get gauntletVersion(): number { - return InternalApi.environment_gauntlet_version() + return environment_gauntlet_version() }, get isDevelopment(): boolean { - return InternalApi.environment_is_development() + return environment_is_development() }, get pluginDataDir(): string { - return InternalApi.environment_plugin_data_dir() + return environment_plugin_data_dir() }, get pluginCacheDir(): string { - return InternalApi.environment_plugin_cache_dir() + return environment_plugin_cache_dir() }, } diff --git a/js/api/src/hooks.ts b/js/api/src/hooks.ts index 535ec31..aa1f8cf 100644 --- a/js/api/src/hooks.ts +++ b/js/api/src/hooks.ts @@ -1,6 +1,6 @@ import { ReactNode, useRef, useId, useState, useCallback, useEffect, MutableRefObject, Dispatch, SetStateAction } from 'react'; // @ts-ignore TODO how to add declaration for this? -import { useGauntletContext } from "gauntlet:renderer"; +import { useGauntletContext } from "ext:gauntlet/renderer.js"; export function useNavigation(): { popView: () => void, pushView: (component: ReactNode) => void } { const { popView, pushView }: { popView: () => void, pushView: (component: ReactNode) => void } = useGauntletContext(); diff --git a/js/api_build/package.json b/js/api_build/package.json index 813d333..bfc1b57 100644 --- a/js/api_build/package.json +++ b/js/api_build/package.json @@ -9,7 +9,7 @@ "run-generator": "node dist/index.js" }, "devDependencies": { - "@types/node": "^18.17.1", - "typescript": "^5.3.3" + "@types/node": "^18.19.67", + "typescript": "^5.7.2" } } diff --git a/js/bridge_build/.gitignore b/js/bridge_build/.gitignore new file mode 100644 index 0000000..d77bc98 --- /dev/null +++ b/js/bridge_build/.gitignore @@ -0,0 +1,2 @@ +component_model.json +dist \ No newline at end of file diff --git a/js/bridge_build/package.json b/js/bridge_build/package.json new file mode 100644 index 0000000..6d01f59 --- /dev/null +++ b/js/bridge_build/package.json @@ -0,0 +1,14 @@ +{ + "name": "@project-gauntlet/bridge-build", + "version": "0.1.0", + "type": "module", + "scripts": { + "build": "npm run build-generator && npm run run-generator", + "build-generator": "tsc", + "run-generator": "node dist/index.js" + }, + "devDependencies": { + "@types/node": "^18.19.67", + "typescript": "^5.7.2" + } +} diff --git a/js/bridge_build/src/index.ts b/js/bridge_build/src/index.ts new file mode 100644 index 0000000..b4ff4c7 --- /dev/null +++ b/js/bridge_build/src/index.ts @@ -0,0 +1,212 @@ +import ts, { + ExpressionStatement, + ImportDeclaration, + isExportDeclaration, + isExportSpecifier, + isIdentifier, + isNamedExports, + ScriptKind, + Statement +} from "typescript"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; + + +function generate(outFile: string, sourceFile: ts.SourceFile) { + const resultFile = ts.createSourceFile("unused", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.JS); + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + + const result = printer.printNode(ts.EmitHint.Unspecified, sourceFile, resultFile); + + writeFileSync(outFile, result) +} + +function collectExports(inputFile: string): string[] { + const sourceFile = ts.createSourceFile( + "input.js", + readFileSync(inputFile).toString(), + ts.ScriptTarget.ESNext, + /*setParentNodes */ false, + ScriptKind.JS + ); + + const result: string[] = [] + + for (const statement of sourceFile.statements) { + if (isExportDeclaration(statement)) { + const exportClause = statement.exportClause; + if (exportClause) { + if (isNamedExports(exportClause)) { + for (const element of exportClause.elements) { + if (isExportSpecifier(element)) { + if (isIdentifier(element.name)) { + if (typeof element.name.escapedText === "string") { + if (element.name.escapedText.startsWith("___")) { + result.push(element.name.escapedText.slice(1)) // remove one _, typescript special case + } else { + result.push(element.name.escapedText) + } + } else { + throw new Error(`unexpected export clause element element name type: ${JSON.stringify(element)}`) + } + } else { + throw new Error(`unknown export clause element element name type: ${JSON.stringify(element)}`) + } + } else { + throw new Error(`unknown export clause element: ${JSON.stringify(element)}`) + } + } + } else { + throw new Error(`unknown export clause: ${JSON.stringify(exportClause)}`) + } + } + } + } + + return result +} + + +function generateInternal(exportConfig: Record): ts.SourceFile { + + function createImport(exports: string[], namespace:string, importString: string): ImportDeclaration { + return ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports(exports.map(value => { + return ts.factory.createImportSpecifier( + false, + ts.factory.createIdentifier(value), + ts.factory.createIdentifier(`${namespace}_${value}`) + ) + })) + ), + ts.factory.createStringLiteral(importString), + undefined + ) + } + + const initialDeclarations: Statement[] = Object.entries(exportConfig) + .map(([namespace, { importUrl, exports }]) => createImport(exports, namespace, importUrl)); + + function createAssignment(namespace: string, variableName: string): ExpressionStatement { + return ts.factory.createExpressionStatement(ts.factory.createBinaryExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier("globalThis"), + ts.factory.createIdentifier(`${namespace}_${variableName}`) + ), + ts.factory.createToken(ts.SyntaxKind.EqualsToken), + ts.factory.createIdentifier(`${namespace}_${variableName}`) + )) + } + + const assignments: Statement[] = Object.entries(exportConfig) + .flatMap(([namespace, { exports }]) => exports.map(value => createAssignment(namespace, value))); + + return ts.factory.createSourceFile( + [ + ...initialDeclarations, + ...assignments, + ], + ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None + ) +} + +function generateExternal(namespace: string, exports: string[]): ts.SourceFile { + + const assignments = exports.map(value => { + + return ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList( + [ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(`${namespace}_${value}`), + undefined, + undefined, + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier("globalThis"), + ts.factory.createIdentifier(`${namespace}_${value}`) + ) + )], + ts.NodeFlags.Const + ) + ); + }); + + const exportDeclaration = ts.factory.createExportDeclaration( + undefined, + false, + ts.factory.createNamedExports(exports.map(value => { + return ts.factory.createExportSpecifier( + false, + ts.factory.createIdentifier(`${namespace}_${value}`), + ts.factory.createIdentifier(value) + ) + })), + undefined, + undefined + ); + + return ts.factory.createSourceFile( + [...assignments, exportDeclaration], + ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None + ) +} + +const outDir = `./dist`; + +if (!existsSync(outDir)) { + mkdirSync(outDir); +} + +const componentExports = collectExports(`../api/dist/gen/components.js`); +const helpersExports = collectExports(`../api/dist/helpers.js`); +const hooksExports = collectExports(`../api/dist/hooks.js`); +const coreExports = collectExports(`../core/dist/core.js`); + +// prod bundle exports are identical and hopefully it stays like this in future +const reactExports = collectExports(`../react/dist/dev/react.development.js`); +const reactJsxRuntimeExports = collectExports(`../react/dist/dev/react-jsx-runtime.development.js`); + +const internalAllExports = collectExports(`../core/dist/internal-all.js`); +const internalLinuxExports = collectExports(`../core/dist/internal-linux.js`); +const internalMacosExports = collectExports(`../core/dist/internal-macos.js`); + +generate( + `${outDir}/bridge-bootstrap.js`, + generateInternal({ + "GauntletComponents": { importUrl: "ext:gauntlet/api/components.js", exports: componentExports }, + "GauntletHelpers": { importUrl: "ext:gauntlet/api/helpers.js", exports: helpersExports }, + "GauntletHooks": { importUrl: "ext:gauntlet/api/hooks.js", exports: hooksExports }, + "GauntletCore": { importUrl: "ext:gauntlet/core.js", exports: coreExports }, + "GauntletReact": { importUrl: "ext:gauntlet/react.js", exports: reactExports }, + "GauntletReactJsxRuntime": { importUrl: "ext:gauntlet/react-jsx-runtime.js", exports: reactJsxRuntimeExports }, + }) +) + +generate(`${outDir}/bridge-internal-all-bootstrap.js`, generateInternal({ + "GauntletInternalAll": { importUrl: "ext:gauntlet/internal-all.js", exports: internalAllExports } +})) +generate(`${outDir}/bridge-internal-linux-bootstrap.js`, generateInternal({ + "GauntletInternalLinux": { importUrl: "ext:gauntlet/internal-linux.js", exports: internalLinuxExports } +})) +generate(`${outDir}/bridge-internal-macos-bootstrap.js`, generateInternal({ + "GauntletInternalMacos": { importUrl: "ext:gauntlet/internal-macos.js", exports: internalMacosExports } +})) + + +generate(`${outDir}/bridge-components.js`, generateExternal("GauntletComponents", componentExports)) +generate(`${outDir}/bridge-helpers.js`, generateExternal("GauntletHelpers", helpersExports)) +generate(`${outDir}/bridge-hooks.js`, generateExternal("GauntletHooks", hooksExports)) +generate(`${outDir}/bridge-core.js`, generateExternal("GauntletCore", coreExports)) +generate(`${outDir}/bridge-react.js`, generateExternal("GauntletReact", reactExports)) +generate(`${outDir}/bridge-react-jsx-runtime.js`, generateExternal("GauntletReactJsxRuntime", reactJsxRuntimeExports)) + +generate(`${outDir}/bridge-internal-all.js`, generateExternal("GauntletInternalAll", internalAllExports)) +generate(`${outDir}/bridge-internal-linux.js`, generateExternal("GauntletInternalLinux", internalLinuxExports)) +generate(`${outDir}/bridge-internal-macos.js`, generateExternal("GauntletInternalMacos", internalMacosExports)) + + diff --git a/js/bridge_build/tsconfig.json b/js/bridge_build/tsconfig.json new file mode 100644 index 0000000..1433d49 --- /dev/null +++ b/js/bridge_build/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react", + "types": ["@types/node"], + "outDir": "./dist" + }, + "lib": ["ES2020"], + "include": ["./src"] +} \ No newline at end of file diff --git a/js/build/package.json b/js/build/package.json index 0828044..5f5362b 100644 --- a/js/build/package.json +++ b/js/build/package.json @@ -23,9 +23,9 @@ "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.5", - "@types/node": "^18.17.1", + "@types/node": "^18.19.67", "@types/cross-spawn": "^6.0.6", "tslib": "^2.6.2", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/js/core/package.json b/js/core/package.json index 59445a3..29400bb 100644 --- a/js/core/package.json +++ b/js/core/package.json @@ -5,15 +5,16 @@ "build": "tsc --noEmit && rollup --config rollup.config.ts --configPlugin typescript" }, "devDependencies": { + "@project-gauntlet/api": "*", + "@project-gauntlet/deno": "*", + "@project-gauntlet/typings": "*", + "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.5", "@types/react": "^18.2.35", - "@project-gauntlet/api": "*", - "@project-gauntlet/typings": "*", - "@project-gauntlet/deno": "*", - "rollup": "^4.3.0", + "rollup": "^4.27.4", "tslib": "^2.6.2", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/js/core/rollup.config.ts b/js/core/rollup.config.ts index 68a33d4..9680de6 100644 --- a/js/core/rollup.config.ts +++ b/js/core/rollup.config.ts @@ -2,10 +2,15 @@ import nodeResolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import typescript from '@rollup/plugin-typescript'; import { defineConfig } from "rollup"; +import alias from '@rollup/plugin-alias'; export default defineConfig({ input: [ - 'src/init.tsx', + 'src/core.tsx', + 'src/init.ts', + 'src/internal-all.ts', + 'src/internal-linux.ts', + 'src/internal-macos.ts', ], output: [ { @@ -14,12 +19,18 @@ export default defineConfig({ sourcemap: 'inline', } ], - external: ["react", "react/jsx-runtime"], + external: [/^ext:.+/], plugins: [ nodeResolve(), commonjs(), typescript({ tsconfig: './tsconfig.json', }), + alias({ + entries: [ + { find: 'react/jsx-runtime', replacement: 'ext:gauntlet/react-jsx-runtime.js' }, + { find: 'react', replacement: 'ext:gauntlet/react.js' }, + ] + }), ] }) diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index 58e861c..239292e 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -1,9 +1,11 @@ +import { + fetch_action_id_for_shortcut, + get_command_generator_entrypoint_ids, + op_log_info, + update_loading_bar +} from "ext:core/ops"; import { reloadSearchIndex } from "./search-index"; -// @ts-expect-error does typescript support such symbol declarations? -const denoCore: DenoCore = Deno[Deno.internal].core; -const InternalApi = denoCore.ops; - interface GeneratedCommand { // TODO is it possible to import api here name: string icon?: ArrayBuffer @@ -46,15 +48,15 @@ export async function runCommandGenerators(): Promise { await reloadSearchIndex(true) - const entrypointIds = await InternalApi.get_command_generator_entrypoint_ids(); + const entrypointIds = await get_command_generator_entrypoint_ids(); for (const generatorEntrypointId of entrypointIds) { try { const generator: Generator = (await import(`gauntlet:entrypoint?${generatorEntrypointId}`)).default; - InternalApi.op_log_info("command_generator", `Running command generator entrypoint ${generatorEntrypointId}`) + op_log_info("command_generator", `Running command generator entrypoint ${generatorEntrypointId}`) const add = (id: string, data: GeneratedCommand) => { - InternalApi.op_log_info("command_generator", `Adding entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) + op_log_info("command_generator", `Adding entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) const lookupId = generatorEntrypointId + ":" + id; @@ -67,7 +69,7 @@ export async function runCommandGenerators(): Promise { reloadSearchIndex(true) } const remove = (id: string) => { - InternalApi.op_log_info("command_generator", `Removing entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) + op_log_info("command_generator", `Removing entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) const lookupId = generatorEntrypointId + ":" + id; delete storedGeneratedCommands[lookupId] @@ -78,9 +80,9 @@ export async function runCommandGenerators(): Promise { // noinspection ES6MissingAwait (async () => { try { - InternalApi.update_loading_bar(generatorEntrypointId, true) + update_loading_bar(generatorEntrypointId, true) let cleanup = await generator({ add, remove }) - InternalApi.update_loading_bar(generatorEntrypointId, false) + update_loading_bar(generatorEntrypointId, false) if (typeof cleanup === "function") { generatorCleanups[generatorEntrypointId] = cleanup } @@ -113,7 +115,7 @@ export async function runGeneratedCommandAction(entrypointId: string, key: strin const command = storedGeneratedCommands[entrypointId]; if (command) { - const id = await InternalApi.fetch_action_id_for_shortcut(command.generatorEntrypointId, key, modifierShift, modifierControl, modifierAlt, modifierMeta); + const id = await fetch_action_id_for_shortcut(command.generatorEntrypointId, key, modifierShift, modifierControl, modifierAlt, modifierMeta); if (id) { const action = command.command.actions?.find(value => value.ref == id); if (action) { diff --git a/js/core/src/init.tsx b/js/core/src/core.tsx similarity index 72% rename from js/core/src/init.tsx rename to js/core/src/core.tsx index 9300b17..e2c8ef5 100644 --- a/js/core/src/init.tsx +++ b/js/core/src/core.tsx @@ -1,11 +1,19 @@ -import { FC } from "react"; +import type { FC } from "react"; import { runCommandGenerators, runGeneratedCommand, runGeneratedCommandAction } from "./command-generator"; import { reloadSearchIndex } from "./search-index"; -import { clearRenderer } from "gauntlet:renderer"; - -// @ts-expect-error does typescript support such symbol declarations? -const denoCore: DenoCore = Deno[Deno.internal].core; -const InternalApi = denoCore.ops; +import { clearRenderer, render } from "ext:gauntlet/renderer.js"; +import { + clear_inline_view, + entrypoint_preferences_required, + fetch_action_id_for_shortcut, + op_inline_view_endpoint_id, + op_log_debug, + op_log_trace, + plugin_preferences_required, + show_plugin_error_view, + show_preferences_required_view, + op_plugin_get_pending_event +} from "ext:core/ops"; let latestRootUiWidget: UiWidget | undefined = undefined @@ -46,16 +54,16 @@ function findAllActionHandlers(widget: UiWidget): { id: string, onAction: () => } function handleEvent(event: ViewEvent) { - InternalApi.op_log_trace("plugin_event_handler", `Handling view event: ${Deno.inspect(event)}`); - InternalApi.op_log_trace("plugin_event_handler", `Root widget: ${Deno.inspect(latestRootUiWidget)}`); + op_log_trace("plugin_event_handler", `Handling view event: ${Deno.inspect(event)}`); + op_log_trace("plugin_event_handler", `Root widget: ${Deno.inspect(latestRootUiWidget)}`); if (latestRootUiWidget) { const widgetWithId = findWidgetWithId(latestRootUiWidget, event.widgetId); - InternalApi.op_log_trace("plugin_event_handler", `Found widget with id ${event.widgetId}: ${Deno.inspect(widgetWithId)}`) + op_log_trace("plugin_event_handler", `Found widget with id ${event.widgetId}: ${Deno.inspect(widgetWithId)}`) if (widgetWithId) { const property = widgetWithId.widgetProperties[event.eventName]; - InternalApi.op_log_trace("plugin_event_handler", `Found event handler with name ${event.eventName}: ${Deno.inspect(property)}`) + op_log_trace("plugin_event_handler", `Found event handler with name ${event.eventName}: ${Deno.inspect(property)}`) if (property) { if (typeof property === "function") { @@ -78,7 +86,7 @@ function handleEvent(event: ViewEvent) { } }); - InternalApi.op_log_trace("plugin_event_handler", `Calling handler with arguments ${Deno.inspect(eventArgs)}`) + op_log_trace("plugin_event_handler", `Calling handler with arguments ${Deno.inspect(eventArgs)}`) property(...eventArgs); } else { @@ -90,7 +98,7 @@ function handleEvent(event: ViewEvent) { } async function handleKeyboardEvent(event: NotReactsKeyboardEvent) { - InternalApi.op_log_trace("plugin_event_handler", `Handling keyboard event: ${Deno.inspect(event)}`); + op_log_trace("plugin_event_handler", `Handling keyboard event: ${Deno.inspect(event)}`); switch (event.origin) { case "MainView": { runGeneratedCommandAction(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta) @@ -100,7 +108,7 @@ async function handleKeyboardEvent(event: NotReactsKeyboardEvent) { if (latestRootUiWidget) { const actionHandlers = findAllActionHandlers(latestRootUiWidget); - const id = await InternalApi.fetch_action_id_for_shortcut(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta); + const id = await fetch_action_id_for_shortcut(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta); if (id) { const actionHandler = actionHandlers.find(value => value.id === id); @@ -116,31 +124,33 @@ async function handleKeyboardEvent(event: NotReactsKeyboardEvent) { } async function checkRequiredPreferences(entrypointId: string): Promise { - const pluginPreferencesRequired = InternalApi.plugin_preferences_required(); - const entrypointPreferencesRequired = InternalApi.entrypoint_preferences_required(entrypointId); + const pluginPreferencesRequired = plugin_preferences_required(); + const entrypointPreferencesRequired = entrypoint_preferences_required(entrypointId); return pluginPreferencesRequired || entrypointPreferencesRequired; } async function checkRequiredPreferencesAndAsk(entrypointId: string): Promise { - const pluginPreferencesRequired = await InternalApi.plugin_preferences_required(); - const entrypointPreferencesRequired = await InternalApi.entrypoint_preferences_required(entrypointId); + const pluginPreferencesRequired = await plugin_preferences_required(); + const entrypointPreferencesRequired = await entrypoint_preferences_required(entrypointId); const required = pluginPreferencesRequired || entrypointPreferencesRequired; if (required) { - InternalApi.show_preferences_required_view(entrypointId, pluginPreferencesRequired, entrypointPreferencesRequired) + show_preferences_required_view(entrypointId, pluginPreferencesRequired, entrypointPreferencesRequired) } return required; } -async function runLoop() { +export async function runPluginLoop() { + await runCommandGenerators(); + // runtime is stopped using tokio cancellation // noinspection InfiniteLoopJS while (true) { - InternalApi.op_log_trace("plugin_loop", "Waiting for next plugin event...") - const pluginEvent = await denoCore.opAsync("op_plugin_get_pending_event"); - InternalApi.op_log_trace("plugin_loop", `Received plugin event: ${Deno.inspect(pluginEvent)}`) + op_log_trace("plugin_loop", "Waiting for next plugin event...") + const pluginEvent = await op_plugin_get_pending_event(); + op_log_trace("plugin_loop", `Received plugin event: ${Deno.inspect(pluginEvent)}`) switch (pluginEvent.type) { case "ViewEvent": { try { @@ -165,11 +175,10 @@ async function runLoop() { } const View: FC = (await import(`gauntlet:entrypoint?${pluginEvent.entrypointId}`)).default; - const { render } = await import("gauntlet:renderer"); latestRootUiWidget = render(pluginEvent.entrypointId, "View", ); } catch (e) { console.error("Error occurred when rendering view", pluginEvent.entrypointId, e) - InternalApi.show_plugin_error_view(pluginEvent.entrypointId, "View") + show_plugin_error_view(pluginEvent.entrypointId, "View") } break; } @@ -199,7 +208,7 @@ async function runLoop() { break; } case "OpenInlineView": { - const endpointId = InternalApi.op_inline_view_endpoint_id(); + const endpointId = op_inline_view_endpoint_id(); if (endpointId) { if (await checkRequiredPreferences(endpointId)) { @@ -208,13 +217,12 @@ async function runLoop() { try { const Handler: FC<{ text: string }> = (await import(`gauntlet:entrypoint?${endpointId}`)).default; - const { render } = await import("gauntlet:renderer"); latestRootUiWidget = render(endpointId, "InlineView", ); if (latestRootUiWidget.widgetChildren.length === 0) { - InternalApi.op_log_debug("plugin_loop", `Inline view rendered no children, clearing inline view...`) - InternalApi.clear_inline_view() + op_log_debug("plugin_loop", `Inline view rendered no children, clearing inline view...`) + clear_inline_view() } } catch (e) { console.error("Error occurred when rendering inline view", e) @@ -234,15 +242,3 @@ async function runLoop() { } } } - -denoCore.setPromiseRejectCallback((_type, _promise, reason) => { - console.error("Rejected promise", reason) -}) - -reloadSearchIndex(true) - -runCommandGenerators(); - -(async () => { - await runLoop() -})(); diff --git a/js/core/src/init.ts b/js/core/src/init.ts new file mode 100644 index 0000000..4f6cfd4 --- /dev/null +++ b/js/core/src/init.ts @@ -0,0 +1,10 @@ +import { runPluginLoop } from "gauntlet:core"; + +globalThis.addEventListener("unhandledrejection", (event) => { + event.preventDefault() + console.error("Rejected promise", event); +}); + +(async () => { + await runPluginLoop() +})(); diff --git a/js/core/src/internal-all.ts b/js/core/src/internal-all.ts new file mode 100644 index 0000000..8175bda --- /dev/null +++ b/js/core/src/internal-all.ts @@ -0,0 +1,5 @@ +export { + run_numbat, + open_settings, + current_os, +} from "ext:core/ops"; diff --git a/js/core/src/internal-linux.ts b/js/core/src/internal-linux.ts new file mode 100644 index 0000000..8505a84 --- /dev/null +++ b/js/core/src/internal-linux.ts @@ -0,0 +1,5 @@ +export { + linux_app_from_path, + linux_application_dirs, + linux_open_application, +} from "ext:core/ops"; diff --git a/js/core/src/internal-macos.ts b/js/core/src/internal-macos.ts new file mode 100644 index 0000000..5c68d70 --- /dev/null +++ b/js/core/src/internal-macos.ts @@ -0,0 +1,12 @@ +export { + macos_app_from_arbitrary_path, + macos_app_from_path, + macos_application_dirs, + macos_major_version, + macos_open_application, + macos_open_setting_13_and_post, + macos_open_setting_pre_13, + macos_settings_13_and_post, + macos_settings_pre_13, + macos_system_applications, +} from "ext:core/ops"; diff --git a/js/core/src/search-index.ts b/js/core/src/search-index.ts index 31caaec..0ae9ba8 100644 --- a/js/core/src/search-index.ts +++ b/js/core/src/search-index.ts @@ -1,9 +1,6 @@ import { generatedCommandSearchIndex } from "./command-generator"; - -// @ts-expect-error does typescript support such symbol declarations? -const denoCore: DenoCore = Deno[Deno.internal].core; -const InternalApi = denoCore.ops; +import { reload_search_index } from "ext:core/ops"; export async function reloadSearchIndex(refreshSearchList: boolean) { - await InternalApi.reload_search_index(generatedCommandSearchIndex(), refreshSearchList); + await reload_search_index(generatedCommandSearchIndex(), refreshSearchList); } \ No newline at end of file diff --git a/js/core/typings/index.d.ts b/js/core/typings/index.d.ts index c17ef8c..a33cb66 100644 --- a/js/core/typings/index.d.ts +++ b/js/core/typings/index.d.ts @@ -1,7 +1,22 @@ -declare module "gauntlet:renderer" { +declare module "ext:gauntlet/renderer.js" { import { ReactNode } from "react"; - const render: (entrypointId: string, renderLocation: RenderLocation, component: ReactNode) => UiWidget; - const clearRenderer: () => void; - export { render, clearRenderer }; -} \ No newline at end of file + export const render: (entrypointId: string, renderLocation: RenderLocation, component: ReactNode) => UiWidget; + export const clearRenderer: () => void; +} + +declare module "gauntlet:core" { + export function runPluginLoop(): Promise; +} + +declare module "ext:gauntlet/api/components.js" { + +} + +declare module "ext:gauntlet/api/hooks.js" { + +} + +declare module "ext:gauntlet/api/helpers.js" { + +} diff --git a/js/deno/generator/index.ts b/js/deno/generator/index.ts index 8f3c6d7..c29e34d 100644 --- a/js/deno/generator/index.ts +++ b/js/deno/generator/index.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs"; -// https://github.com/denoland/deno/releases/tag/v1.36.4 -const LIB_DENO_DECLARATION_URL = "https://github.com/denoland/deno/releases/download/v1.36.4/lib.deno.d.ts"; +// https://github.com/denoland/deno/releases/tag/v2.1.1 +const LIB_DENO_DECLARATION_URL = "https://github.com/denoland/deno/releases/download/v2.1.1/lib.deno.d.ts"; const res = await fetch(LIB_DENO_DECLARATION_URL); const content = await res.text(); diff --git a/js/deno/package.json b/js/deno/package.json index 910dc3a..0ec399c 100644 --- a/js/deno/package.json +++ b/js/deno/package.json @@ -20,8 +20,8 @@ "run-generator-source": "tsc --project tsconfig.json && node builddist/index.js" }, "devDependencies": { - "@types/node": "^18.17.1", - "typescript": "^5.3.3" + "@types/node": "^18.19.67", + "typescript": "^5.7.2" }, "publishConfig": { "access": "public" diff --git a/js/react/package.json b/js/react/package.json index 5a0d165..0851866 100644 --- a/js/react/package.json +++ b/js/react/package.json @@ -9,10 +9,10 @@ }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-replace": "^6.0.1", "@rollup/plugin-typescript": "^11.1.5", - "rollup": "^4.3.0", + "rollup": "^4.27.4", "tslib": "^2.6.2", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/js/react/rollup.config.ts b/js/react/rollup.config.ts index 5df2085..5c2d6fa 100644 --- a/js/react/rollup.config.ts +++ b/js/react/rollup.config.ts @@ -1,6 +1,7 @@ import commonjs from '@rollup/plugin-commonjs'; import replace from "@rollup/plugin-replace"; import { defineConfig, RollupOptions } from "rollup"; +import alias from "@rollup/plugin-alias"; const fixedDevExports = ` @@ -54,7 +55,7 @@ const config = (nodeEnv: string, reactBundle: string, outDir: string): RollupOpt dir: outDir, format: 'esm', }, - external: ['react'], + external: [/^ext:.+/], plugins: [ commonjs(), replace({ @@ -65,7 +66,13 @@ const config = (nodeEnv: string, reactBundle: string, outDir: string): RollupOpt // To fix exports in development bundle https://github.com/rollup/plugins/issues/1546 'export { react_development as default };': fixedDevExports, } - }) + }), + alias({ + entries: [ + { find: 'react/jsx-runtime', replacement: 'ext:gauntlet/react-jsx-runtime.js' }, + { find: 'react', replacement: 'ext:gauntlet/react.js' }, + ] + }), ] } } diff --git a/js/react_renderer/package.json b/js/react_renderer/package.json index 9745046..a16067a 100644 --- a/js/react_renderer/package.json +++ b/js/react_renderer/package.json @@ -8,16 +8,17 @@ "react-reconciler": "^0.29.0" }, "devDependencies": { + "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-replace": "^6.0.1", "@rollup/plugin-typescript": "^11.1.5", "@types/react": "^18.2.35", "@types/react-reconciler": "^0.28.6", "@project-gauntlet/typings": "*", "@project-gauntlet/deno": "*", - "rollup": "^4.3.0", + "rollup": "^4.27.4", "tslib": "^2.6.2", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/js/react_renderer/rollup.config.ts b/js/react_renderer/rollup.config.ts index 41587a3..e44ffd9 100644 --- a/js/react_renderer/rollup.config.ts +++ b/js/react_renderer/rollup.config.ts @@ -3,6 +3,7 @@ import commonjs from '@rollup/plugin-commonjs'; import typescript from '@rollup/plugin-typescript'; import replace from "@rollup/plugin-replace"; import { defineConfig, RollupOptions } from "rollup"; +import alias from '@rollup/plugin-alias'; const config = (nodeEnv: string, outDir: string): RollupOptions => { return { @@ -16,7 +17,7 @@ const config = (nodeEnv: string, outDir: string): RollupOptions => { sourcemap: 'inline', } ], - external: ["react", "react/jsx-runtime"], + external: [/^ext:.+/], plugins: [ nodeResolve(), commonjs({ @@ -34,7 +35,13 @@ const config = (nodeEnv: string, outDir: string): RollupOptions => { '–': "-", '—': "-" } - }) + }), + alias({ + entries: [ + { find: 'react/jsx-runtime', replacement: 'ext:gauntlet/react-jsx-runtime.js' }, + { find: 'react', replacement: 'ext:gauntlet/react.js' }, + ] + }), ] } } diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index cdfc2f7..3ee53a0 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -1,10 +1,16 @@ import ReactReconciler, { HostConfig, OpaqueHandle } from "react-reconciler"; -import { createContext, FC, ReactNode, useContext } from 'react'; +import { createContext, ReactNode, useContext } from 'react'; import { DefaultEventPriority } from 'react-reconciler/constants'; - -// @ts-expect-error does typescript support such symbol declarations? -const denoCore: DenoCore = Deno[Deno.internal].core; -const InternalApi = denoCore.ops; +import { + asset_data, + asset_data_blocking, + get_entrypoint_preferences, + get_plugin_preferences, + op_component_model, + op_log_trace, + op_react_replace_view, + show_hud +} from "ext:core/ops"; // Usage of MessageChannel seems to block Deno runtime from exiting // causing plugin to be in stuck state where it is disabled but still have running runtime @@ -89,11 +95,11 @@ class GauntletContextValue { }; entrypointPreferences = () => { - return InternalApi.get_entrypoint_preferences(this.entrypointId()) + return get_entrypoint_preferences(this.entrypointId()) } pluginPreferences = () => { - return InternalApi.get_plugin_preferences() + return get_plugin_preferences() } } @@ -105,12 +111,12 @@ export function useGauntletContext() { } export async function getAssetData(path: string): Promise { - const vecU8 = await InternalApi.asset_data(path); + const vecU8 = await asset_data(path); return new Uint8Array(vecU8).buffer; // FIXME move array creation into rust if possible } export function getAssetDataSync(path: string): ArrayBuffer { - const vecU8 = InternalApi.asset_data_blocking(path); + const vecU8 = asset_data_blocking(path); return new Uint8Array(vecU8).buffer; } @@ -123,7 +129,7 @@ export function getEntrypointPreferences(): Record { } export function showHudWindow(display: string): void { - InternalApi.show_hud(display) + show_hud(display) } function createWidget(hostContext: HostContext, type: ComponentType, properties: Props, children: UiWidget[] = []): Instance { @@ -168,9 +174,9 @@ export const createHostConfig = (): HostConfig< hostContext: HostContext, _internalHandle: OpaqueHandle, ): Instance => { - InternalApi.op_log_trace("renderer_js_common", `createInstance is called, type: ${type}, props: ${Deno.inspect(props)}, rootContainer: ${Deno.inspect(rootContainer)}`) + op_log_trace("renderer_js_common", `createInstance is called, type: ${type}, props: ${Deno.inspect(props)}, rootContainer: ${Deno.inspect(rootContainer)}`) const instance = createWidget(hostContext, type, props) - InternalApi.op_log_trace("renderer_js_common", `createInstance returned, widget: ${Deno.inspect(instance)}`) + op_log_trace("renderer_js_common", `createInstance returned, widget: ${Deno.inspect(instance)}`) return instance; }, @@ -181,15 +187,15 @@ export const createHostConfig = (): HostConfig< hostContext: HostContext, _internalHandle: OpaqueHandle ): TextInstance => { - InternalApi.op_log_trace("renderer_js_common", `createTextInstance is called, text: ${text}, rootContainer: ${Deno.inspect(rootContainer)}`) + op_log_trace("renderer_js_common", `createTextInstance is called, text: ${text}, rootContainer: ${Deno.inspect(rootContainer)}`) const textInstance = createWidget(hostContext, "gauntlet:text_part", { value: text }) - InternalApi.op_log_trace("renderer_js_common", `createTextInstance returned, widget: ${Deno.inspect(textInstance)}`) + op_log_trace("renderer_js_common", `createTextInstance returned, widget: ${Deno.inspect(textInstance)}`) return textInstance; }, appendInitialChild: (parentInstance: Instance, child: Instance | TextInstance): void => { - InternalApi.op_log_trace("renderer_js_common", `appendInitialChild is called, parentInstance: ${Deno.inspect(parentInstance)}, child: ${Deno.inspect(child)}`) + op_log_trace("renderer_js_common", `appendInitialChild is called, parentInstance: ${Deno.inspect(parentInstance)}, child: ${Deno.inspect(child)}`) parentInstance.widgetChildren.push(child) }, @@ -201,7 +207,7 @@ export const createHostConfig = (): HostConfig< _rootContainer: RootUiWidget, _hostContext: HostContext ): boolean => { - InternalApi.op_log_trace("renderer_js_common", `finalizeInitialChildren is called, instance: ${Deno.inspect(instance)}, type: ${type}, props: ${Deno.inspect(props)}`) + op_log_trace("renderer_js_common", `finalizeInitialChildren is called, instance: ${Deno.inspect(instance)}, type: ${type}, props: ${Deno.inspect(props)}`) return false; }, @@ -213,16 +219,16 @@ export const createHostConfig = (): HostConfig< _rootContainer: RootUiWidget, _hostContext: HostContext, ): UpdatePayload | null => { - InternalApi.op_log_trace("renderer_js_common", `prepareUpdate is called, instance: ${Deno.inspect(instance)}, type: ${type}, oldProps: ${Deno.inspect(oldProps)}, newProps: ${Deno.inspect(newProps)}`) + op_log_trace("renderer_js_common", `prepareUpdate is called, instance: ${Deno.inspect(instance)}, type: ${type}, oldProps: ${Deno.inspect(oldProps)}, newProps: ${Deno.inspect(newProps)}`) const diff = shallowDiff(oldProps, newProps); - InternalApi.op_log_trace("renderer_js_common", `prepareUpdate shallowDiff returned: ${Deno.inspect(diff)}`) + op_log_trace("renderer_js_common", `prepareUpdate shallowDiff returned: ${Deno.inspect(diff)}`) return diff; }, shouldSetTextContent: (_type: ComponentType, _props: PropsWithChildren): boolean => { return false; }, getRootHostContext: (_rootContainer: RootUiWidget): HostContext | null => { - const componentModel = InternalApi.op_component_model(); + const componentModel = op_component_model(); return new HostContext(1, componentModel); }, @@ -286,7 +292,7 @@ export const createHostConfig = (): HostConfig< keepChildren: boolean, recyclableInstance: null | Instance, ): Instance { - InternalApi.op_log_trace("renderer_js_persistence", `cloneInstance is called, instance: ${Deno.inspect(instance)}, updatePayload: ${Deno.inspect(updatePayload)}, type: ${type}, oldProps: ${Deno.inspect(oldProps)}, newProps: ${Deno.inspect(newProps)}, keepChildren: ${keepChildren}, recyclableInstance: ${Deno.inspect(recyclableInstance)}`) + op_log_trace("renderer_js_persistence", `cloneInstance is called, instance: ${Deno.inspect(instance)}, updatePayload: ${Deno.inspect(updatePayload)}, type: ${type}, oldProps: ${Deno.inspect(oldProps)}, newProps: ${Deno.inspect(newProps)}, keepChildren: ${keepChildren}, recyclableInstance: ${Deno.inspect(recyclableInstance)}`) let clonedInstance: Instance; @@ -304,37 +310,37 @@ export const createHostConfig = (): HostConfig< } } - InternalApi.op_log_trace("renderer_js_persistence", `cloneInstance returned, widget: ${Deno.inspect(clonedInstance)}`) + op_log_trace("renderer_js_persistence", `cloneInstance returned, widget: ${Deno.inspect(clonedInstance)}`) return clonedInstance; }, createContainerChildSet(container: RootUiWidget): ChildSet { - InternalApi.op_log_trace("renderer_js_persistence", `createContainerChildSet is called, container: ${Deno.inspect(container)}`) + op_log_trace("renderer_js_persistence", `createContainerChildSet is called, container: ${Deno.inspect(container)}`) return [] }, appendChildToContainerChildSet(childSet: ChildSet, child: Instance | TextInstance): void { - InternalApi.op_log_trace("renderer_js_persistence", `appendChildToContainerChildSet is called, childSet: ${Deno.inspect(childSet)}, child: ${Deno.inspect(child)}`) + op_log_trace("renderer_js_persistence", `appendChildToContainerChildSet is called, childSet: ${Deno.inspect(childSet)}, child: ${Deno.inspect(child)}`) childSet.push(child); }, finalizeContainerChildren(container: RootUiWidget, newChildren: ChildSet): void { - InternalApi.op_log_trace("renderer_js_persistence", `finalizeContainerChildren is called, container: ${Deno.inspect(container)}, newChildren: ${Deno.inspect(newChildren)}`) + op_log_trace("renderer_js_persistence", `finalizeContainerChildren is called, container: ${Deno.inspect(container)}, newChildren: ${Deno.inspect(newChildren)}`) }, replaceContainerChildren(container: RootUiWidget, newChildren: ChildSet): void { - // InternalApi.op_log_info("renderer_js_persistence", `replaceContainerChildren is called, container: ${Deno.inspect(container)}, newChildren: ${Deno.inspect(newChildren, { depth: Number.MAX_VALUE })}`) + // op_log_info("renderer_js_persistence", `replaceContainerChildren is called, container: ${Deno.inspect(container)}, newChildren: ${Deno.inspect(newChildren, { depth: Number.MAX_VALUE })}`) container.widgetChildren = newChildren const containerComponent = { content: newChildren.map(value => convertComponents(value)) } - // InternalApi.op_log_info("renderer_js_persistence", `Converted container: ${Deno.inspect(containerComponent, { depth: Number.MAX_VALUE })}`) + // op_log_info("renderer_js_persistence", `Converted container: ${Deno.inspect(containerComponent, { depth: Number.MAX_VALUE })}`) - InternalApi.op_react_replace_view(gauntletContextValue.renderLocation(), gauntletContextValue.isBottommostView(), gauntletContextValue.entrypointId(), containerComponent) + op_react_replace_view(gauntletContextValue.renderLocation(), gauntletContextValue.isBottommostView(), gauntletContextValue.entrypointId(), containerComponent) }, cloneHiddenInstance( diff --git a/js/scenario_runner_cli/package.json b/js/scenario_runner_cli/package.json index 09b97d8..37b73bb 100644 --- a/js/scenario_runner_cli/package.json +++ b/js/scenario_runner_cli/package.json @@ -13,8 +13,8 @@ "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.5", - "@types/node": "^18.17.1", + "@types/node": "^18.19.67", "tslib": "^2.6.2", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index c190871..12db725 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -1,10 +1,40 @@ // js runtime types -interface DenoCore { - opAsync: (op: "op_plugin_get_pending_event") => Promise - setPromiseRejectCallback: (cb: PromiseRejectCallback) => undefined | PromiseRejectCallback; - ops: InternalApi +type DesktopPathAction = DesktopPathActionAdd | DesktopPathActionRemove + +type DesktopPathActionAdd = { + type: "add", + id: string, + data: DATA +} + +type DesktopPathActionRemove = { + type: "remove" + id: string +} + +type LinuxDesktopApplicationData = { + name: string + icon: ArrayBuffer | undefined, +} + +type MacOSDesktopApplicationData = { + name: string + path: string, + icon: ArrayBuffer | undefined, +} + +type MacOSDesktopSettingsPre13Data = { + name: string + path: string, + icon: ArrayBuffer | undefined, +} + +type MacOSDesktopSettings13AndPostData = { + name: string + preferences_id: string + icon: ArrayBuffer | undefined, } type PromiseRejectCallback = (type: number, promise: Promise, reason: any) => void; @@ -95,48 +125,97 @@ type AdditionalSearchItemAction = { label: string, } -interface InternalApi { - op_log_trace(target: string, message: string): void; - op_log_debug(target: string, message: string): void; - op_log_info(target: string, message: string): void; - op_log_warn(target: string, message: string): void; - op_log_error(target: string, message: string): void; +declare module "gauntlet:bridge/internal-all" { + function open_settings(): void + function run_numbat(input: string): { left: string, right: string } + function current_os(): string +} - op_component_model(): Record; - asset_data(path: string): Promise; - asset_data_blocking(path: string): number[]; +declare module "gauntlet:bridge/internal-linux" { + function linux_open_application(desktop_id: string): void + function linux_application_dirs(): string[] + function linux_app_from_path(path: string): Promise> - op_inline_view_endpoint_id(): string | null; - clear_inline_view(): void; +} - get_command_generator_entrypoint_ids(): Promise +declare module "gauntlet:bridge/internal-macos" { + function macos_major_version(): number + function macos_settings_pre_13(): MacOSDesktopSettingsPre13Data[] + function macos_settings_13_and_post(): MacOSDesktopSettings13AndPostData[] + function macos_open_setting_13_and_post(preferences_id: String): void + function macos_open_setting_pre_13(setting_path: String): void - get_plugin_preferences(): Record; - get_entrypoint_preferences(entrypointId: string): Record; - plugin_preferences_required(): Promise; - entrypoint_preferences_required(entrypointId: string): Promise; - show_preferences_required_view(entrypointId: string, pluginPreferencesRequired: boolean, entrypointPreferencesRequired: boolean): void; + function macos_system_applications(): string[] + function macos_application_dirs(): string[] + function macos_app_from_path(path: string): Promise> + function macos_app_from_arbitrary_path(path: string): Promise> + function macos_open_application(app_path: String): void +} - reload_search_index(searchItems: AdditionalSearchItem[], refreshSearchList: boolean): Promise; +declare module "ext:core/ops" { + function open_settings(): void + function run_numbat(input: string): { left: string, right: string } - show_hud(display: string): void; - update_loading_bar(entrypoint_id: string, show: boolean): void; + function current_os(): string - op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, entrypoint_id: string, container: any): void; - show_plugin_error_view(entrypoint_id: string, render_location: RenderLocation): void; + function linux_open_application(desktop_id: string): void + function linux_application_dirs(): string[] + function linux_app_from_path(path: string): Promise> - fetch_action_id_for_shortcut(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean): Promise; + function macos_major_version(): number + function macos_settings_pre_13(): MacOSDesktopSettingsPre13Data[] + function macos_settings_13_and_post(): MacOSDesktopSettings13AndPostData[] + function macos_open_setting_13_and_post(preferences_id: String): void + function macos_open_setting_pre_13(setting_path: String): void - clipboard_read(): Promise<{ text_data?: string, png_data?: Blob }>; - clipboard_read_text(): Promise; - clipboard_write(data: { text_data?: string, png_data?: number[] }): Promise; - clipboard_write_text(data: string): Promise; - clipboard_clear(): Promise; + function macos_system_applications(): string[] + function macos_application_dirs(): string[] + function macos_app_from_path(path: string): Promise> + function macos_app_from_arbitrary_path(path: string): Promise> + function macos_open_application(app_path: String): void - environment_gauntlet_version(): number; - environment_is_development(): boolean; - environment_plugin_data_dir(): string; - environment_plugin_cache_dir(): string; + function op_log_trace(target: string, message: string): void; + function op_log_debug(target: string, message: string): void; + function op_log_info(target: string, message: string): void; + function op_log_warn(target: string, message: string): void; + function op_log_error(target: string, message: string): void; + + function op_component_model(): Record; + function asset_data(path: string): Promise; + function asset_data_blocking(path: string): number[]; + + function op_inline_view_endpoint_id(): string | null; + function clear_inline_view(): void; + function op_plugin_get_pending_event(): Promise; + + function get_command_generator_entrypoint_ids(): Promise + + function get_plugin_preferences(): Record; + function get_entrypoint_preferences(entrypointId: string): Record; + function plugin_preferences_required(): Promise; + function entrypoint_preferences_required(entrypointId: string): Promise; + function show_preferences_required_view(entrypointId: string, pluginPreferencesRequired: boolean, entrypointPreferencesRequired: boolean): void; + + function reload_search_index(searchItems: AdditionalSearchItem[], refreshSearchList: boolean): Promise; + + function show_hud(display: string): void; + function update_loading_bar(entrypoint_id: string, show: boolean): void; + + function op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, entrypoint_id: string, container: any): void; + function show_plugin_error_view(entrypoint_id: string, render_location: RenderLocation): void; + + function fetch_action_id_for_shortcut(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean): Promise; + + function clipboard_read(): Promise<{ text_data?: string, png_data?: Blob }>; + function clipboard_read_text(): Promise; + function clipboard_write(data: { text_data?: string, png_data?: number[] }): Promise; + function clipboard_write_text(data: string): Promise; + function clipboard_clear(): Promise; + + function environment_gauntlet_version(): number; + function environment_is_development(): boolean; + function environment_plugin_data_dir(): string; + function environment_plugin_cache_dir(): string; } // component model types diff --git a/package-lock.json b/package-lock.json index 1175e4a..2d58a9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "js/react", "js/core", "js/react_renderer", + "js/bridge_build", "js/scenario_runner_cli" ] }, @@ -32,7 +33,7 @@ "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../tools", "@types/react": "^18.2.14", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "dev_plugin": { @@ -46,7 +47,7 @@ "@project-gauntlet/deno": "file:../js/deno", "@project-gauntlet/tools": "file:../tools", "@types/react": "^18.2.14", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "js/api": { @@ -54,15 +55,105 @@ "version": "0.11.0", "devDependencies": { "@project-gauntlet/typings": "*", - "typescript": "^5.3.3" + "@rollup/plugin-alias": "^5.1.1", + "@types/react": "^18.2.35", + "glob": "^11.0.0", + "rollup": "^4.27.4", + "tslib": "^2.6.2", + "typescript": "^5.7.2" } }, "js/api_build": { "name": "@project-gauntlet/api-build", "version": "0.1.0", "devDependencies": { - "@types/node": "^18.17.1", - "typescript": "^5.3.3" + "@types/node": "^18.19.67", + "typescript": "^5.7.2" + } + }, + "js/api/node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "js/api/node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "js/api/node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "js/api/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "js/api/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "js/bridge_build": { + "version": "0.1.0", + "devDependencies": { + "@types/node": "^18.19.67", + "typescript": "^5.7.2" } }, "js/build": { @@ -79,9 +170,9 @@ "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.5", "@types/cross-spawn": "^6.0.6", - "@types/node": "^18.17.1", + "@types/node": "^18.19.67", "tslib": "^2.6.2", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "js/core": { @@ -90,21 +181,22 @@ "@project-gauntlet/api": "*", "@project-gauntlet/deno": "*", "@project-gauntlet/typings": "*", + "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.5", "@types/react": "^18.2.35", - "rollup": "^4.3.0", + "rollup": "^4.27.4", "tslib": "^2.6.2", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "js/deno": { "name": "@project-gauntlet/deno", "version": "0.11.0", "devDependencies": { - "@types/node": "^18.17.1", - "typescript": "^5.3.3" + "@types/node": "^18.19.67", + "typescript": "^5.7.2" } }, "js/react": { @@ -114,11 +206,11 @@ }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-replace": "^6.0.1", "@rollup/plugin-typescript": "^11.1.5", - "rollup": "^4.3.0", + "rollup": "^4.27.4", "tslib": "^2.6.2", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "js/react_renderer": { @@ -129,15 +221,16 @@ "devDependencies": { "@project-gauntlet/deno": "*", "@project-gauntlet/typings": "*", + "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-replace": "^6.0.1", "@rollup/plugin-typescript": "^11.1.5", "@types/react": "^18.2.35", "@types/react-reconciler": "^0.28.6", - "rollup": "^4.3.0", + "rollup": "^4.27.4", "tslib": "^2.6.2", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "js/scenario_runner_cli": { @@ -149,9 +242,9 @@ "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.5", - "@types/node": "^18.17.1", + "@types/node": "^18.19.67", "tslib": "^2.6.2", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "js/typings": { @@ -1006,6 +1099,10 @@ "resolved": "js/api_build", "link": true }, + "node_modules/@project-gauntlet/bridge-build": { + "resolved": "js/bridge_build", + "link": true + }, "node_modules/@project-gauntlet/build": { "resolved": "js/build", "link": true @@ -1128,6 +1225,23 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@rollup/plugin-alias": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz", + "integrity": "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "25.0.7", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz", @@ -1197,9 +1311,9 @@ } }, "node_modules/@rollup/plugin-replace": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz", - "integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", + "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -1265,9 +1379,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.2.tgz", - "integrity": "sha512-OHflWINKtoCFSpm/WmuQaWW4jeX+3Qt3XQDepkkiFTsoxFc5BpF3Z5aDxFZgBqRjO6ATP5+b1iilp4kGIZVWlA==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.4.tgz", + "integrity": "sha512-2Y3JT6f5MrQkICUyRVCw4oa0sutfAsgaSsb0Lmmy1Wi2y7X5vT9Euqw4gOsCyy0YfKURBg35nhUKZS4mDcfULw==", "cpu": [ "arm" ], @@ -1277,9 +1391,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.2.tgz", - "integrity": "sha512-k0OC/b14rNzMLDOE6QMBCjDRm3fQOHAL8Ldc9bxEWvMo4Ty9RY6rWmGetNTWhPo+/+FNd1lsQYRd0/1OSix36A==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.4.tgz", + "integrity": "sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA==", "cpu": [ "arm64" ], @@ -1289,9 +1403,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.2.tgz", - "integrity": "sha512-IIARRgWCNWMTeQH+kr/gFTHJccKzwEaI0YSvtqkEBPj7AshElFq89TyreKNFAGh5frLfDCbodnq+Ye3dqGKPBw==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.4.tgz", + "integrity": "sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q==", "cpu": [ "arm64" ], @@ -1301,9 +1415,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.2.tgz", - "integrity": "sha512-52udDMFDv54BTAdnw+KXNF45QCvcJOcYGl3vQkp4vARyrcdI/cXH8VXTEv/8QWfd6Fru8QQuw1b2uNersXOL0g==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.4.tgz", + "integrity": "sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ==", "cpu": [ "x64" ], @@ -1312,10 +1426,34 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.4.tgz", + "integrity": "sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.4.tgz", + "integrity": "sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.2.tgz", - "integrity": "sha512-r+SI2t8srMPYZeoa1w0o/AfoVt9akI1ihgazGYPQGRilVAkuzMGiTtexNZkrPkQsyFrvqq/ni8f3zOnHw4hUbA==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.4.tgz", + "integrity": "sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==", "cpu": [ "arm" ], @@ -1325,9 +1463,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.2.tgz", - "integrity": "sha512-+tYiL4QVjtI3KliKBGtUU7yhw0GMcJJuB9mLTCEauHEsqfk49gtUBXGtGP3h1LW8MbaTY6rSFIQV1XOBps1gBA==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.4.tgz", + "integrity": "sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==", "cpu": [ "arm" ], @@ -1337,9 +1475,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.2.tgz", - "integrity": "sha512-OR5DcvZiYN75mXDNQQxlQPTv4D+uNCUsmSCSY2FolLf9W5I4DSoJyg7z9Ea3TjKfhPSGgMJiey1aWvlWuBzMtg==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.4.tgz", + "integrity": "sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==", "cpu": [ "arm64" ], @@ -1349,9 +1487,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.2.tgz", - "integrity": "sha512-Hw3jSfWdUSauEYFBSFIte6I8m6jOj+3vifLg8EU3lreWulAUpch4JBjDMtlKosrBzkr0kwKgL9iCfjA8L3geoA==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.4.tgz", + "integrity": "sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==", "cpu": [ "arm64" ], @@ -1361,9 +1499,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.2.tgz", - "integrity": "sha512-rhjvoPBhBwVnJRq/+hi2Q3EMiVF538/o9dBuj9TVLclo9DuONqt5xfWSaE6MYiFKpo/lFPJ/iSI72rYWw5Hc7w==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.4.tgz", + "integrity": "sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==", "cpu": [ "ppc64" ], @@ -1373,9 +1511,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.2.tgz", - "integrity": "sha512-EAz6vjPwHHs2qOCnpQkw4xs14XJq84I81sDRGPEjKPFVPBw7fwvtwhVjcZR6SLydCv8zNK8YGFblKWd/vRmP8g==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.4.tgz", + "integrity": "sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==", "cpu": [ "riscv64" ], @@ -1385,9 +1523,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.2.tgz", - "integrity": "sha512-IJSUX1xb8k/zN9j2I7B5Re6B0NNJDJ1+soezjNojhT8DEVeDNptq2jgycCOpRhyGj0+xBn7Cq+PK7Q+nd2hxLA==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.4.tgz", + "integrity": "sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==", "cpu": [ "s390x" ], @@ -1397,9 +1535,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.2.tgz", - "integrity": "sha512-OgaToJ8jSxTpgGkZSkwKE+JQGihdcaqnyHEFOSAU45utQ+yLruE1dkonB2SDI8t375wOKgNn8pQvaWY9kPzxDQ==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz", + "integrity": "sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==", "cpu": [ "x64" ], @@ -1409,9 +1547,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.2.tgz", - "integrity": "sha512-5V3mPpWkB066XZZBgSd1lwozBk7tmOkKtquyCJ6T4LN3mzKENXyBwWNQn8d0Ci81hvlBw5RoFgleVpL6aScLYg==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.4.tgz", + "integrity": "sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==", "cpu": [ "x64" ], @@ -1421,9 +1559,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.2.tgz", - "integrity": "sha512-ayVstadfLeeXI9zUPiKRVT8qF55hm7hKa+0N1V6Vj+OTNFfKSoUxyZvzVvgtBxqSb5URQ8sK6fhwxr9/MLmxdA==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.4.tgz", + "integrity": "sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==", "cpu": [ "arm64" ], @@ -1433,9 +1571,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.2.tgz", - "integrity": "sha512-Mda7iG4fOLHNsPqjWSjANvNZYoW034yxgrndof0DwCy0D3FvTjeNo+HGE6oGWgvcLZNLlcp0hLEFcRs+UGsMLg==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.4.tgz", + "integrity": "sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ==", "cpu": [ "ia32" ], @@ -1445,9 +1583,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.2.tgz", - "integrity": "sha512-DPi0ubYhSow/00YqmG1jWm3qt1F8aXziHc/UNy8bo9cpCacqhuWu+iSq/fp2SyEQK7iYTZ60fBU9cat3MXTjIQ==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.4.tgz", + "integrity": "sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug==", "cpu": [ "x64" ], @@ -1491,9 +1629,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@types/jsonwebtoken": { "version": "9.0.6", @@ -1509,9 +1647,9 @@ "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==" }, "node_modules/@types/node": { - "version": "18.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.17.tgz", - "integrity": "sha512-SzyGKgwPzuWp2SHhlpXKzCX0pIOfcI4V2eF37nNBJOhwlegQ83omtVQ1XxZpDE06V/d6AQvfQdPfnw0tRC//Ng==", + "version": "18.19.67", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", + "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -2471,11 +2609,11 @@ } }, "node_modules/rollup": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.2.tgz", - "integrity": "sha512-6/jgnN1svF9PjNYJ4ya3l+cqutg49vOZ4rVgsDKxdl+5gpGPnByFXWGyfH9YGx9i3nfBwSu1Iyu6vGwFFA0BdQ==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.4.tgz", + "integrity": "sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw==", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -2485,22 +2623,24 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.19.2", - "@rollup/rollup-android-arm64": "4.19.2", - "@rollup/rollup-darwin-arm64": "4.19.2", - "@rollup/rollup-darwin-x64": "4.19.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.19.2", - "@rollup/rollup-linux-arm-musleabihf": "4.19.2", - "@rollup/rollup-linux-arm64-gnu": "4.19.2", - "@rollup/rollup-linux-arm64-musl": "4.19.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.19.2", - "@rollup/rollup-linux-riscv64-gnu": "4.19.2", - "@rollup/rollup-linux-s390x-gnu": "4.19.2", - "@rollup/rollup-linux-x64-gnu": "4.19.2", - "@rollup/rollup-linux-x64-musl": "4.19.2", - "@rollup/rollup-win32-arm64-msvc": "4.19.2", - "@rollup/rollup-win32-ia32-msvc": "4.19.2", - "@rollup/rollup-win32-x64-msvc": "4.19.2", + "@rollup/rollup-android-arm-eabi": "4.27.4", + "@rollup/rollup-android-arm64": "4.27.4", + "@rollup/rollup-darwin-arm64": "4.27.4", + "@rollup/rollup-darwin-x64": "4.27.4", + "@rollup/rollup-freebsd-arm64": "4.27.4", + "@rollup/rollup-freebsd-x64": "4.27.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.27.4", + "@rollup/rollup-linux-arm-musleabihf": "4.27.4", + "@rollup/rollup-linux-arm64-gnu": "4.27.4", + "@rollup/rollup-linux-arm64-musl": "4.27.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.27.4", + "@rollup/rollup-linux-riscv64-gnu": "4.27.4", + "@rollup/rollup-linux-s390x-gnu": "4.27.4", + "@rollup/rollup-linux-x64-gnu": "4.27.4", + "@rollup/rollup-linux-x64-musl": "4.27.4", + "@rollup/rollup-win32-arm64-msvc": "4.27.4", + "@rollup/rollup-win32-ia32-msvc": "4.27.4", + "@rollup/rollup-win32-x64-msvc": "4.27.4", "fsevents": "~2.3.2" } }, @@ -2725,9 +2865,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2922,7 +3062,7 @@ "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", "@types/react": "^18.2.14", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "scenarios/plugins/docs_detail/node_modules/@project-gauntlet/api": { @@ -2946,7 +3086,7 @@ "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", "@types/react": "^18.2.14", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "scenarios/plugins/docs_form/node_modules/@project-gauntlet/api": { @@ -2970,7 +3110,7 @@ "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", "@types/react": "^18.2.14", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "scenarios/plugins/docs_grid/node_modules/@project-gauntlet/api": { @@ -2999,6 +3139,7 @@ } }, "scenarios/plugins/docs_inline_separators": { + "name": "@project-gauntlet/docs-inline-separators", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -3006,7 +3147,7 @@ "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", "@types/react": "^18.2.14", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "scenarios/plugins/docs_inline_separators/node_modules/@project-gauntlet/api": { @@ -3018,6 +3159,7 @@ "link": true }, "scenarios/plugins/docs_inline_three_sections": { + "name": "@project-gauntlet/docs-inline-three-sections", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -3025,7 +3167,7 @@ "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", "@types/react": "^18.2.14", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "scenarios/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/api": { @@ -3037,6 +3179,7 @@ "link": true }, "scenarios/plugins/docs_inline_two_sections": { + "name": "@project-gauntlet/docs-inline-two-sections", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -3044,7 +3187,7 @@ "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", "@types/react": "^18.2.14", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "scenarios/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/api": { @@ -3064,7 +3207,7 @@ "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", "@types/react": "^18.2.14", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "scenarios/plugins/docs_list/node_modules/@project-gauntlet/api": { @@ -3084,7 +3227,7 @@ }, "tools": { "name": "@project-gauntlet/tools", - "version": "0.7.0", + "version": "0.9.0", "dependencies": { "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", @@ -3093,7 +3236,7 @@ "@rollup/plugin-node-resolve": "^15.2.3", "chalk": "^5.3.0", "commander": "^12.1.0", - "rollup": "^4.19.0", + "rollup": "^4.27.4", "rollup-plugin-cleandir": "^2.0.0", "rollup-plugin-typescript2": "^0.36.0", "simple-git": "^3.25.0", @@ -3108,7 +3251,7 @@ "gauntlet": "bin/main.js" }, "devDependencies": { - "@types/node": "^18.17.1", + "@types/node": "^18.19.67", "@types/tail": "^2.2.3", "workspace-root": "^3.2.0" } diff --git a/package.json b/package.json index dd62cdd..a33500c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "js/react", "js/core", "js/react_renderer", + "js/bridge_build", "js/scenario_runner_cli" ] } diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index cee7216..2ae2e1f 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -5,13 +5,12 @@ edition = "2021" [dependencies] serde = { version = "1.0", features = ["derive"] } serde-value = "0.7.0" -deno_core = { version = "0.204.0" } -deno_runtime = { version = "0.126.0" } +deno_core = { version = "0.321.0" } # deno 2.1.1 +deno_runtime = { version = "0.188.0" } tokio = "1.28.1" tokio-util = "0.7.11" toml = "0.8.10" tantivy = "0.22.0" -zstd-sys = "=2.0.9" # TODO REMOVE https://github.com/gyscos/zstd-rs/issues/270 regex = "1.9.3" once_cell = "1.18.0" git2 = { version = "0.19.0", features = ["vendored-libgit2", "vendored-openssl"] } @@ -21,7 +20,7 @@ anyhow = { version = "1", features = ["backtrace"] } thiserror = "1.0.48" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -sqlx = { version = "0.7", features = [ "runtime-tokio", "json", "sqlite" ] } +sqlx = { version = "0.8.2", features = [ "runtime-tokio", "json", "sqlite" ] } common = { path = "../common" } utils = { path = "../utils" } component_model = { path = "../component_model" } @@ -44,9 +43,6 @@ scenario_runner = { path = "../scenario_runner", optional = true } itertools = "0.10.5" vergen-pretty = "0.3.5" -# TODO REMOVE when updating dependencies -async-channel = "=2.3.1" # because concurrent-queue version is incorrect in automatically pulled version - [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] libc = "0.2.153" diff --git a/rust/server/src/plugins/js/assets.rs b/rust/server/src/plugins/js/assets.rs index ca063b8..327ef28 100644 --- a/rust/server/src/plugins/js/assets.rs +++ b/rust/server/src/plugins/js/assets.rs @@ -1,12 +1,13 @@ -use std::cell::RefCell; -use std::rc::Rc; -use deno_core::{op, OpState}; -use deno_core::futures::executor::block_on; use crate::plugins::data_db_repository::DataDbRepository; use crate::plugins::js::PluginData; +use deno_core::futures::executor::block_on; +use deno_core::{op2, OpState}; +use std::cell::RefCell; +use std::rc::Rc; -#[op] -async fn asset_data(state: Rc>, path: String) -> anyhow::Result> { +#[op2(async)] +#[buffer] +pub async fn asset_data(state: Rc>, #[string] path: String) -> anyhow::Result> { let (plugin_id, repository) = { let state = state.borrow(); @@ -27,8 +28,9 @@ async fn asset_data(state: Rc>, path: String) -> anyhow::Result repository.get_asset_data(&plugin_id.to_string(), &path).await } -#[op] -fn asset_data_blocking(state: Rc>, path: String) -> anyhow::Result> { +#[op2] +#[buffer] +pub fn asset_data_blocking(state: Rc>, #[string] path: String) -> anyhow::Result> { let (plugin_id, repository) = { let state = state.borrow(); diff --git a/rust/server/src/plugins/js/clipboard.rs b/rust/server/src/plugins/js/clipboard.rs index 4fa4155..05ff427 100644 --- a/rust/server/src/plugins/js/clipboard.rs +++ b/rust/server/src/plugins/js/clipboard.rs @@ -5,7 +5,7 @@ use std::sync::{Arc, RwLock}; use std::time::Duration; use anyhow::{anyhow, Context, Error}; use arboard::ImageData; -use deno_core::{op, OpState}; +use deno_core::{op2, OpState}; use image::RgbaImage; use serde::{Deserialize, Serialize}; use tokio::task::spawn_blocking; @@ -159,8 +159,9 @@ struct ClipboardData { png_data: Option> } -#[op] -async fn clipboard_read(state: Rc>) -> anyhow::Result { +#[op2(async)] +#[serde] +pub async fn clipboard_read(state: Rc>) -> anyhow::Result { let clipboard = { let state = state.borrow(); @@ -189,8 +190,9 @@ async fn clipboard_read(state: Rc>) -> anyhow::Result>) -> anyhow::Result> { +#[op2(async)] +#[string] +pub async fn clipboard_read_text(state: Rc>) -> anyhow::Result> { let clipboard = { let state = state.borrow(); @@ -218,8 +220,8 @@ async fn clipboard_read_text(state: Rc>) -> anyhow::Result>, data: ClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2 +#[op2(async)] +pub async fn clipboard_write(state: Rc>, #[serde] data: ClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2 let clipboard = { let state = state.borrow(); @@ -247,8 +249,8 @@ async fn clipboard_write(state: Rc>, data: ClipboardData) -> an spawn_blocking(move || clipboard.write(data)).await? } -#[op] -async fn clipboard_write_text(state: Rc>, data: String) -> anyhow::Result<()> { +#[op2(async)] +pub async fn clipboard_write_text(state: Rc>, #[string] data: String) -> anyhow::Result<()> { let clipboard = { let state = state.borrow(); @@ -276,8 +278,8 @@ async fn clipboard_write_text(state: Rc>, data: String) -> anyh spawn_blocking(move || clipboard.write_text(data)).await? } -#[op] -async fn clipboard_clear(state: Rc>) -> anyhow::Result<()> { +#[op2(async)] +pub async fn clipboard_clear(state: Rc>) -> anyhow::Result<()> { let clipboard = { let state = state.borrow(); diff --git a/rust/server/src/plugins/js/command_generators.rs b/rust/server/src/plugins/js/command_generators.rs index 43a0f07..78acd03 100644 --- a/rust/server/src/plugins/js/command_generators.rs +++ b/rust/server/src/plugins/js/command_generators.rs @@ -1,12 +1,13 @@ use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginEntrypointType}; use crate::plugins::js::PluginData; -use deno_core::{op, OpState}; +use deno_core::{op2, OpState}; use std::cell::RefCell; use std::rc::Rc; -#[op] -async fn get_command_generator_entrypoint_ids(state: Rc>) -> anyhow::Result> { +#[op2(async)] +#[serde] +pub async fn get_command_generator_entrypoint_ids(state: Rc>) -> anyhow::Result> { let (plugin_id, repository) = { let state = state.borrow(); diff --git a/rust/server/src/plugins/js/environment.rs b/rust/server/src/plugins/js/environment.rs index 352cdb2..0700094 100644 --- a/rust/server/src/plugins/js/environment.rs +++ b/rust/server/src/plugins/js/environment.rs @@ -1,15 +1,15 @@ use crate::plugins::js::PluginData; -use deno_core::{op, OpState}; +use deno_core::{op2, OpState}; -#[op] -fn environment_gauntlet_version() -> u16 { +#[op2(fast)] +pub fn environment_gauntlet_version() -> u16 { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../VERSION")) .parse() .expect("version is not a number?") } -#[op] -fn environment_is_development(state: &mut OpState) -> bool { +#[op2(fast)] +pub fn environment_is_development(state: &mut OpState) -> bool { let plugin_id = state .borrow::() .plugin_id(); @@ -19,16 +19,18 @@ fn environment_is_development(state: &mut OpState) -> bool { .starts_with("file://") } -#[op] -fn environment_plugin_data_dir(state: &mut OpState) -> String { +#[op2] +#[string] +pub fn environment_plugin_data_dir(state: &mut OpState) -> String { state .borrow::() .plugin_data_dir() .to_string() } -#[op] -fn environment_plugin_cache_dir(state: &mut OpState) -> String { +#[op2] +#[string] +pub fn environment_plugin_cache_dir(state: &mut OpState) -> String { state .borrow::() .plugin_cache_dir() diff --git a/rust/server/src/plugins/js/logs.rs b/rust/server/src/plugins/js/logs.rs index 460c209..471d998 100644 --- a/rust/server/src/plugins/js/logs.rs +++ b/rust/server/src/plugins/js/logs.rs @@ -1,10 +1,10 @@ use std::cell::RefCell; use std::rc::Rc; -use deno_core::{op, OpState}; +use deno_core::{op2, OpState}; use crate::plugins::js::PluginData; -#[op] -fn op_log_trace(state: Rc>, target: String, message: String) -> anyhow::Result<()> { +#[op2(fast)] +pub fn op_log_trace(state: Rc>, #[string] target: String, #[string] message: String) -> anyhow::Result<()> { let plugin_id = state.borrow() .borrow::() .plugin_id() @@ -15,8 +15,8 @@ fn op_log_trace(state: Rc>, target: String, message: String) -> Ok(()) } -#[op] -fn op_log_debug(state: Rc>, target: String, message: String) -> anyhow::Result<()> { +#[op2(fast)] +pub fn op_log_debug(state: Rc>, #[string] target: String, #[string] message: String) -> anyhow::Result<()> { let plugin_id = state.borrow() .borrow::() .plugin_id() @@ -27,8 +27,8 @@ fn op_log_debug(state: Rc>, target: String, message: String) -> Ok(()) } -#[op] -fn op_log_info(state: Rc>, target: String, message: String) -> anyhow::Result<()> { +#[op2(fast)] +pub fn op_log_info(state: Rc>, #[string] target: String, #[string] message: String) -> anyhow::Result<()> { let plugin_id = state.borrow() .borrow::() .plugin_id() @@ -39,8 +39,8 @@ fn op_log_info(state: Rc>, target: String, message: String) -> Ok(()) } -#[op] -fn op_log_warn(state: Rc>, target: String, message: String) -> anyhow::Result<()> { +#[op2(fast)] +pub fn op_log_warn(state: Rc>, #[string] target: String, #[string] message: String) -> anyhow::Result<()> { let plugin_id = state.borrow() .borrow::() .plugin_id() @@ -51,8 +51,8 @@ fn op_log_warn(state: Rc>, target: String, message: String) -> Ok(()) } -#[op] -fn op_log_error(state: Rc>, target: String, message: String) -> anyhow::Result<()> { +#[op2(fast)] +pub fn op_log_error(state: Rc>, #[string] target: String, #[string] message: String) -> anyhow::Result<()> { let plugin_id = state.borrow() .borrow::() .plugin_id() diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index bd1b5e0..7063fc2 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -7,19 +7,21 @@ use std::path::{Path, PathBuf}; use std::pin::Pin; use std::rc::Rc; use std::str::FromStr; +use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, Context}; use deno_core::futures::executor::block_on; use deno_core::futures::{FutureExt, Stream, StreamExt}; use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; -use deno_core::{futures, op, serde_v8, v8, FastString, ModuleLoader, ModuleSource, ModuleSourceFuture, ModuleType, OpState, ResolutionKind, StaticModuleLoader}; +use deno_core::{futures, op2, serde_v8, v8, FastString, ModuleLoadResponse, ModuleLoader, ModuleSource, ModuleSourceCode, ModuleSourceFuture, ModuleType, OpState, RequestedModuleType, ResolutionKind, StaticModuleLoader}; +use deno_core::url::Url; use deno_runtime::deno_core::ModuleSpecifier; use deno_runtime::deno_io::{Stdio, StdioPipe}; -use deno_runtime::permissions::{Descriptor, EnvDescriptor, NetDescriptor, Permissions, PermissionsContainer, PermissionsOptions, ReadDescriptor, UnaryPermission, WriteDescriptor}; -use deno_runtime::worker::MainWorker; +use deno_runtime::worker::{MainWorker, WorkerServiceOptions}; use deno_runtime::worker::WorkerOptions; use deno_runtime::BootstrapOptions; +use deno_runtime::deno_fs::{FileSystem, FileSystemRc, RealFs}; use indexmap::IndexMap; use once_cell::sync::Lazy; use regex::Regex; @@ -41,6 +43,7 @@ use crate::plugins::js::command_generators::{get_command_generator_entrypoint_id use crate::plugins::js::environment::{environment_gauntlet_version, environment_is_development, environment_plugin_cache_dir, environment_plugin_data_dir}; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; use crate::plugins::js::permissions::{permissions_to_deno, PluginPermissions, PluginPermissionsClipboard}; +use crate::plugins::js::plugins::applications::current_os; use crate::plugins::js::plugins::numbat::{run_numbat, NumbatContext}; use crate::plugins::js::plugins::settings::open_settings; use crate::plugins::js::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; @@ -298,8 +301,8 @@ async fn start_js_runtime( dirs: Dirs, clipboard: Clipboard, ) -> anyhow::Result<()> { - - let dev_plugin = plugin_id.to_string().starts_with("file://"); + let plugin_id_str = plugin_id.to_string(); + let dev_plugin = plugin_id_str.starts_with("file://"); let (stdout, stderr) = if dev_plugin { let (out_log_file, err_log_file) = dirs.plugin_log_files(&plugin_uuid); @@ -310,75 +313,101 @@ async fn start_js_runtime( let out_log_file = File::create(out_log_file)?; let err_log_file = File::create(err_log_file)?; - (StdioPipe::File(out_log_file), StdioPipe::File(err_log_file)) + (StdioPipe::file(out_log_file), StdioPipe::file(err_log_file)) } else { - (StdioPipe::Inherit, StdioPipe::Inherit) + (StdioPipe::inherit(), StdioPipe::inherit()) }; let local_storage_dir = dirs.plugin_local_storage(&plugin_uuid); let plugin_cache_dir = dirs.plugin_cache(&plugin_uuid)?.to_str().expect("non-uft8 paths are not supported").to_string(); let plugin_data_dir = dirs.plugin_data(&plugin_uuid)?.to_str().expect("non-uft8 paths are not supported").to_string(); - // let _inspector_server = Arc::new( - // InspectorServer::new( - // "127.0.0.1:9229".parse::().unwrap(), - // "test", - // ) - // ); + let init_url: ModuleSpecifier = "gauntlet:init".parse().expect("should be valid"); - let core_url = "gauntlet:core".parse().expect("should be valid"); - let unused_url = "gauntlet:unused".parse().expect("should be valid"); - - let numbat_context = if plugin_id.to_string() == "bundled://gauntlet" { + let numbat_context = if plugin_id_str == "bundled://gauntlet" { Some(NumbatContext::new()) } else { None }; - let permissions_container = permissions_to_deno(&permissions, &dirs, &plugin_uuid)?; + let fs: Arc = Arc::new(RealFs); + + let permissions_container = permissions_to_deno(fs.clone(), &permissions, &dirs, &plugin_uuid)?; let runtime_permissions = PluginRuntimePermissions { clipboard: permissions.clipboard, }; + let gauntlet_esm = if cfg!(feature = "release") && !dev_plugin { + prod::gauntlet_esm::init_ops_and_esm() + } else { + dev::gauntlet_esm::init_ops_and_esm() + }; + + let mut extensions = vec![ + gauntlet::init_ops( + EventReceiver::new(event_stream), + PluginData::new( + plugin_id, + plugin_uuid, + plugin_name, + entrypoint_names, + plugin_cache_dir, + plugin_data_dir, + inline_view_entrypoint_id, + runtime_permissions, + ), + frontend_api, + ComponentModel::new(component_model), + repository, + search_index, + icon_cache, + numbat_context, + clipboard, + ), + gauntlet_esm, + ]; + + if plugin_id_str == "bundled://gauntlet" { + extensions.push(gauntlet_internal_all::init_ops_and_esm()); + + #[cfg(target_os = "macos")] + extensions.push(gauntlet_internal_macos::init_ops_and_esm()); + + #[cfg(target_os = "linux")] + extensions.push(gauntlet_internal_linux::init_ops_and_esm()); + } + let mut worker = MainWorker::bootstrap_from_options( - unused_url, - permissions_container, + init_url.clone(), + WorkerServiceOptions { + blob_store: Arc::new(Default::default()), + broadcast_channel: Default::default(), + feature_checker: Arc::new(Default::default()), + fs, + module_loader: Rc::new(CustomModuleLoader::new(code, dev_plugin)), + node_services: None, + npm_process_state_provider: None, + permissions: permissions_container, + root_cert_store_provider: None, + fetch_dns_resolver: Default::default(), + shared_array_buffer_store: None, + compiled_wasm_module_store: None, + v8_code_cache: None, + }, WorkerOptions { bootstrap: BootstrapOptions { - is_tty: false, + is_stderr_tty: false, + is_stdout_tty: false, ..Default::default() }, - module_loader: Rc::new(CustomModuleLoader::new(code, dev_plugin)), - extensions: vec![plugin_ext::init_ops_and_esm( - EventReceiver::new(event_stream), - PluginData::new( - plugin_id, - plugin_uuid, - plugin_name, - entrypoint_names, - plugin_cache_dir, - plugin_data_dir, - inline_view_entrypoint_id, - runtime_permissions, - ), - frontend_api, - ComponentModel::new(component_model), - repository, - search_index, - icon_cache, - numbat_context, - clipboard, - )], - // maybe_inspector_server: Some(inspector_server.clone()), - // should_wait_for_inspector_session: true, - // should_break_on_first_statement: true, + extensions, maybe_inspector_server: None, should_wait_for_inspector_session: false, should_break_on_first_statement: false, origin_storage_dir: Some(local_storage_dir), stdio: Stdio { - stdin: StdioPipe::Inherit, + stdin: StdioPipe::inherit(), stdout, stderr, }, @@ -386,7 +415,7 @@ async fn start_js_runtime( }, ); - worker.execute_side_module(&core_url).await?; + worker.execute_main_module(&init_url).await?; worker.run_event_loop(false).await?; Ok(()) @@ -412,16 +441,16 @@ impl CustomModuleLoader { } const MODULES: [(&str, &str); 10] = [ - ("gauntlet:renderer:prod", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/react_renderer/dist/prod/renderer.js"))), - ("gauntlet:renderer:dev", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/react_renderer/dist/dev/renderer.js"))), - ("gauntlet:react:prod", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/react/dist/prod/react.production.min.js"))), - ("gauntlet:react:dev", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/react/dist/dev/react.development.js"))), - ("gauntlet:react-jsx-runtime:prod", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/react/dist/prod/react-jsx-runtime.production.min.js"))), - ("gauntlet:react-jsx-runtime:dev", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/react/dist/dev/react-jsx-runtime.development.js"))), - ("gauntlet:core", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/core/dist/init.js"))), - ("gauntlet:api-components", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/api/dist/gen/components.js"))), - ("gauntlet:api-hooks", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/api/dist/hooks.js"))), - ("gauntlet:api-helpers", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/api/dist/helpers.js"))), + ("gauntlet:init", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/core/dist/init.js"))), + ("gauntlet:bridge/components", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-components.js"))), + ("gauntlet:bridge/hooks", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-hooks.js"))), + ("gauntlet:bridge/helpers", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-helpers.js"))), + ("gauntlet:bridge/core", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-core.js"))), + ("gauntlet:bridge/react", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-react.js"))), + ("gauntlet:bridge/react-jsx-runtime", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-react-jsx-runtime.js"))), + ("gauntlet:bridge/internal-all", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-all.js"))), + ("gauntlet:bridge/internal-linux", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-linux.js"))), + ("gauntlet:bridge/internal-macos", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-macos.js"))), ]; impl ModuleLoader for CustomModuleLoader { @@ -429,7 +458,7 @@ impl ModuleLoader for CustomModuleLoader { &self, specifier: &str, referrer: &str, - kind: ResolutionKind, + _kind: ResolutionKind, ) -> Result { static PLUGIN_ENTRYPOINT_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^gauntlet:entrypoint\?(?[a-zA-Z0-9_-]+)$").expect("invalid regex")); static PLUGIN_MODULE_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^gauntlet:module\?(?[a-zA-Z0-9_-]+)$").expect("invalid regex")); @@ -445,56 +474,69 @@ impl ModuleLoader for CustomModuleLoader { } } - let prod_react = cfg!(feature = "release") && !self.dev_plugin; - let specifier = match (specifier, referrer) { - ("gauntlet:core", _) => "gauntlet:core", - ("gauntlet:renderer", _) => if prod_react { "gauntlet:renderer:prod" } else { "gauntlet:renderer:dev" }, - ("react", _) => if prod_react { "gauntlet:react:prod" } else { "gauntlet:react:dev" }, - ("react/jsx-runtime", _) => if prod_react { "gauntlet:react-jsx-runtime:prod" } else { "gauntlet:react-jsx-runtime:dev" }, - ("@project-gauntlet/api/components", _) => "gauntlet:api-components", - ("@project-gauntlet/api/hooks", _) => "gauntlet:api-hooks", - ("@project-gauntlet/api/helpers", _) => "gauntlet:api-helpers", + ("gauntlet:init", _) => "gauntlet:init", + ("gauntlet:core", _) => "gauntlet:bridge/core", + ("gauntlet:bridge/internal-all", _) => "gauntlet:bridge/internal-all", + ("gauntlet:bridge/internal-linux", _) => "gauntlet:bridge/internal-linux", + ("gauntlet:bridge/internal-macos", _) => "gauntlet:bridge/internal-macos", + ("react", _) => "gauntlet:bridge/react", + ("react/jsx-runtime", _) => "gauntlet:bridge/react-jsx-runtime", + ("@project-gauntlet/api/components", _) => "gauntlet:bridge/components", + ("@project-gauntlet/api/hooks", _) => "gauntlet:bridge/hooks", + ("@project-gauntlet/api/helpers", _) => "gauntlet:bridge/helpers", _ => { - return Err(anyhow!("Could not resolve module with specifier: {} and referrer: {}", specifier, referrer)); + return Err(anyhow!("Illegal import with specifier '{}' and referrer '{}'", specifier, referrer)) } }; - self.static_loader.resolve(specifier, referrer, kind) + Ok(Url::parse(specifier)?) } fn load( &self, module_specifier: &ModuleSpecifier, maybe_referrer: Option<&ModuleSpecifier>, - is_dynamic: bool, - ) -> Pin> { + is_dyn_import: bool, + requested_module_type: RequestedModuleType, + ) -> ModuleLoadResponse { + let mut specifier = module_specifier.clone(); specifier.set_query(None); - if &specifier == &"gauntlet:entrypoint".parse().unwrap() || &specifier == &"gauntlet:module".parse().unwrap() { - let module = get_js_code(module_specifier, &self.code.js); + match specifier.as_str() { + "gauntlet:init" => { + self.static_loader.load(module_specifier, maybe_referrer, is_dyn_import, requested_module_type) + } + "gauntlet:entrypoint" | "gauntlet:module" => { + match module_specifier.query() { + None => { + ModuleLoadResponse::Sync(Err(anyhow!("Module specifier doesn't have query part"))) + }, + Some(entrypoint_id) => { + let result = self.code.js + .get(entrypoint_id) + .ok_or(anyhow!("Cannot find JS code path: {:?}", entrypoint_id)) + .map(|js| ModuleSourceCode::String(js.clone().into())) + .map(|js| ModuleSource::new(ModuleType::JavaScript, js, module_specifier, None)); - return futures::future::ready(module).boxed_local(); + ModuleLoadResponse::Sync(result) + } + } + } + _ => { + if specifier.as_str().starts_with("gauntlet:bridge/"){ + self.static_loader.load(module_specifier, maybe_referrer, is_dyn_import, requested_module_type) + } else { + ModuleLoadResponse::Sync(Err(anyhow!("Module not found: specifier '{}' and referrer '{:?}'", specifier, maybe_referrer.map(|url| url.as_str())))) + } + } } - - self.static_loader.load(module_specifier, maybe_referrer, is_dynamic) } } -fn get_js_code(module_specifier: &ModuleSpecifier, js: &HashMap) -> anyhow::Result { - let entrypoint_id = module_specifier.query().expect("invalid specifier, should be validated earlier"); - - let js = js.get(entrypoint_id).ok_or(anyhow!("no code provided for view: {:?}", entrypoint_id))?; - - let module = ModuleSource::new(ModuleType::JavaScript, js.clone().into(), module_specifier); - - Ok(module) -} - - deno_core::extension!( - plugin_ext, + gauntlet, ops = [ // core op_plugin_get_pending_event, @@ -540,45 +582,6 @@ deno_core::extension!( clipboard_write_text, clipboard_clear, - // plugins numbat - run_numbat, - - // plugins applications - crate::plugins::js::plugins::applications::current_os, - - // plugins applications linux - #[cfg(target_os = "linux")] - crate::plugins::js::plugins::applications::linux_app_from_path, - #[cfg(target_os = "linux")] - crate::plugins::js::plugins::applications::linux_application_dirs, - #[cfg(target_os = "linux")] - crate::plugins::js::plugins::applications::linux_open_application, - - // plugins applications macos - #[cfg(target_os = "macos")] - crate::plugins::js::plugins::applications::macos_major_version, - #[cfg(target_os = "macos")] - crate::plugins::js::plugins::applications::macos_settings_pre_13, - #[cfg(target_os = "macos")] - crate::plugins::js::plugins::applications::macos_settings_13_and_post, - #[cfg(target_os = "macos")] - crate::plugins::js::plugins::applications::macos_open_setting_13_and_post, - #[cfg(target_os = "macos")] - crate::plugins::js::plugins::applications::macos_open_setting_pre_13, - #[cfg(target_os = "macos")] - crate::plugins::js::plugins::applications::macos_system_applications, - #[cfg(target_os = "macos")] - crate::plugins::js::plugins::applications::macos_application_dirs, - #[cfg(target_os = "macos")] - crate::plugins::js::plugins::applications::macos_app_from_arbitrary_path, - #[cfg(target_os = "macos")] - crate::plugins::js::plugins::applications::macos_app_from_path, - #[cfg(target_os = "macos")] - crate::plugins::js::plugins::applications::macos_open_application, - - // plugins settings - open_settings, - // plugin environment environment_gauntlet_version, environment_is_development, @@ -609,8 +612,102 @@ deno_core::extension!( }, ); -#[op] -async fn op_plugin_get_pending_event(state: Rc>) -> anyhow::Result { +mod prod { + deno_core::extension!( + gauntlet_esm, + esm_entry_point = "ext:gauntlet/bootstrap.js", + esm = [ + "ext:gauntlet/bootstrap.js" = "../../js/bridge_build/dist/bridge-bootstrap.js", + "ext:gauntlet/core.js" = "../../js/core/dist/core.js", + "ext:gauntlet/api/components.js" = "../../js/api/dist/gen/components.js", + "ext:gauntlet/api/hooks.js" = "../../js/api/dist/hooks.js", + "ext:gauntlet/api/helpers.js" = "../../js/api/dist/helpers.js", + "ext:gauntlet/renderer.js" = "../../js/react_renderer/dist/prod/renderer.js", + "ext:gauntlet/react.js" = "../../js/react/dist/prod/react.production.min.js", + "ext:gauntlet/react-jsx-runtime.js" = "../../js/react/dist/prod/react-jsx-runtime.production.min.js", + ], + ); +} + +#[allow(long_running_const_eval)] // dev renderer is 22K line file which triggers rust lint +mod dev { + deno_core::extension!( + gauntlet_esm, + esm_entry_point = "ext:gauntlet/bootstrap.js", + esm = [ + "ext:gauntlet/bootstrap.js" = "../../js/bridge_build/dist/bridge-bootstrap.js", + "ext:gauntlet/core.js" = "../../js/core/dist/core.js", + "ext:gauntlet/api/components.js" = "../../js/api/dist/gen/components.js", + "ext:gauntlet/api/hooks.js" = "../../js/api/dist/hooks.js", + "ext:gauntlet/api/helpers.js" = "../../js/api/dist/helpers.js", + "ext:gauntlet/renderer.js" = "../../js/react_renderer/dist/dev/renderer.js", + "ext:gauntlet/react.js" = "../../js/react/dist/dev/react.development.js", + "ext:gauntlet/react-jsx-runtime.js" = "../../js/react/dist/dev/react-jsx-runtime.development.js", + ], + ); +} + +deno_core::extension!( + gauntlet_internal_all, + ops = [ + // plugins numbat + run_numbat, + + // plugins applications + current_os, + + // plugins settings + open_settings, + ], + esm_entry_point = "ext:gauntlet/internal-all/bootstrap.js", + esm = [ + "ext:gauntlet/internal-all/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-all-bootstrap.js", + "ext:gauntlet/internal-all.js" = "../../js/core/dist/internal-all.js", + ] +); + +#[cfg(target_os = "linux")] +deno_core::extension!( + gauntlet_internal_linux, + ops = [ + // plugins applications linux + crate::plugins::js::plugins::applications::linux_app_from_path, + crate::plugins::js::plugins::applications::linux_application_dirs, + crate::plugins::js::plugins::applications::linux_open_application, + ], + esm_entry_point = "ext:gauntlet/internal-linux/bootstrap.js", + esm = [ + "ext:gauntlet/internal-linux/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-linux-bootstrap.js", + "ext:gauntlet/internal-linux.js" = "../../js/core/dist/internal-linux.js", + ] +); + +#[cfg(target_os = "macos")] +deno_core::extension!( + gauntlet_internal_macos, + ops = [ + // plugins applications macos + crate::plugins::js::plugins::applications::macos_major_version, + crate::plugins::js::plugins::applications::macos_settings_pre_13, + crate::plugins::js::plugins::applications::macos_settings_13_and_post, + crate::plugins::js::plugins::applications::macos_open_setting_13_and_post, + crate::plugins::js::plugins::applications::macos_open_setting_pre_13, + crate::plugins::js::plugins::applications::macos_system_applications, + crate::plugins::js::plugins::applications::macos_application_dirs, + crate::plugins::js::plugins::applications::macos_app_from_arbitrary_path, + crate::plugins::js::plugins::applications::macos_app_from_path, + crate::plugins::js::plugins::applications::macos_open_application, + ], + esm_entry_point = "ext:gauntlet/internal-macos/bootstrap.js", + esm = [ + "ext:gauntlet/internal-macos/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-macos-bootstrap.js", + "ext:gauntlet/internal-macos.js" = "../../js/core/dist/internal-macos.js", + ] +); + +#[op2(async)] +#[serde] +pub async fn op_plugin_get_pending_event(state: Rc>) -> anyhow::Result { let event_stream = { state.borrow() .borrow::() diff --git a/rust/server/src/plugins/js/permissions.rs b/rust/server/src/plugins/js/permissions.rs index bca14dd..2bf85ab 100644 --- a/rust/server/src/plugins/js/permissions.rs +++ b/rust/server/src/plugins/js/permissions.rs @@ -1,12 +1,15 @@ -use deno_runtime::permissions::{Descriptor, EnvDescriptor, NetDescriptor, Permissions, PermissionsContainer, ReadDescriptor, RunDescriptor, SysDescriptor, UnaryPermission, WriteDescriptor}; use std::collections::HashSet; use std::hash::Hash; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::str::FromStr; +use std::sync::Arc; use anyhow::anyhow; +use deno_runtime::deno_fs::{FileSystemRc, RealFs}; +use deno_runtime::deno_permissions::{AllowRunDescriptor, EnvDescriptor, EnvQueryDescriptor, NetDescriptor, Permissions, PermissionsContainer, QueryDescriptor, ReadDescriptor, RunQueryDescriptor, SysDescriptor, SysDescriptorParseError, UnaryPermission, WriteDescriptor}; +use deno_runtime::permissions::RuntimePermissionDescriptorParser; +use tonic::codegen::Body; use typed_path::Utf8TypedPath; use common::dirs::Dirs; -use common::model::PluginId; use crate::plugins::loader::VARIABLE_PATTERN; pub struct PluginPermissions { @@ -41,26 +44,30 @@ pub enum PluginPermissionsMainSearchBar { Read, } -pub fn permissions_to_deno(permissions: &PluginPermissions, dirs: &Dirs, plugin_uuid: &str) -> anyhow::Result { - Ok(PermissionsContainer::new(Permissions { - read: path_permission(&permissions.filesystem.read, ReadDescriptor, dirs, plugin_uuid)?, - write: path_permission(&permissions.filesystem.write, WriteDescriptor, dirs, plugin_uuid)?, - net: net_permission(&permissions.network), - env: env_permission(&permissions.environment), - sys: sys_permission(&permissions.system), - run: run_permission(&permissions.exec, dirs, plugin_uuid)?, - ffi: Permissions::new_ffi(&None, &None, false).expect("new_ffi should always succeed"), - hrtime: Permissions::new_hrtime(true, false), - })) +pub fn permissions_to_deno(fs: FileSystemRc, permissions: &PluginPermissions, dirs: &Dirs, plugin_uuid: &str) -> anyhow::Result { + Ok(PermissionsContainer::new( + Arc::new(RuntimePermissionDescriptorParser::new(fs)), + Permissions { + read: path_permission(&permissions.filesystem.read, ReadDescriptor, dirs, plugin_uuid)?, + write: path_permission(&permissions.filesystem.write, WriteDescriptor, dirs, plugin_uuid)?, + net: net_permission(&permissions.network), + env: env_permission(&permissions.environment), + sys: sys_permission(&permissions.system)?, + run: run_permission(&permissions.exec, dirs, plugin_uuid)?, + ffi: Permissions::new_unary(None, None, false), + import: UnaryPermission::default(), + all: Permissions::new_all(false), + } + )) } -fn path_permission( +fn path_permission + Hash>( paths: &[String], - to_permission: fn(PathBuf) -> T, + to_permission: fn(PathBuf) -> P, dirs: &Dirs, plugin_uuid: &str ) -> anyhow::Result> { - let granted = paths + let allow_list = paths .into_iter() .map(|path| { augment_path(path, dirs, plugin_uuid) @@ -71,91 +78,90 @@ fn path_permission( .filter_map(std::convert::identity) .collect::>(); - Ok(UnaryPermission { - prompt: false, - granted_global: false, - flag_denied_global: false, - granted_list: granted, - ..Default::default() - }) + let allow_list = if allow_list.is_empty() { + None + } else { + Some(allow_list) + }; + + Ok(Permissions::new_unary(allow_list, None, false)) } fn net_permission(domain_and_ports: &[String]) -> UnaryPermission { - let granted = domain_and_ports - .into_iter() - .map(|domain_and_port| { - NetDescriptor::from_str(&domain_and_port) - .expect("should be validated when loading") - }) - .collect(); + let allow_list = if domain_and_ports.is_empty() { + None + } else { + let allow_list = domain_and_ports.into_iter() + .map(|domain_and_port| { + NetDescriptor::parse(&domain_and_port) + .expect("should be validated when loading") + }) + .collect(); - UnaryPermission { - prompt: false, - granted_global: false, - flag_denied_global: false, - granted_list: granted, - ..Default::default() - } + Some(allow_list) + }; + + Permissions::new_unary(allow_list, None, false) } -fn env_permission(envs: &[String]) -> UnaryPermission { - let granted = envs - .into_iter() - .map(|env| EnvDescriptor::new(env)) - .collect(); +fn env_permission(envs: &[String]) -> UnaryPermission { + let allow_list = if envs.is_empty() { + None + } else { + let allow_list = envs.into_iter() + .map(|env| EnvDescriptor::new(env)) + .collect(); - UnaryPermission { - prompt: false, - granted_global: false, - flag_denied_global: false, - granted_list: granted, - ..Default::default() - } + Some(allow_list) + }; + + Permissions::new_unary(allow_list, None, false) } -fn sys_permission(system: &[String]) -> UnaryPermission { - let granted = system - .into_iter() - .map(|system| SysDescriptor(system.to_owned())) - .collect(); +fn sys_permission(system: &[String]) -> anyhow::Result> { + let allow_list = if system.is_empty() { + None + } else { + let allow_list = system.into_iter() + .map(|system| SysDescriptor::parse(system.to_owned())) + .collect::, _>>()? + .into_iter() + .collect(); - UnaryPermission { - prompt: false, - granted_global: false, - flag_denied_global: false, - granted_list: granted, - ..Default::default() - } + Some(allow_list) + }; + + Ok(Permissions::new_unary(allow_list, None, false)) } -fn run_permission(permissions: &PluginPermissionsExec, dirs: &Dirs, plugin_uuid: &str) -> anyhow::Result> { +fn run_permission(permissions: &PluginPermissionsExec, dirs: &Dirs, plugin_uuid: &str) -> anyhow::Result> { let granted_executable = permissions.executable .iter() .map(|path| { augment_path(path, dirs, plugin_uuid) - .map(|path| path.map(|path| RunDescriptor::Path(path))) + .map(|path| path.map(|path| AllowRunDescriptor(path))) }) .collect::>>()? .into_iter() .filter_map(std::convert::identity) - .collect::>(); + .collect::>(); let granted_command = permissions.command .iter() - .map(|cmd| RunDescriptor::Name(cmd.to_owned())) - .collect::>(); + .map(|cmd| AllowRunDescriptor(PathBuf::from(cmd))) + .collect::>(); let mut granted = HashSet::new(); granted.extend(granted_executable); granted.extend(granted_command); - Ok(UnaryPermission { - prompt: false, - granted_global: false, - flag_denied_global: false, - granted_list: granted, - ..Default::default() - }) + let allow_list = if granted.is_empty() { + None + } else { + Some(granted) + }; + + Ok(Permissions::new_unary(allow_list, None, false)) } fn augment_path(path: &String, dirs: &Dirs, plugin_uuid: &str) -> anyhow::Result> { diff --git a/rust/server/src/plugins/js/plugins/applications.rs b/rust/server/src/plugins/js/plugins/applications.rs index 557c662..1084bb1 100644 --- a/rust/server/src/plugins/js/plugins/applications.rs +++ b/rust/server/src/plugins/js/plugins/applications.rs @@ -1,4 +1,4 @@ -use deno_core::op; +use deno_core::op2; use std::path::PathBuf; use image::ImageFormat; use image::imageops::FilterType; @@ -63,19 +63,22 @@ pub struct DesktopSettings13AndPostData { } -#[op] +#[op2] +#[string] pub fn current_os() -> &'static str { std::env::consts::OS } #[cfg(target_os = "linux")] -#[op] -pub async fn linux_app_from_path(path: String) -> anyhow::Result> { +#[op2(async)] +#[serde] +pub async fn linux_app_from_path(#[string] path: String) -> anyhow::Result> { Ok(spawn_blocking(|| linux::linux_app_from_path(PathBuf::from(path))).await?) } #[cfg(target_os = "linux")] -#[op] +#[op2] +#[serde] pub fn linux_application_dirs() -> Vec { linux::linux_application_dirs() .into_iter() @@ -84,8 +87,8 @@ pub fn linux_application_dirs() -> Vec { } #[cfg(target_os = "linux")] -#[op] -pub fn linux_open_application(desktop_file_id: String) -> anyhow::Result<()> { +#[op2(fast)] +pub fn linux_open_application(#[string] desktop_file_id: String) -> anyhow::Result<()> { spawn_detached("gtk-launch", &[desktop_file_id])?; @@ -93,25 +96,28 @@ pub fn linux_open_application(desktop_file_id: String) -> anyhow::Result<()> { } #[cfg(target_os = "macos")] -#[op] +#[op2(fast)] pub fn macos_major_version() -> u8 { macos::macos_major_version() } #[cfg(target_os = "macos")] -#[op] -pub async fn macos_app_from_path(path: String) -> anyhow::Result> { +#[op2(async)] +#[serde] +pub async fn macos_app_from_path(#[string] path: String) -> anyhow::Result> { Ok(spawn_blocking(|| macos::macos_app_from_path(&PathBuf::from(path))).await?) } #[cfg(target_os = "macos")] -#[op] -pub async fn macos_app_from_arbitrary_path(path: String) -> anyhow::Result> { +#[op2(async)] +#[serde] +pub async fn macos_app_from_arbitrary_path(#[string] path: String) -> anyhow::Result> { Ok(spawn_blocking(|| macos::macos_app_from_arbitrary_path(PathBuf::from(path))).await?) } #[cfg(target_os = "macos")] -#[op] +#[op2] +#[serde] pub fn macos_system_applications() -> Vec { macos::macos_system_applications() .into_iter() @@ -120,7 +126,8 @@ pub fn macos_system_applications() -> Vec { } #[cfg(target_os = "macos")] -#[op] +#[op2] +#[serde] pub fn macos_application_dirs() -> Vec { macos::macos_application_dirs() .into_iter() @@ -129,8 +136,8 @@ pub fn macos_application_dirs() -> Vec { } #[cfg(target_os = "macos")] -#[op] -pub fn macos_open_application(app_path: String) -> anyhow::Result<()> { +#[op2(fast)] +pub fn macos_open_application(#[string] app_path: String) -> anyhow::Result<()> { spawn_detached("open", &[app_path])?; @@ -138,20 +145,22 @@ pub fn macos_open_application(app_path: String) -> anyhow::Result<()> { } #[cfg(target_os = "macos")] -#[op] +#[op2] +#[serde] pub fn macos_settings_pre_13() -> Vec { macos::macos_settings_pre_13() } #[cfg(target_os = "macos")] -#[op] +#[op2] +#[serde] pub fn macos_settings_13_and_post() -> Vec { macos::macos_settings_13_and_post() } #[cfg(target_os = "macos")] -#[op] -pub fn macos_open_setting_13_and_post(preferences_id: String) -> anyhow::Result<()> { +#[op2(fast)] +pub fn macos_open_setting_13_and_post(#[string] preferences_id: String) -> anyhow::Result<()> { spawn_detached( "open", @@ -164,8 +173,8 @@ pub fn macos_open_setting_13_and_post(preferences_id: String) -> anyhow::Result< } #[cfg(target_os = "macos")] -#[op] -pub fn macos_open_setting_pre_13(setting_path: String) -> anyhow::Result<()> { +#[op2(fast)] +pub fn macos_open_setting_pre_13(#[string] setting_path: String) -> anyhow::Result<()> { spawn_detached( "open", diff --git a/rust/server/src/plugins/js/plugins/numbat.rs b/rust/server/src/plugins/js/plugins/numbat.rs index 2bfc5a8..c7507d2 100644 --- a/rust/server/src/plugins/js/plugins/numbat.rs +++ b/rust/server/src/plugins/js/plugins/numbat.rs @@ -1,5 +1,5 @@ use anyhow::anyhow; -use deno_core::{op, OpState}; +use deno_core::{op2, OpState}; use numbat::markup::{Formatter, PlainTextFormatter}; use numbat::module_importer::BuiltinModuleImporter; use numbat::pretty_print::PrettyPrint; @@ -34,8 +34,9 @@ struct NumbatResult { right: String, } -#[op] -fn run_numbat(state: Rc>, input: String) -> anyhow::Result { +#[op2] +#[serde] +pub fn run_numbat(state: Rc>, #[string] input: String) -> anyhow::Result { let context = { let state = state.borrow(); diff --git a/rust/server/src/plugins/js/plugins/settings.rs b/rust/server/src/plugins/js/plugins/settings.rs index 772bb42..df880e4 100644 --- a/rust/server/src/plugins/js/plugins/settings.rs +++ b/rust/server/src/plugins/js/plugins/settings.rs @@ -1,7 +1,7 @@ -use deno_core::op; +use deno_core::op2; -#[op] -fn open_settings() -> anyhow::Result<()> { +#[op2(fast)] +pub fn open_settings() -> anyhow::Result<()> { std::process::Command::new(std::env::current_exe()?) .args(["settings"]) .spawn()?; diff --git a/rust/server/src/plugins/js/preferences.rs b/rust/server/src/plugins/js/preferences.rs index 889d3ed..e0b1c32 100644 --- a/rust/server/src/plugins/js/preferences.rs +++ b/rust/server/src/plugins/js/preferences.rs @@ -1,15 +1,16 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use deno_core::{op, OpState}; +use deno_core::{op2, OpState}; use deno_core::futures::executor::block_on; use crate::model::PreferenceUserData; use crate::plugins::data_db_repository::{DataDbRepository, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::js::PluginData; -#[op] -fn get_plugin_preferences(state: Rc>) -> anyhow::Result> { +#[op2] +#[serde] +pub fn get_plugin_preferences(state: Rc>) -> anyhow::Result> { let (plugin_id, repository) = { let state = state.borrow(); @@ -34,8 +35,9 @@ fn get_plugin_preferences(state: Rc>) -> anyhow::Result>, entrypoint_id: &str) -> anyhow::Result> { +#[op2] +#[serde] +pub fn get_entrypoint_preferences(state: Rc>, #[string] entrypoint_id: &str) -> anyhow::Result> { let (plugin_id, repository) = { let state = state.borrow(); @@ -61,8 +63,8 @@ fn get_entrypoint_preferences(state: Rc>, entrypoint_id: &str) } -#[op] -async fn plugin_preferences_required(state: Rc>) -> anyhow::Result { +#[op2(async)] +pub async fn plugin_preferences_required(state: Rc>) -> anyhow::Result { let (plugin_id, repository) = { let state = state.borrow(); @@ -84,8 +86,8 @@ async fn plugin_preferences_required(state: Rc>) -> anyhow::Res Ok(any_preferences_missing_value(preferences, preferences_user_data)) } -#[op] -async fn entrypoint_preferences_required(state: Rc>, entrypoint_id: String) -> anyhow::Result { +#[op2(async)] +pub async fn entrypoint_preferences_required(state: Rc>, #[string] entrypoint_id: String) -> anyhow::Result { let (plugin_id, repository) = { let state = state.borrow(); diff --git a/rust/server/src/plugins/js/search.rs b/rust/server/src/plugins/js/search.rs index f6b482c..193d44a 100644 --- a/rust/server/src/plugins/js/search.rs +++ b/rust/server/src/plugins/js/search.rs @@ -4,14 +4,14 @@ use crate::plugins::js::PluginData; use crate::search::{SearchIndex, SearchIndexItem, SearchIndexItemAction}; use anyhow::Context; use common::model::{EntrypointId, SearchResultEntrypointType}; -use deno_core::{op, OpState}; +use deno_core::{op2, OpState}; use serde::Deserialize; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -#[op] -async fn reload_search_index(state: Rc>, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { +#[op2(async)] +pub async fn reload_search_index(state: Rc>, #[serde] generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { let (plugin_id, plugin_uuid, repository, mut search_index, icon_cache) = { let state = state.borrow(); diff --git a/rust/server/src/plugins/js/ui.rs b/rust/server/src/plugins/js/ui.rs index 0dc54df..e868a5d 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/server/src/plugins/js/ui.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use std::io::Read; use std::rc::Rc; use anyhow::{anyhow, Context}; -use deno_core::{op, OpState, serde_v8, v8}; +use deno_core::{op2, OpState, serde_v8, v8}; use deno_core::futures::executor::block_on; use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; use indexmap::IndexMap; @@ -16,8 +16,8 @@ use crate::model::{JsUiRenderLocation, JsUiRequestData, JsUiResponseData}; use crate::plugins::data_db_repository::DataDbRepository; use crate::plugins::js::{ComponentModel, make_request, PluginData}; -#[op] -fn show_plugin_error_view(state: Rc>, entrypoint_id: String, render_location: JsUiRenderLocation) -> anyhow::Result<()> { +#[op2] +pub fn show_plugin_error_view(state: Rc>, #[string] entrypoint_id: String, #[serde] render_location: JsUiRenderLocation) -> anyhow::Result<()> { let data = JsUiRequestData::ShowPluginErrorView { entrypoint_id: EntrypointId::from_string(entrypoint_id), render_location, @@ -32,8 +32,8 @@ fn show_plugin_error_view(state: Rc>, entrypoint_id: String, re } } -#[op] -fn show_preferences_required_view(state: Rc>, entrypoint_id: String, plugin_preferences_required: bool, entrypoint_preferences_required: bool) -> anyhow::Result<()> { +#[op2(fast)] +pub fn show_preferences_required_view(state: Rc>, #[string] entrypoint_id: String, plugin_preferences_required: bool, entrypoint_preferences_required: bool) -> anyhow::Result<()> { let data = JsUiRequestData::ShowPreferenceRequiredView { entrypoint_id: EntrypointId::from_string(entrypoint_id), plugin_preferences_required, @@ -49,8 +49,8 @@ fn show_preferences_required_view(state: Rc>, entrypoint_id: St } } -#[op] -fn clear_inline_view(state: Rc>) -> anyhow::Result<()> { +#[op2(fast)] +pub fn clear_inline_view(state: Rc>) -> anyhow::Result<()> { let data = JsUiRequestData::ClearInlineView; match make_request(&state, data).context("ClearInlineView frontend response")? { @@ -62,22 +62,23 @@ fn clear_inline_view(state: Rc>) -> anyhow::Result<()> { } } -#[op] -fn op_inline_view_endpoint_id(state: Rc>) -> Option { +#[op2] +#[string] +pub fn op_inline_view_endpoint_id(state: Rc>) -> Option { state.borrow() .borrow::() .inline_view_entrypoint_id() .clone() } -#[op(v8)] -fn op_react_replace_view<'a>( +#[op2] +pub fn op_react_replace_view<'a>( scope: &mut v8::HandleScope, state: Rc>, - render_location: JsUiRenderLocation, + #[serde] render_location: JsUiRenderLocation, top_level_view: bool, - entrypoint_id: &str, - container: serde_v8::Value<'a>, + #[string] entrypoint_id: &str, + #[serde] container: serde_v8::Value<'a>, ) -> anyhow::Result<()> { tracing::trace!(target = "renderer_rs", "Calling op_react_replace_view..."); @@ -124,19 +125,21 @@ fn op_react_replace_view<'a>( } } -#[op] -fn op_component_model(state: Rc>) -> HashMap { +#[op2] +#[serde] +pub fn op_component_model(state: Rc>) -> HashMap { state.borrow() .borrow::() .components .clone() } -#[op] -async fn fetch_action_id_for_shortcut( +#[op2(async)] +#[string] +pub async fn fetch_action_id_for_shortcut( state: Rc>, - entrypoint_id: String, - key: String, + #[string] entrypoint_id: String, + #[string] key: String, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, @@ -170,8 +173,8 @@ async fn fetch_action_id_for_shortcut( Ok(result) } -#[op] -async fn show_hud(state: Rc>, display: String) -> anyhow::Result<()> { +#[op2(async)] +pub async fn show_hud(state: Rc>, #[string] display: String) -> anyhow::Result<()> { let data = JsUiRequestData::ShowHud { display }; @@ -185,8 +188,8 @@ async fn show_hud(state: Rc>, display: String) -> anyhow::Resul } } -#[op] -async fn update_loading_bar(state: Rc>, entrypoint_id: String, show: bool) -> anyhow::Result<()> { +#[op2(async)] +pub async fn update_loading_bar(state: Rc>, #[string] entrypoint_id: String, show: bool) -> anyhow::Result<()> { let plugin_id = { let state = state.borrow(); diff --git a/scenarios/plugins/docs_detail/package.json b/scenarios/plugins/docs_detail/package.json index 5fe4a4d..d0995ad 100644 --- a/scenarios/plugins/docs_detail/package.json +++ b/scenarios/plugins/docs_detail/package.json @@ -12,6 +12,6 @@ "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_form/package.json b/scenarios/plugins/docs_form/package.json index 4e606fc..d134d67 100644 --- a/scenarios/plugins/docs_form/package.json +++ b/scenarios/plugins/docs_form/package.json @@ -12,6 +12,6 @@ "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_grid/package.json b/scenarios/plugins/docs_grid/package.json index 21918ec..d99c7c7 100644 --- a/scenarios/plugins/docs_grid/package.json +++ b/scenarios/plugins/docs_grid/package.json @@ -12,6 +12,6 @@ "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_inline_separators/package.json b/scenarios/plugins/docs_inline_separators/package.json index 5dd69d6..2a97183 100644 --- a/scenarios/plugins/docs_inline_separators/package.json +++ b/scenarios/plugins/docs_inline_separators/package.json @@ -12,6 +12,6 @@ "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_inline_three_sections/package.json b/scenarios/plugins/docs_inline_three_sections/package.json index 1a04f8a..0ba42d0 100644 --- a/scenarios/plugins/docs_inline_three_sections/package.json +++ b/scenarios/plugins/docs_inline_three_sections/package.json @@ -12,6 +12,6 @@ "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_inline_two_sections/package.json b/scenarios/plugins/docs_inline_two_sections/package.json index 5daa663..17ea73d 100644 --- a/scenarios/plugins/docs_inline_two_sections/package.json +++ b/scenarios/plugins/docs_inline_two_sections/package.json @@ -12,6 +12,6 @@ "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_list/package.json b/scenarios/plugins/docs_list/package.json index befab30..663e982 100644 --- a/scenarios/plugins/docs_list/package.json +++ b/scenarios/plugins/docs_list/package.json @@ -12,6 +12,6 @@ "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } } diff --git a/tools b/tools index 6e78122..7bc5ef7 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit 6e781221180857778684106d229aecf6c1869a31 +Subproject commit 7bc5ef7d8326172b4353d37763b3c55e4ace051f From 364beb647b11e1cb084f65d96b56e946fab38aea Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:02:01 +0100 Subject: [PATCH 207/540] Abstract away repository, icon_cache and search index from js runtime ops --- Cargo.lock | 4241 ++++++++++------- Cargo.toml | 1 + rust/common_plugin_runtime/Cargo.toml | 9 + .../src/backend_for_plugin_runtime_api.rs | 22 + rust/common_plugin_runtime/src/lib.rs | 2 + rust/common_plugin_runtime/src/model.rs | 27 + rust/server/Cargo.toml | 1 + rust/server/src/model.rs | 10 - rust/server/src/plugins/js/assets.rs | 33 +- .../src/plugins/js/command_generators.rs | 27 +- rust/server/src/plugins/js/mod.rs | 325 +- rust/server/src/plugins/js/preferences.rs | 158 +- rust/server/src/plugins/js/search.rs | 183 +- rust/server/src/plugins/js/ui.rs | 30 +- rust/server/src/search.rs | 2 +- 15 files changed, 3080 insertions(+), 1991 deletions(-) create mode 100644 rust/common_plugin_runtime/Cargo.toml create mode 100644 rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs create mode 100644 rust/common_plugin_runtime/src/lib.rs create mode 100644 rust/common_plugin_runtime/src/model.rs diff --git a/Cargo.lock b/Cargo.lock index cc263dc..373be30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "adler32" version = "1.2.0" @@ -56,7 +62,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array 0.14.7", + "generic-array", +] + +[[package]] +name = "aead-gcm-stream" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4947a169074c7e038fa43051d1c4e073f4488b0e4b0a30658f1e1a1b06449ce8" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", ] [[package]] @@ -99,7 +119,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.14", + "getrandom", "once_cell", "version_check", ] @@ -111,7 +131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom 0.2.14", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -251,9 +271,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" dependencies = [ "backtrace", ] @@ -286,7 +306,7 @@ dependencies = [ "objc", "objc-foundation", "objc_id", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "thiserror", "winapi", "wl-clipboard-rs", @@ -305,8 +325,8 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -321,6 +341,9 @@ name = "arrayvec" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +dependencies = [ + "serde", +] [[package]] name = "as-raw-xcb-connection" @@ -328,6 +351,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "ash" +version = "0.37.3+1.3.251" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +dependencies = [ + "libloading 0.7.4", +] + [[package]] name = "ash" version = "0.38.0+1.3.281" @@ -346,11 +378,11 @@ dependencies = [ "asn1-rs-derive", "asn1-rs-impl", "displaydoc", - "nom", + "nom 7.1.3", "num-traits", "rusticata-macros", "thiserror", - "time 0.3.36", + "time", ] [[package]] @@ -359,10 +391,10 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 1.0.109", - "synstructure", + "synstructure 0.12.6", ] [[package]] @@ -371,20 +403,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 1.0.109", ] [[package]] name = "ast_node" -version = "0.9.5" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c09c69dffe06d222d072c878c3afe86eee2179806f20503faec97250268b4c24" +checksum = "f9184f2b369b3e8625712493c89b785881f27eedc6cde480a81883cef78868b2" dependencies = [ - "pmutil", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "swc_macros_common", "syn 2.0.89", ] @@ -395,7 +426,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" dependencies = [ - "event-listener 5.3.1", + "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -476,7 +507,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 5.3.1", + "event-listener", "event-listener-strategy", "pin-project-lite", ] @@ -494,7 +525,7 @@ dependencies = [ "async-task", "blocking", "cfg-if", - "event-listener 5.3.1", + "event-listener", "futures-lite", "rustix", "tracing", @@ -506,8 +537,8 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -546,8 +577,8 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -563,8 +594,8 @@ version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -616,7 +647,7 @@ dependencies = [ "log", "rustls 0.22.4", "url", - "webpki-roots 0.26.3", + "webpki-roots", ] [[package]] @@ -645,7 +676,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 7.1.3", "num-rational", "v_frame", ] @@ -666,7 +697,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "bitflags 1.3.2", "bytes", "futures-util", @@ -681,7 +712,34 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "sync_wrapper 0.1.2", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 1.0.2", "tower", "tower-layer", "tower-service", @@ -704,6 +762,26 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -719,12 +797,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base16ct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" - [[package]] name = "base16ct" version = "0.2.0" @@ -732,10 +804,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] -name = "base64" -version = "0.13.1" +name = "base32" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" [[package]] name = "base64" @@ -749,13 +821,22 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +[[package]] +name = "base64-simd" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5" +dependencies = [ + "simd-abstraction", +] + [[package]] name = "base64-simd" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" dependencies = [ - "outref", + "outref 0.5.1", "vsimd", ] @@ -783,15 +864,59 @@ dependencies = [ "scoped-tls", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.89", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" @@ -834,6 +959,27 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06c9989a51171e2e81038ab168b6ae22886fe9ded214430dbb4f41c28cf176da" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block" version = "0.1.6" @@ -846,7 +992,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -855,7 +1001,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -881,14 +1027,13 @@ dependencies = [ ] [[package]] -name = "brotli" -version = "3.5.0" +name = "boxed_error" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +checksum = "17d4f95e880cfd28c4ca5a006cf7f6af52b4bcb7b5866f573b2faa126fb7affb" dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor 2.5.1", + "quote", + "syn 2.0.89", ] [[package]] @@ -903,13 +1048,14 @@ dependencies = [ ] [[package]] -name = "brotli-decompressor" -version = "2.5.1" +name = "brotli" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", + "brotli-decompressor 4.0.1", ] [[package]] @@ -922,6 +1068,16 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bstr" version = "1.9.1" @@ -943,6 +1099,9 @@ name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +dependencies = [ + "allocator-api2", +] [[package]] name = "bytemuck" @@ -959,8 +1118,8 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -1151,12 +1310,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.94" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -1171,6 +1331,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfg-expr" version = "0.15.8" @@ -1201,18 +1370,17 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", - "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets 0.52.6", ] [[package]] @@ -1225,6 +1393,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.3", +] + [[package]] name = "clap" version = "4.5.4" @@ -1254,8 +1433,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -1288,7 +1467,7 @@ dependencies = [ "common", "common-ui", "component_model", - "convert_case 0.6.0", + "convert_case", "global-hotkey", "iced", "iced_aw", @@ -1301,7 +1480,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tonic", + "tonic 0.11.0", "tracing", "tracing-subscriber", "tray-icon", @@ -1358,15 +1537,6 @@ dependencies = [ "x11rb 0.13.1", ] -[[package]] -name = "cmake" -version = "0.1.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" -dependencies = [ - "cc", -] - [[package]] name = "cocoa" version = "0.26.0" @@ -1407,6 +1577,27 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color-print" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4" +dependencies = [ + "color-print-proc-macro", +] + +[[package]] +name = "color-print-proc-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22" +dependencies = [ + "nom 7.1.3", + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -1437,7 +1628,7 @@ dependencies = [ "base64 0.22.0", "bytes", "component_model", - "convert_case 0.6.0", + "convert_case", "directories", "gix-url", "indexmap 2.2.6", @@ -1448,7 +1639,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tonic", + "tonic 0.11.0", "tonic-build", "utils", ] @@ -1463,6 +1654,15 @@ dependencies = [ "iced_fonts", ] +[[package]] +name = "common_plugin_runtime" +version = "0.1.0" +dependencies = [ + "anyhow", + "common", + "serde", +] + [[package]] name = "component_model" version = "0.0.0" @@ -1482,16 +1682,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console_static_text" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4be93df536dfbcbd39ff7c129635da089901116b88bfc29ec1acb9b56f8ff35" -dependencies = [ - "unicode-width", - "vte", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -1513,17 +1703,11 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.14", + "getrandom", "once_cell", "tiny-keccak", ] -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "convert_case" version = "0.6.0" @@ -1533,6 +1717,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cooked-waker" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147be55d677052dabc6b22252d5dd0fd4c29c8c27aa4f2fbef0f94aa003b406f" + [[package]] name = "core-foundation" version = "0.9.4" @@ -1725,26 +1915,14 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array 0.14.7", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - [[package]] name = "crypto-bigint" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array 0.14.7", - "rand_core 0.6.4", + "generic-array", + "rand_core", "subtle", "zeroize", ] @@ -1755,8 +1933,8 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.7", - "rand_core 0.6.4", + "generic-array", + "rand_core", "typenum", ] @@ -1783,28 +1961,15 @@ checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" [[package]] name = "curve25519-dalek" -version = "2.1.3" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b85542f99a2dfa2a1b8e192662741c9859a846b296bef1c92ef9b58b5a216" -dependencies = [ - "byteorder", - "digest 0.8.1", - "rand_core 0.5.1", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "fiat-crypto", - "platforms", + "digest", + "fiat-crypto 0.2.7", "rustc_version 0.4.0", "subtle", "zeroize", @@ -1816,11 +1981,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] +[[package]] +name = "d3d12" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" +dependencies = [ + "bitflags 2.6.0", + "libloading 0.8.3", + "winapi", +] + [[package]] name = "dark-light" version = "1.1.1" @@ -1855,8 +2031,8 @@ checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "strsim", "syn 2.0.89", ] @@ -1868,7 +2044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", - "quote 1.0.37", + "quote", "syn 2.0.89", ] @@ -1879,7 +2055,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core 0.9.9", @@ -1923,40 +2099,20 @@ dependencies = [ "byteorder", ] -[[package]] -name = "deno-proc-macro-rules" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c65c2ffdafc1564565200967edc4851c7b55422d3913466688907efd05ea26f" -dependencies = [ - "deno-proc-macro-rules-macros", - "proc-macro2 1.0.92", - "syn 2.0.89", -] - -[[package]] -name = "deno-proc-macro-rules-macros" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3047b312b7451e3190865713a4dd6e1f821aed614ada219766ebc3024a690435" -dependencies = [ - "once_cell", - "proc-macro2 1.0.92", - "quote 1.0.37", - "syn 2.0.89", -] - [[package]] name = "deno_ast" -version = "0.28.0" +version = "0.43.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c93119b1c487a85603406a988a0ca9a1d0e5315404cccc5c158fb484b1f5a2" +checksum = "48d00b724e06d2081a141ec1155756a0b465d413d8e2a7515221f61d482eb2ee" dependencies = [ - "anyhow", - "base64 0.13.1", + "base64 0.21.7", "deno_media_type", + "deno_terminal 0.1.1", "dprint-swc-ext", + "once_cell", + "percent-encoding", "serde", + "sourcemap 9.1.2", "swc_atoms", "swc_common", "swc_config", @@ -1979,102 +2135,150 @@ dependencies = [ "swc_visit", "swc_visit_macros", "text_lines", + "thiserror", + "unicode-width", "url", ] [[package]] name = "deno_broadcast_channel" -version = "0.112.0" +version = "0.173.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6632cedee6cbc6ee121edf9e81985edc4547efa9d5dec5d9437ee47d6d2a6749" +checksum = "348ecdacfdd262e6b2f9740d07a41e8f4d79d06a670378a060515d0208495c9f" dependencies = [ "async-trait", "deno_core", + "thiserror", "tokio", "uuid", ] [[package]] name = "deno_cache" -version = "0.50.0" +version = "0.111.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66ea6e93e9102cfe3bfff31e1e12e8f64fac80dfcd42ceaf0149cea9a933f3e9" +checksum = "3a6e35cb122e56c22149652327c90c563790ddcef24ea1fc77454e193131318e" dependencies = [ "async-trait", "deno_core", "rusqlite", "serde", "sha2", + "thiserror", "tokio", ] [[package]] -name = "deno_console" -version = "0.118.0" +name = "deno_canvas" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1c32084b9ff8667d7d2176262739ce473e0341c7625325b48a2fbab0c903dc" +checksum = "1bbfd1437bc01ab775b1a60e3061bbf2e9517e31fb5eedf89b2b703104c835e6" +dependencies = [ + "deno_core", + "deno_webgpu", + "image 0.24.9", + "serde", + "thiserror", +] + +[[package]] +name = "deno_console" +version = "0.179.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e09f2bbb2d842329b602da25dbab5cd4a342f9a8adcb7c02509fc322f796e79" dependencies = [ "deno_core", ] [[package]] name = "deno_core" -version = "0.204.0" +version = "0.321.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ddf51deb9a3bb60a4ab74784414b3f2f89de83a77d6d90a64c6447f7765d68" +checksum = "cd2a54cda74cdc187d5fc2d23370a45cf09f912caf566dd1cd24a50157d809c7" dependencies = [ "anyhow", + "bincode", + "bit-set 0.5.3", + "bit-vec 0.6.3", "bytes", + "cooked-waker", + "deno_core_icudata", "deno_ops", "deno_unsync", "futures", - "indexmap 1.9.3", + "indexmap 2.2.6", "libc", - "log", - "once_cell", - "parking_lot 0.12.1", + "memoffset 0.9.1", + "parking_lot 0.12.3", + "percent-encoding", "pin-project", "serde", "serde_json", "serde_v8", "smallvec", - "sourcemap", + "sourcemap 8.0.1", + "static_assertions", "tokio", "url", "v8", + "wasm_dep_analyzer", +] + +[[package]] +name = "deno_core_icudata" +version = "0.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4dccb6147bb3f3ba0c7a48e993bfeb999d2c2e47a81badee80e2b370c8d695" + +[[package]] +name = "deno_cron" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f936f036e9e3f88205db8efd0ec68c65efb47bc0cbe4b715bafecd6e9c407931" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "deno_core", + "saffron", + "thiserror", + "tokio", ] [[package]] name = "deno_crypto" -version = "0.132.0" +version = "0.193.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39deb9b4cadcb1a7a98138ac34fadf5389bd2fee4b4e8e8c3f6333c09efea89e" +checksum = "4b582f30887c7c0902b4445c64d7c8b98d0043ec547c44de8de26104b093e1be" dependencies = [ "aes", "aes-gcm", "aes-kw", - "base64 0.13.1", + "base64 0.21.7", "cbc", "const-oid", "ctr", - "curve25519-dalek 2.1.3", + "curve25519-dalek", "deno_core", "deno_web", - "elliptic-curve 0.12.3", + "ed448-goldilocks", + "elliptic-curve", "num-traits", "once_cell", - "p256 0.11.1", - "p384 0.11.2", + "p256", + "p384", + "p521", "rand", - "ring 0.16.20", - "rsa 0.7.2", - "sec1 0.3.0", + "ring", + "rsa", + "sec1", "serde", "serde_bytes", "sha1", "sha2", - "signature 1.6.4", - "spki 0.6.0", + "signature", + "spki", + "thiserror", "tokio", "uuid", "x25519-dalek", @@ -2082,90 +2286,118 @@ dependencies = [ [[package]] name = "deno_fetch" -version = "0.142.0" +version = "0.203.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "067336cb3a415cce643007c8ace7872a19a517d6e1eccbf9e8cfff8d886fb31a" +checksum = "a18e66bd3bf786e24a8b8bdc97049fa82957b095a5fd1e142545c5a7cdd2272a" dependencies = [ + "base64 0.21.7", "bytes", "data-url", "deno_core", + "deno_permissions", "deno_tls", "dyn-clone", - "http 0.2.12", - "reqwest", + "error_reporter", + "hickory-resolver", + "http 1.1.0", + "http-body-util", + "hyper 1.5.1", + "hyper-rustls", + "hyper-util", + "ipnet", + "percent-encoding", + "rustls-webpki", "serde", + "serde_json", + "thiserror", "tokio", + "tokio-rustls", + "tokio-socks", "tokio-util", + "tower", + "tower-http", + "tower-service", ] [[package]] name = "deno_ffi" -version = "0.105.0" +version = "0.166.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8903b6b849ab9d6d24f0bffe21bce6f64205cab411daf9e73482fe4260e4feca" +checksum = "5e6d2f13ebfa93833446abeb3bd1836fdf86bcb96678276b21a0622146f42284" dependencies = [ "deno_core", - "dlopen", + "deno_permissions", + "dlopen2 0.6.1", "dynasmrt", "libffi", "libffi-sys", + "log", + "num-bigint", "serde", "serde-value", "serde_json", + "thiserror", "tokio", "winapi", ] [[package]] name = "deno_fs" -version = "0.28.0" +version = "0.89.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bf05b1bc2c42e407a83e8724d4569006acaf97f2c43e04ae013541add84f05" +checksum = "f53829328c344736d7fdda44733057299536f3379513cdcd258823ef273540ec" dependencies = [ "async-trait", + "base32", + "boxed_error", "deno_core", "deno_io", + "deno_path_util", + "deno_permissions", "filetime", - "fs3", + "junction", "libc", - "log", - "nix 0.26.2", + "nix 0.27.1", "rand", + "rayon", "serde", - "tokio", + "thiserror", "winapi", + "windows-sys 0.52.0", ] [[package]] name = "deno_http" -version = "0.113.0" +version = "0.177.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d42a3e4d7c2340014f586b112844946fedbbe3aed1393ae078fe458c61a59c" +checksum = "42b4ee6dbac20aa287a416f8905ed64b95cb484063c2af6be4eb232382c7fcb6" dependencies = [ "async-compression", "async-trait", - "base64 0.13.1", - "brotli 3.5.0", + "base64 0.21.7", + "brotli 6.0.0", "bytes", "cache_control", "deno_core", "deno_net", "deno_websocket", "flate2", - "fly-accept-encoding", "http 0.2.12", + "http 1.1.0", "httparse", "hyper 0.14.28", - "hyper 1.0.0-rc.4", + "hyper 1.5.1", + "hyper-util", + "itertools 0.10.5", "memmem", "mime", "once_cell", "percent-encoding", - "phf 0.10.1", + "phf", "pin-project", - "ring 0.16.20", + "ring", + "scopeguard", "serde", - "slab", "smallvec", "thiserror", "tokio", @@ -2174,63 +2406,66 @@ dependencies = [ [[package]] name = "deno_io" -version = "0.28.0" +version = "0.89.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c09188978bdaea1ac7c413a75993df239f4e605641b2ac2a125f0e7216cdc8" +checksum = "cc19195805a6b256d5ffe697c81ac79f8acd22246616fe880d6c9ec2dacf9bb4" dependencies = [ "async-trait", "deno_core", "filetime", "fs3", + "libc", + "log", "once_cell", + "os_pipe", + "parking_lot 0.12.3", + "pin-project", + "rand", "tokio", + "uuid", "winapi", + "windows-sys 0.52.0", ] [[package]] name = "deno_kv" -version = "0.26.0" +version = "0.87.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc353fbbaa560e9c6e1256471314f22dd9f79c716585af40752b9a14feffcb68" +checksum = "5a25347cd7ae561d0b05c24eebb3047e85a3af3f398675d5a9894fd167f2714f" dependencies = [ "anyhow", "async-trait", - "base64 0.13.1", + "base64 0.21.7", + "boxed_error", + "bytes", "chrono", "deno_core", - "deno_unsync", - "hex", + "deno_fetch", + "deno_path_util", + "deno_permissions", + "deno_tls", + "denokv_proto", + "denokv_remote", + "denokv_sqlite", + "faster-hex 0.9.0", + "http 1.1.0", + "http-body-util", "log", "num-bigint", - "prost 0.11.9", - "prost-build 0.11.9", + "prost 0.13.4", + "prost-build 0.13.4", "rand", - "reqwest", "rusqlite", "serde", - "serde_json", - "tokio", - "url", - "uuid", -] - -[[package]] -name = "deno_lockfile" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1fcc91fa4e18c3e0574965d7133709e76eda665cb589de703219f0819dfaec" -dependencies = [ - "ring 0.16.20", - "serde", - "serde_json", "thiserror", + "url", ] [[package]] name = "deno_media_type" -version = "0.1.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edf9879493856d1622be70f396b0b0d3e519538dd6501b7c609ecbaa7e2194d2" +checksum = "eaa135b8a9febc9a51c16258e294e268a1276750780d69e46edb31cced2826e4" dependencies = [ "data-url", "serde", @@ -2239,142 +2474,222 @@ dependencies = [ [[package]] name = "deno_napi" -version = "0.48.0" +version = "0.110.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab18a468aa3b7053f1bfa075d989fc75f69bb7ebca72cca9e2125eec141a4dd" +checksum = "ea57b67488969f82594cb008fed1bd99830e6db042e31ee9878933d8c76be41c" dependencies = [ "deno_core", + "deno_permissions", + "libc", "libloading 0.7.4", + "log", + "napi_sym", + "thiserror", + "windows-sys 0.52.0", +] + +[[package]] +name = "deno_native_certs" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bc737e098a45aa5742d51ce694ac7236a1e69fb0d9df8c862e9b4c9583c5f9" +dependencies = [ + "dlopen2 0.7.0", + "dlopen2_derive", + "once_cell", + "rustls-native-certs", + "rustls-pemfile", ] [[package]] name = "deno_net" -version = "0.110.0" +version = "0.171.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea49a9aeca4e5f93e47223aee15297b580b1bea758c557a80b4717c2ee1502e" +checksum = "f7b3a51f7b4d5d64d17a7bc6f7495498f20d809930979d21a059d75e850cdea6" dependencies = [ "deno_core", + "deno_permissions", "deno_tls", - "enum-as-inner", - "log", + "hickory-proto", + "hickory-resolver", "pin-project", + "rustls-tokio-stream", "serde", "socket2", + "thiserror", "tokio", - "trust-dns-proto", - "trust-dns-resolver", ] [[package]] name = "deno_node" -version = "0.55.0" +version = "0.116.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "438bf61110801052e18555ce23235032b69d0f83313518330ed47151b0f6ea85" +checksum = "3bd0d1a757f75224e84ce8a553c2465e4a352fba4b7551ec15809d8a119847e7" dependencies = [ + "aead-gcm-stream", "aes", - "brotli 3.5.0", + "async-trait", + "base64 0.21.7", + "blake2", + "boxed_error", + "brotli 6.0.0", + "bytes", "cbc", + "const-oid", "data-encoding", "deno_core", "deno_fetch", "deno_fs", + "deno_io", "deno_media_type", - "deno_npm", - "deno_semver", - "digest 0.10.7", + "deno_net", + "deno_package_json", + "deno_path_util", + "deno_permissions", + "deno_whoami", + "der", + "digest", "dsa", "ecb", - "elliptic-curve 0.13.8", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", "errno 0.2.8", - "hex", + "faster-hex 0.9.0", + "h2 0.4.7", "hkdf", - "idna 0.3.0", + "home", + "http 1.1.0", + "http-body-util", + "hyper 1.5.1", + "hyper-util", + "idna", "indexmap 2.2.6", - "lazy-regex 3.1.0", + "ipnetwork", + "k256", + "lazy-regex", "libc", "libz-sys", "md-5", "md4", + "memchr", + "node_resolver", "num-bigint", "num-bigint-dig", "num-integer", "num-traits", "once_cell", "p224", - "p256 0.13.2", - "p384 0.13.0", + "p256", + "p384", "path-clean", "pbkdf2", + "pin-project-lite", + "pkcs8", "rand", "regex", - "reqwest", - "ring 0.16.20", + "ring", "ripemd", - "rsa 0.7.2", + "rsa", "scrypt", - "secp256k1", + "sec1", "serde", - "sha-1", + "sha1", "sha2", - "signature 1.6.4", + "sha3", + "signature", + "simd-json", + "sm3", + "spki", + "stable_deref_trait", + "thiserror", "tokio", - "typenum", - "whoami", + "tokio-eld", + "url", + "webpki-root-certs", "winapi", + "windows-sys 0.52.0", "x25519-dalek", "x509-parser", -] - -[[package]] -name = "deno_npm" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90198ae433bf22ac9b39fe5e18748d9d5b36db042ef1c24637f43d3b5e101e0" -dependencies = [ - "anyhow", - "async-trait", - "deno_lockfile", - "deno_semver", - "futures", - "log", - "monch", - "serde", - "thiserror", + "yoke", ] [[package]] name = "deno_ops" -version = "0.82.0" +version = "0.197.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b660872f9a9737d3424470483dd6730d2129481af5055449a2a37ab5bc2145e" +checksum = "37a8825d92301cf445727c43f17fee2a20fcdf4370004339965156ae7c56c97e" dependencies = [ - "deno-proc-macro-rules", - "lazy-regex 2.5.0", - "once_cell", - "pmutil", - "proc-macro-crate 1.3.1", - "proc-macro2 1.0.92", - "quote 1.0.37", - "regex", + "proc-macro-rules", + "proc-macro2", + "quote", + "stringcase", "strum", "strum_macros", - "syn 1.0.109", "syn 2.0.89", "thiserror", ] [[package]] -name = "deno_runtime" -version = "0.126.0" +name = "deno_package_json" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2caeeb917cc8749d6a516317d23ac35c88dc71535ec5c37c0b8dca8e87ddbc4" +checksum = "6cbc4c4d3eb0960b58e8f43f9fc2d3f620fcac9a03cd85203e08db5b04e83c1f" dependencies = [ - "console_static_text", + "deno_semver", + "indexmap 2.2.6", + "serde", + "serde_json", + "thiserror", + "url", +] + +[[package]] +name = "deno_path_util" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff25f6e08e7a0214bbacdd6f7195c7f1ebcd850c87a624e4ff06326b68b42d99" +dependencies = [ + "percent-encoding", + "thiserror", + "url", +] + +[[package]] +name = "deno_permissions" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e822f98185ab3ddf06104b2407681e0008af52361af32f1cd171b7eda5aa59" +dependencies = [ + "deno_core", + "deno_path_util", + "deno_terminal 0.2.0", + "fqdn", + "libc", + "log", + "once_cell", + "percent-encoding", + "serde", + "thiserror", + "which 4.4.2", + "winapi", +] + +[[package]] +name = "deno_runtime" +version = "0.188.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516ed4f796ab0f5dc092b5592ed6159c759f4f3a94f4a23455fecc94edc51dd1" +dependencies = [ + "async-trait", + "color-print", "deno_ast", "deno_broadcast_channel", "deno_cache", + "deno_canvas", "deno_console", "deno_core", + "deno_cron", "deno_crypto", "deno_fetch", "deno_ffi", @@ -2385,44 +2700,62 @@ dependencies = [ "deno_napi", "deno_net", "deno_node", + "deno_path_util", + "deno_permissions", + "deno_terminal 0.2.0", "deno_tls", "deno_url", "deno_web", + "deno_webgpu", "deno_webidl", "deno_websocket", "deno_webstorage", - "dlopen", + "dlopen2 0.6.1", "encoding_rs", "fastwebsockets", - "filetime", - "fs3", - "fwdansi", - "http 0.2.12", + "flate2", + "http 1.1.0", + "http-body-util", "hyper 0.14.28", + "hyper 1.5.1", + "hyper-util", "libc", "log", "netif", - "nix 0.26.2", + "nix 0.27.1", + "node_resolver", "notify", "ntapi", "once_cell", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "percent-encoding", + "pin-project", "regex", - "ring 0.16.20", + "rustyline", + "same-file", "serde", + "signal-hook", "signal-hook-registry", - "termcolor", + "tempfile", + "thiserror", "tokio", "tokio-metrics", + "twox-hash", "uuid", + "which 4.4.2", "winapi", - "winres", + "windows-sys 0.52.0", ] [[package]] name = "deno_semver" -version = "0.4.0" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f739a9d90c47e2af7e2fcbae0976360f3fb5292f7288a084d035ed44d12a288" +checksum = "c957c6a57c38b7dde2315df0da0ec228911e56a74f185b108a488d0401841a67" dependencies = [ "monch", "once_cell", @@ -2432,109 +2765,219 @@ dependencies = [ ] [[package]] -name = "deno_tls" -version = "0.105.0" +name = "deno_terminal" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8d622d1a59b48192258274bf58a5a6f44d4a0497fc8c4396c1a37759385417" +checksum = "7e6337d4e7f375f8b986409a76fbeecfa4bd8a1343e63355729ae4befa058eaf" +dependencies = [ + "once_cell", + "termcolor", +] + +[[package]] +name = "deno_terminal" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daef12499e89ee99e51ad6000a91f600d3937fb028ad4918af76810c5bc9e0d5" +dependencies = [ + "once_cell", + "termcolor", +] + +[[package]] +name = "deno_tls" +version = "0.166.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688175eed35e7b3053ec114227894ef24786855405d8844058a48bffa997d85a" dependencies = [ "deno_core", - "once_cell", - "rustls 0.21.10", - "rustls-native-certs", + "deno_native_certs", + "rustls 0.23.20", "rustls-pemfile", - "rustls-webpki 0.101.7", + "rustls-tokio-stream", + "rustls-webpki", "serde", - "webpki-roots 0.25.4", + "thiserror", + "tokio", + "webpki-roots", ] [[package]] name = "deno_unsync" -version = "0.1.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac0984205f25e71ddd1be603d76e70255953c12ff864707359ab195d26dfc7b3" +checksum = "d774fd83f26b24f0805a6ab8b26834a0d06ceac0db517b769b1e4633c96a2057" dependencies = [ + "futures", + "parking_lot 0.12.3", "tokio", ] [[package]] name = "deno_url" -version = "0.118.0" +version = "0.179.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9915e0eecac8ed1177b99eca8a220abfdcd8f4e7c5b0d99443b6b81eb90472b" +checksum = "ad9a108794e505f2b07665e19ff336c1bcba6adcf7182c90c1d3a6c741d7fcd0" dependencies = [ "deno_core", - "serde", + "thiserror", "urlpattern", ] [[package]] name = "deno_web" -version = "0.149.0" +version = "0.210.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c183739430b71fd42f6edee0303f13f01b31e9cf80844157b7ca37b91e369275" +checksum = "7679087bcc41f7ae3385f8c12d43bc81cfc54cb9b1ef73983d20f5e39fa4e0da" dependencies = [ "async-trait", - "base64-simd", + "base64-simd 0.8.0", "bytes", "deno_core", + "deno_permissions", "encoding_rs", "flate2", "futures", "serde", + "thiserror", "tokio", "uuid", - "windows-sys 0.48.0", +] + +[[package]] +name = "deno_webgpu" +version = "0.146.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f78b73638be1552b31778e42267f4fb47e902f7b261bdb0f951ba2b1d6bfab" +dependencies = [ + "deno_core", + "raw-window-handle", + "serde", + "thiserror", + "tokio", + "wgpu-core 0.21.1", + "wgpu-types 0.20.0", ] [[package]] name = "deno_webidl" -version = "0.118.0" +version = "0.179.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b84c418b9748f166a004eba3a67b2a8f8c1b9661bbe509c7c31cc21d7c72f6" +checksum = "5b55d845e3d64f8de7eff67aaa4b6fe1b23bbc2efe967c984f8c64c8dd85fad4" dependencies = [ "deno_core", ] [[package]] name = "deno_websocket" -version = "0.123.0" +version = "0.184.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89753655b393b285fea4bfc4ecda5c0f53d431f26f05bbe2310e8cc3feb28156" +checksum = "d00407052c6524828f2708557c47059ba9b87874758416c66f47f5102ac68422" dependencies = [ "bytes", "deno_core", "deno_net", + "deno_permissions", "deno_tls", "fastwebsockets", - "http 0.2.12", - "hyper 0.14.28", + "h2 0.4.7", + "http 1.1.0", + "http-body-util", + "hyper 1.5.1", + "hyper-util", "once_cell", + "rustls-tokio-stream", "serde", + "thiserror", "tokio", - "tokio-rustls", ] [[package]] name = "deno_webstorage" -version = "0.113.0" +version = "0.174.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03592d0706804268875215a0c6c7e03f9c14759c4796565864f95b46f1292a7d" +checksum = "7ecaabbb1580d21811642f11cc12fe8599684efeb9398eaa998a3db8811e8edc" dependencies = [ "deno_core", "deno_web", "rusqlite", - "serde", + "thiserror", ] [[package]] -name = "der" -version = "0.6.1" +name = "deno_whoami" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +checksum = "e75e4caa92b98a27f09c671d1399aee0f5970aa491b9a598523aac000a2192e3" dependencies = [ - "const-oid", - "pem-rfc7468 0.6.0", - "zeroize", + "libc", + "whoami", +] + +[[package]] +name = "denokv_proto" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7ba1f99ed11a9c11e868a8521b1f71a7e1aba785d7f42ea9ecbdc01146c89ec" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "futures", + "num-bigint", + "prost 0.13.4", + "serde", + "uuid", +] + +[[package]] +name = "denokv_remote" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ed833073189e8f6d03155fe3b05a024e75e29d8a28a4c2e9ec3b5c925e727b" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "bytes", + "chrono", + "denokv_proto", + "futures", + "http 1.1.0", + "log", + "prost 0.13.4", + "rand", + "serde", + "serde_json", + "tokio", + "tokio-util", + "url", + "uuid", +] + +[[package]] +name = "denokv_sqlite" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b790f01d1302d53a0c3cbd27de88a06b3abd64ec8ab8673924e490541c7c713" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "chrono", + "denokv_proto", + "futures", + "hex", + "log", + "num-bigint", + "rand", + "rusqlite", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "uuid", + "v8_valueserializer", ] [[package]] @@ -2544,7 +2987,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", - "pem-rfc7468 0.7.0", + "der_derive", + "pem-rfc7468", "zeroize", ] @@ -2556,12 +3000,23 @@ checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" dependencies = [ "asn1-rs", "displaydoc", - "nom", + "nom 7.1.3", "num-bigint", "num-traits", "rusticata-macros", ] +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "deranged" version = "0.3.11" @@ -2578,8 +3033,8 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 1.0.109", ] @@ -2599,8 +3054,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" dependencies = [ "darling", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -2614,34 +3069,12 @@ dependencies = [ "syn 2.0.89", ] -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case 0.4.0", - "proc-macro2 1.0.92", - "quote 1.0.37", - "rustc_version 0.4.0", - "syn 1.0.109", -] - [[package]] name = "detect-desktop-environment" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - [[package]] name = "digest" version = "0.10.7" @@ -2716,8 +3149,8 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -2731,26 +3164,38 @@ dependencies = [ ] [[package]] -name = "dlopen" -version = "0.1.8" +name = "dlopen2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937" +checksum = "6bc2c7ed06fd72a8513ded8d0d2f6fd2655a85d6885c48cae8625d80faf28c03" dependencies = [ - "dlopen_derive", - "lazy_static", + "dlopen2_derive", "libc", + "once_cell", "winapi", ] [[package]] -name = "dlopen_derive" -version = "0.1.4" +name = "dlopen2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581" +checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" dependencies = [ + "dlopen2_derive", "libc", - "quote 0.6.13", - "syn 0.15.44", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", ] [[package]] @@ -2802,11 +3247,10 @@ source = "git+https://github.com/project-gauntlet/winit.git?rev=ddf41619ffb9e1fc [[package]] name = "dprint-swc-ext" -version = "0.11.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f115ea5b6f5d0d02a25a9364f41b8c4f857452c299309dcfd29a694724d0566" +checksum = "0ba28c12892aadb751c2ba7001d8460faee4748a04b4edc51c7121cc67ee03db" dependencies = [ - "bumpalo", "num-bigint", "rustc-hash 1.1.0", "swc_atoms", @@ -2861,13 +3305,13 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" dependencies = [ - "digest 0.10.7", + "digest", "num-bigint-dig", "num-traits", - "pkcs8 0.10.2", - "rfc6979 0.4.0", + "pkcs8", + "rfc6979", "sha2", - "signature 2.2.0", + "signature", "zeroize", ] @@ -2887,8 +3331,8 @@ dependencies = [ "byteorder", "lazy_static", "proc-macro-error", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 1.0.109", ] @@ -2912,30 +3356,56 @@ dependencies = [ "cipher", ] -[[package]] -name = "ecdsa" -version = "0.14.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der 0.6.1", - "elliptic-curve 0.12.3", - "rfc6979 0.3.1", - "signature 1.6.4", -] - [[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der 0.7.9", - "digest 0.10.7", - "elliptic-curve 0.13.8", - "rfc6979 0.4.0", - "signature 2.2.0", - "spki 0.7.3", + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "ed448-goldilocks" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06924531e9e90130842b012e447f85bdaf9161bc8a0f8092be8cb70b01ebe092" +dependencies = [ + "fiat-crypto 0.1.20", + "hex", + "subtle", + "zeroize", ] [[package]] @@ -2947,45 +3417,26 @@ dependencies = [ "serde", ] -[[package]] -name = "elliptic-curve" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" -dependencies = [ - "base16ct 0.1.1", - "crypto-bigint 0.4.9", - "der 0.6.1", - "digest 0.10.7", - "ff 0.12.1", - "generic-array 0.14.7", - "group 0.12.1", - "hkdf", - "pem-rfc7468 0.6.0", - "pkcs8 0.9.0", - "rand_core 0.6.4", - "sec1 0.3.0", - "subtle", - "zeroize", -] - [[package]] name = "elliptic-curve" version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct 0.2.0", - "crypto-bigint 0.5.5", - "digest 0.10.7", - "ff 0.13.0", - "generic-array 0.14.7", - "group 0.13.0", + "base16ct", + "base64ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", "hkdf", - "pem-rfc7468 0.7.0", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "sec1 0.7.3", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "serde_json", + "serdect", "subtle", "zeroize", ] @@ -3006,15 +3457,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] -name = "enum-as-inner" -version = "0.5.1" +name = "endian-type" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck 0.4.1", - "proc-macro2 1.0.92", - "quote 1.0.37", - "syn 1.0.109", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.89", ] [[package]] @@ -3033,8 +3490,8 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -3101,6 +3558,12 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +[[package]] +name = "error_reporter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8" + [[package]] name = "etagere" version = "0.2.10" @@ -3131,12 +3594,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "5.3.1" @@ -3154,7 +3611,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ - "event-listener 5.3.1", + "event-listener", "pin-project-lite", ] @@ -3176,9 +3633,9 @@ dependencies = [ [[package]] name = "fallible-iterator" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fallible-streaming-iterator" @@ -3207,6 +3664,15 @@ dependencies = [ "serde", ] +[[package]] +name = "faster-hex" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" +dependencies = [ + "serde", +] + [[package]] name = "fastrand" version = "2.2.0" @@ -3215,12 +3681,15 @@ checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fastwebsockets" -version = "0.4.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e6185b6dc9dddc4db0dedd2e213047e93bcbf7a0fb092abc4c4e4f3195efdb4" +checksum = "26da0c7b5cef45c521a6f9cdfffdfeb6c9f5804fbac332deb5ae254634c7a6be" dependencies = [ "base64 0.21.7", - "hyper 0.14.28", + "bytes", + "http-body-util", + "hyper 1.5.1", + "hyper-util", "pin-project", "rand", "sha1", @@ -3230,6 +3699,17 @@ dependencies = [ "utf-8", ] +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "fdeflate" version = "0.3.4" @@ -3239,26 +3719,22 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "ff" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core 0.6.4", + "rand_core", "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + [[package]] name = "fiat-crypto" version = "0.2.7" @@ -3301,13 +3777,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", - "libz-ng-sys", - "miniz_oxide 0.7.2", + "miniz_oxide 0.8.0", ] [[package]] @@ -3316,6 +3791,15 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "flume" version = "0.11.0" @@ -3327,17 +3811,6 @@ dependencies = [ "spin 0.9.8", ] -[[package]] -name = "fly-accept-encoding" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3afa7516fdcfd8e5e93a938f8fec857785ced190a1f62d842d1fe1ffbe22ba8" -dependencies = [ - "http 0.2.12", - "itertools 0.10.5", - "thiserror", -] - [[package]] name = "fnv" version = "1.0.7" @@ -3401,8 +3874,8 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -3427,6 +3900,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fqdn" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb540cf7bc4fe6df9d8f7f0c974cfd0dce8ed4e9e8884e73433b503ee78b4e7d" + [[package]] name = "freedesktop-icons" version = "0.2.6" @@ -3446,18 +3925,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4" dependencies = [ - "nom", + "nom 7.1.3", "thiserror", ] [[package]] name = "from_variant" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ec5dc38ee19078d84a692b1c41181ff9f94331c76cee66ff0208c770b5e54f" +checksum = "32016f1242eb82af5474752d00fd8ebcd9004bd69b462b1c91de833972d08ed4" dependencies = [ - "pmutil", - "proc-macro2 1.0.92", + "proc-macro2", "swc_macros_common", "syn 2.0.89", ] @@ -3494,14 +3972,20 @@ dependencies = [ [[package]] name = "fslock" -version = "0.1.8" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57eafdd0c16f57161105ae1b98a1238f97645f2f588438b2949c99a2af9616bf" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" dependencies = [ "libc", "winapi", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -3553,7 +4037,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot 0.12.1", + "parking_lot 0.12.3", ] [[package]] @@ -3581,8 +4065,8 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -3616,16 +4100,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fwdansi" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c1f5787fe85505d1f7777268db5103d80a7a374d2316a7ce262e57baf8f208" -dependencies = [ - "memchr", - "termcolor", -] - [[package]] name = "gauntlet" version = "0.0.0" @@ -3704,15 +4178,6 @@ dependencies = [ "windows 0.48.0", ] -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -3744,17 +4209,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.14" @@ -3762,8 +4216,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", + "wasm-bindgen", ] [[package]] @@ -3856,7 +4312,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d4796bac3aaf0c2f8bea152ca924ae3bdc5f135caefe6431116bcd67e98eab9" dependencies = [ - "faster-hex", + "faster-hex 0.8.1", "thiserror", ] @@ -3942,8 +4398,8 @@ dependencies = [ "heck 0.4.1", "proc-macro-crate 2.0.0", "proc-macro-error", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -3957,6 +4413,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "global-hotkey" version = "0.4.2" @@ -3971,6 +4433,18 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "glow" version = "0.14.2" @@ -3983,6 +4457,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "glutin_wgl_sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" +dependencies = [ + "gl_generator", +] + [[package]] name = "glutin_wgl_sys" version = "0.6.0" @@ -4054,7 +4537,7 @@ checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" dependencies = [ "bitflags 2.6.0", "gpu-descriptor-types", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -4066,25 +4549,14 @@ dependencies = [ "bitflags 2.6.0", ] -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff 0.12.1", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff 0.13.0", - "rand_core 0.6.4", + "ff", + "rand_core", "subtle", ] @@ -4135,8 +4607,8 @@ checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro-error", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -4150,6 +4622,15 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "gzip-header" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" +dependencies = [ + "crc32fast", +] + [[package]] name = "h2" version = "0.3.26" @@ -4169,6 +4650,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.4.1" @@ -4179,6 +4679,16 @@ dependencies = [ "crunchy", ] +[[package]] +name = "halfbrown" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8588661a8607108a5ca69cab034063441a0413a0b041c13618a7dd348021ef6f" +dependencies = [ + "hashbrown 0.14.5", + "serde", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -4190,9 +4700,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash 0.8.11", "allocator-api2", @@ -4206,11 +4716,25 @@ checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "hashlink" -version = "0.8.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", +] + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64 0.21.7", + "byteorder", + "crossbeam-channel", + "flate2", + "nom 7.1.3", + "num-traits", ] [[package]] @@ -4246,6 +4770,53 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hickory-proto" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447afdcdb8afb9d0a852af6dc65d9b285ce720ed7a59e42a8bf2e931c67bc1b5" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand", + "serde", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2e2aba9c389ce5267d31cf1e4dace82390ae276b0b364ea55630b1fa1b44b4" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot 0.12.3", + "rand", + "resolv-conf", + "serde", + "smallvec", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -4261,7 +4832,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -4284,6 +4855,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "hstr" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae404c0c5d4e95d4858876ab02eecd6a196bb8caa42050dfa809938833fc412" +dependencies = [ + "hashbrown 0.14.5", + "new_debug_unreachable", + "once_cell", + "phf", + "rustc-hash 1.1.0", + "triomphe", +] + [[package]] name = "htmlescape" version = "0.3.1" @@ -4325,12 +4910,25 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0-rc.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951dfc2e32ac02d67c90c0d65bd27009a635dc9b381a2cc7d284ab01e3a0150d" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 0.2.12", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", ] [[package]] @@ -4364,7 +4962,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -4380,37 +4978,40 @@ dependencies = [ [[package]] name = "hyper" -version = "1.0.0-rc.4" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d280a71f348bcc670fc55b02b63c53a04ac0bf2daff2980795aeaf53edae10e6" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", - "http 0.2.12", - "http-body 1.0.0-rc.2", + "h2 0.4.7", + "http 1.1.0", + "http-body 1.0.1", "httparse", "httpdate", "itoa", "pin-project-lite", + "smallvec", "tokio", - "tracing", "want", ] [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", - "http 0.2.12", - "hyper 0.14.28", - "rustls 0.21.10", + "http 1.1.0", + "hyper 1.5.1", + "hyper-util", + "rustls 0.23.20", + "rustls-pki-types", "tokio", "tokio-rustls", + "tower-service", ] [[package]] @@ -4425,6 +5026,39 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper 1.5.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.5.1", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -4564,8 +5198,8 @@ source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch= dependencies = [ "darling", "manyhow", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -4680,6 +5314,124 @@ dependencies = [ "png 0.16.8", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -4688,33 +5440,23 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.3" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "idna" -version = "0.3.0" +name = "idna_adapter" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", ] [[package]] @@ -4801,8 +5543,8 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", ] [[package]] @@ -4823,7 +5565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "serde", ] @@ -4854,7 +5596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ "block-padding", - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -4875,8 +5617,8 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -4898,6 +5640,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + [[package]] name = "is-docker" version = "0.2.0" @@ -4914,8 +5665,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59a85abdc13717906baccb5a1e435556ce0df215f242892f721dff62bf25288f" dependencies = [ "Inflector", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -5038,6 +5789,30 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "junction" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be39922b087cecaba4e2d5592dedfc8bda5d4a5a1231f143337cca207950b61d" +dependencies = [ + "scopeguard", + "winapi", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + [[package]] name = "kamadak-exif" version = "0.5.5" @@ -5047,6 +5822,15 @@ dependencies = [ "mutate_once", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "keyboard-types" version = "0.7.0" @@ -5136,48 +5920,25 @@ dependencies = [ "wayland-protocols-wlr 0.3.5", ] -[[package]] -name = "lazy-regex" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff63c423c68ea6814b7da9e88ce585f793c87ddd9e78f646970891769c8235d4" -dependencies = [ - "lazy-regex-proc_macros 2.4.1", - "once_cell", - "regex", -] - [[package]] name = "lazy-regex" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c" dependencies = [ - "lazy-regex-proc_macros 3.1.0", + "lazy-regex-proc_macros", "once_cell", "regex", ] -[[package]] -name = "lazy-regex-proc_macros" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8edfc11b8f56ce85e207e62ea21557cfa09bb24a8f6b04ae181b086ff8611c22" -dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", - "regex", - "syn 1.0.109", -] - [[package]] name = "lazy-regex-proc_macros" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "regex", "syn 2.0.89", ] @@ -5326,9 +6087,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.26.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -5349,21 +6110,11 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "libz-ng-sys" -version = "1.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6409efc61b12687963e602df8ecf70e8ddacf95bc6576bcf16e3ac6328083c5" -dependencies = [ - "cmake", - "libc", -] - [[package]] name = "libz-sys" -version = "1.1.16" +version = "1.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" dependencies = [ "cc", "libc", @@ -5395,6 +6146,12 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0b5399f6804fbab912acbd8878ed3532d506b7c951b8f9f164ef90fef39e3f4" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "litrs" version = "0.4.1" @@ -5413,9 +6170,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "loom" @@ -5446,7 +6203,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -5488,7 +6245,7 @@ dependencies = [ "itertools 0.12.1", "thiserror", "tokio", - "tonic", + "tonic 0.11.0", "tracing", "tracing-subscriber", ] @@ -5501,8 +6258,8 @@ checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" dependencies = [ "darling_core", "manyhow-macros", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -5513,8 +6270,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" dependencies = [ "proc-macro-utils", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", ] [[package]] @@ -5532,12 +6289,6 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "matchit" version = "0.7.3" @@ -5561,7 +6312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest 0.10.7", + "digest", ] [[package]] @@ -5570,7 +6321,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da5ac363534dce5fabf69949225e174fbf111a498bf0ff794c8ea1fba9f3dda" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -5585,9 +6336,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" @@ -5622,15 +6373,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -5649,6 +6391,21 @@ dependencies = [ "serde", ] +[[package]] +name = "metal" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5637e166ea14be6063a3f8ba5ccb9a4159df7d8f6d61c02fc3d480b1f90dcfcb" +dependencies = [ + "bitflags 2.6.0", + "block", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "log", + "objc", + "paste", +] + [[package]] name = "metal" version = "0.29.0" @@ -5705,6 +6462,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "0.8.11" @@ -5713,15 +6479,15 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.48.0", ] [[package]] name = "monch" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4519a88847ba2d5ead3dc53f1060ec6a571de93f325d9c5c4968147382b1cbc3" +checksum = "b52c1b33ff98142aecea13138bd399b68aa7ab5d9546c300988c345004001eea" [[package]] name = "muda" @@ -5741,12 +6507,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "multimap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" - [[package]] name = "multimap" version = "0.10.0" @@ -5765,6 +6525,28 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" +[[package]] +name = "naga" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" +dependencies = [ + "arrayvec", + "bit-set 0.5.3", + "bitflags 2.6.0", + "codespan-reporting", + "hexf-parse", + "indexmap 2.2.6", + "log", + "num-traits", + "rustc-hash 1.1.0", + "serde", + "spirv", + "termcolor", + "thiserror", + "unicode-xid", +] + [[package]] name = "naga" version = "23.0.0" @@ -5772,7 +6554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d5941e45a15b53aad4375eedf02033adb7a28931eedc31117faffa52e6a857e" dependencies = [ "arrayvec", - "bit-set", + "bit-set 0.8.0", "bitflags 2.6.0", "cfg_aliases 0.1.1", "codespan-reporting", @@ -5783,7 +6565,19 @@ dependencies = [ "spirv", "termcolor", "thiserror", - "unicode-xid 0.2.6", + "unicode-xid", +] + +[[package]] +name = "napi_sym" +version = "0.109.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b3ee1b2d30885de3ee82429b5aebe6f22b3eae5cb290cd8d6537a62212812b" +dependencies = [ + "quote", + "serde", + "serde_json", + "syn 2.0.89", ] [[package]] @@ -5841,6 +6635,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "nix" version = "0.24.3" @@ -5855,16 +6658,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.2" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "cfg-if", "libc", - "memoffset 0.7.1", - "pin-utils", - "static_assertions", ] [[package]] @@ -5880,6 +6680,39 @@ dependencies = [ "memoffset 0.9.1", ] +[[package]] +name = "node_resolver" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e999e1cdbb49cdfa3f63ddd061c57205aa5f7be8f43bdbc4081c0f60d24d7d" +dependencies = [ + "anyhow", + "async-trait", + "boxed_error", + "deno_media_type", + "deno_package_json", + "deno_path_util", + "futures", + "lazy-regex", + "once_cell", + "path-clean", + "regex", + "serde_json", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "nom" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "memchr", + "version_check", +] + [[package]] name = "nom" version = "7.1.3" @@ -5898,20 +6731,21 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "notify" -version = "5.0.0" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2c66da08abae1c024c01d635253e402341b4060a12e99b31c7594063bf490a" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "crossbeam-channel", "filetime", "fsevent-sys", "inotify", "kqueue", "libc", + "log", "mio", "walkdir", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -5976,8 +6810,8 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -6059,8 +6893,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate 3.1.0", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -6377,7 +7211,7 @@ checksum = "ed29bb6f7d6ac14023acb332a356f3891265d780e254057c866dbe7a909d2d2d" dependencies = [ "ahash 0.8.11", "hashbrown 0.15.0", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "stable_deref_trait", ] @@ -6435,6 +7269,92 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab70038c28ed37b97d8ed414b6429d343a8bbf44c9f79ec854f3a643029ba6d7" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a8a7f5f6ba7c1b286c2fbca0454eaba116f63bbe69ed250b642d36fbb04d80" +dependencies = [ + "async-trait", + "bytes", + "http 1.1.0", + "opentelemetry", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" +dependencies = [ + "async-trait", + "futures-core", + "http 1.1.0", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost 0.13.4", + "serde_json", + "thiserror", + "tokio", + "tonic 0.12.3", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05acbfada5ec79023c85368af14abd0b307c015e9064d249b2a950ef459a6" +dependencies = [ + "hex", + "opentelemetry", + "opentelemetry_sdk", + "prost 0.13.4", + "serde", + "tonic 0.12.3", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc1b6902ff63b32ef6c489e8048c5e253e2e4a803ea3ea7e783914536eb15c52" + +[[package]] +name = "opentelemetry_sdk" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231e9d6ceef9b0b2546ddf52335785ce41252bc7474ee8ba05bfad277be13ab8" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "opentelemetry", + "percent-encoding", + "rand", + "serde_json", + "thiserror", + "tracing", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -6476,7 +7396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list 0.5.2", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -6502,12 +7422,12 @@ dependencies = [ [[package]] name = "os_pipe" -version = "1.2.1" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6529,12 +7449,18 @@ checksum = "b645dcde5f119c2c454a92d0dfa271a2a3b205da92e4292a68ead4bdbfde1f33" dependencies = [ "heck 0.4.1", "itertools 0.12.1", - "proc-macro2 1.0.92", + "proc-macro2", "proc-macro2-diagnostics", - "quote 1.0.37", + "quote", "syn 2.0.89", ] +[[package]] +name = "outref" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" + [[package]] name = "outref" version = "0.5.1" @@ -6571,58 +7497,50 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30c06436d66652bc2f01ade021592c80a2aad401570a18aa18b82e440d2b9aa1" dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", + "ecdsa", + "elliptic-curve", "primeorder", "sha2", ] -[[package]] -name = "p256" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" -dependencies = [ - "ecdsa 0.14.8", - "elliptic-curve 0.12.3", - "sha2", -] - [[package]] name = "p256" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", + "ecdsa", + "elliptic-curve", "primeorder", "sha2", ] -[[package]] -name = "p384" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" -dependencies = [ - "ecdsa 0.14.8", - "elliptic-curve 0.12.3", - "sha2", -] - [[package]] name = "p384" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", + "ecdsa", + "elliptic-curve", "primeorder", "sha2", ] +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core", + "sha2", +] + [[package]] name = "palette" version = "0.7.5" @@ -6632,7 +7550,7 @@ dependencies = [ "approx", "fast-srgb8", "palette_derive", - "phf 0.11.2", + "phf", ] [[package]] @@ -6641,8 +7559,8 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8890702dbec0bad9116041ae586f84805b13eecd1d8b1df27c29998a9969d6d" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -6690,9 +7608,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core 0.9.9", @@ -6732,7 +7650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -6760,19 +7678,10 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest 0.10.7", + "digest", "hmac", ] -[[package]] -name = "pem-rfc7468" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" -dependencies = [ - "base64ct", -] - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -6784,9 +7693,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" @@ -6798,35 +7707,14 @@ dependencies = [ "indexmap 2.2.6", ] -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", -] - [[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.2", - "phf_shared 0.11.2", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand", + "phf_macros", + "phf_shared", ] [[package]] @@ -6835,46 +7723,23 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ - "phf_shared 0.11.2", + "phf_shared", "rand", ] -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2 1.0.92", - "quote 1.0.37", - "syn 1.0.109", -] - [[package]] name = "phf_macros" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2 1.0.92", - "quote 1.0.37", + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", "syn 2.0.89", ] -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - [[package]] name = "phf_shared" version = "0.11.2" @@ -6905,8 +7770,8 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -6933,37 +7798,30 @@ dependencies = [ "futures-io", ] -[[package]] -name = "pkcs1" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" -dependencies = [ - "der 0.6.1", - "pkcs8 0.9.0", - "spki 0.6.0", - "zeroize", -] - [[package]] name = "pkcs1" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der 0.7.9", - "pkcs8 0.10.2", - "spki 0.7.3", + "der", + "pkcs8", + "spki", ] [[package]] -name = "pkcs8" -version = "0.9.0" +name = "pkcs5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" dependencies = [ - "der 0.6.1", - "spki 0.6.0", + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2", + "spki", ] [[package]] @@ -6972,8 +7830,10 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der 0.7.9", - "spki 0.7.3", + "der", + "pkcs5", + "rand_core", + "spki", ] [[package]] @@ -6982,12 +7842,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[package]] -name = "platforms" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" - [[package]] name = "plist" version = "1.6.1" @@ -6999,7 +7853,7 @@ dependencies = [ "line-wrap", "quick-xml 0.31.0", "serde", - "time 0.3.36", + "time", ] [[package]] @@ -7027,19 +7881,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69e940d8d8db30c6f4cc37dab9aab61f4c9cc1e6efb6d18902ab88fa09c03560" dependencies = [ "darling", - "proc-macro2 1.0.92", - "quote 1.0.37", - "syn 2.0.89", -] - -[[package]] -name = "pmutil" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" -dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -7107,12 +7950,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - [[package]] name = "presser" version = "0.3.1" @@ -7128,23 +7965,13 @@ dependencies = [ "ryu_floating_decimal", ] -[[package]] -name = "prettyplease" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" -dependencies = [ - "proc-macro2 1.0.92", - "syn 1.0.109", -] - [[package]] name = "prettyplease" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" dependencies = [ - "proc-macro2 1.0.92", + "proc-macro2", "syn 2.0.89", ] @@ -7154,7 +7981,7 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "elliptic-curve 0.13.8", + "elliptic-curve", ] [[package]] @@ -7192,8 +8019,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 1.0.109", "version_check", ] @@ -7204,16 +8031,33 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "version_check", ] [[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" +name = "proc-macro-rules" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" +checksum = "07c277e4e643ef00c1233393c673f655e3672cf7eb3ba08a00bdd0ea59139b5f" +dependencies = [ + "proc-macro-rules-macros", + "proc-macro2", + "syn 2.0.89", +] + +[[package]] +name = "proc-macro-rules-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207fffb0fe655d1d47f6af98cc2793405e85929bdbc420d685554ff07be27ac7" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.89", +] [[package]] name = "proc-macro-utils" @@ -7221,20 +8065,11 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "smallvec", ] -[[package]] -name = "proc-macro2" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -dependencies = [ - "unicode-xid 0.1.0", -] - [[package]] name = "proc-macro2" version = "1.0.92" @@ -7250,8 +8085,8 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", "version_check", "yansi", @@ -7272,20 +8107,10 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ - "quote 1.0.37", + "quote", "syn 2.0.89", ] -[[package]] -name = "prost" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" -dependencies = [ - "bytes", - "prost-derive 0.11.9", -] - [[package]] name = "prost" version = "0.12.4" @@ -7297,25 +8122,13 @@ dependencies = [ ] [[package]] -name = "prost-build" -version = "0.11.9" +name = "prost" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" dependencies = [ "bytes", - "heck 0.4.1", - "itertools 0.10.5", - "lazy_static", - "log", - "multimap 0.8.3", - "petgraph", - "prettyplease 0.1.25", - "prost 0.11.9", - "prost-types 0.11.9", - "regex", - "syn 1.0.109", - "tempfile", - "which", + "prost-derive 0.13.4", ] [[package]] @@ -7328,10 +8141,10 @@ dependencies = [ "heck 0.5.0", "itertools 0.12.1", "log", - "multimap 0.10.0", + "multimap", "once_cell", "petgraph", - "prettyplease 0.2.19", + "prettyplease", "prost 0.12.4", "prost-types 0.12.4", "regex", @@ -7340,16 +8153,23 @@ dependencies = [ ] [[package]] -name = "prost-derive" -version = "0.11.9" +name = "prost-build" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b" dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2 1.0.92", - "quote 1.0.37", - "syn 1.0.109", + "heck 0.5.0", + "itertools 0.13.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost 0.13.4", + "prost-types 0.13.4", + "regex", + "syn 2.0.89", + "tempfile", ] [[package]] @@ -7360,18 +8180,22 @@ checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" dependencies = [ "anyhow", "itertools 0.12.1", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] [[package]] -name = "prost-types" -version = "0.11.9" +name = "prost-derive" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" dependencies = [ - "prost 0.11.9", + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.89", ] [[package]] @@ -7383,6 +8207,15 @@ dependencies = [ "prost 0.12.4", ] +[[package]] +name = "prost-types" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" +dependencies = [ + "prost 0.13.4", +] + [[package]] name = "psm" version = "0.1.21" @@ -7392,6 +8225,26 @@ dependencies = [ "cc", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "qoi" version = "0.4.1" @@ -7431,22 +8284,29 @@ dependencies = [ "memchr", ] -[[package]] -name = "quote" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" -dependencies = [ - "proc-macro2 0.4.30", -] - [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ - "proc-macro2 1.0.92", + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", ] [[package]] @@ -7457,7 +8317,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -7467,16 +8327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", + "rand_core", ] [[package]] @@ -7485,7 +8336,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.14", + "getrandom", ] [[package]] @@ -7629,11 +8480,31 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom 0.2.14", + "getrandom", "libredox 0.1.3", "thiserror", ] +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "regex" version = "1.11.0" @@ -7684,51 +8555,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "async-compression", - "base64 0.21.7", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", - "hyper-rustls", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls 0.21.10", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-rustls", - "tokio-socks", - "tokio-util", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots 0.25.4", - "winreg 0.50.0", -] - [[package]] name = "resolv-conf" version = "0.7.0" @@ -7753,17 +8579,6 @@ dependencies = [ "usvg", ] -[[package]] -name = "rfc6979" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", -] - [[package]] name = "rfc6979" version = "0.4.0" @@ -7783,21 +8598,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.8" @@ -7806,10 +8606,10 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.14", + "getrandom", "libc", "spin 0.9.8", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] @@ -7839,8 +8639,8 @@ dependencies = [ "mime", "mime_guess", "once_map", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "rinja_parser", "rustc-hash 2.0.0", "serde", @@ -7854,7 +8654,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea17639e1f35032e1c67539856e498c04cd65fe2a45f55ec437ec55e4be941" dependencies = [ "memchr", - "nom", + "nom 7.1.3", "serde", ] @@ -7864,7 +8664,19 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" dependencies = [ - "digest 0.10.7", + "digest", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.6.0", + "serde", + "serde_derive", ] [[package]] @@ -7873,27 +8685,6 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" -[[package]] -name = "rsa" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c" -dependencies = [ - "byteorder", - "digest 0.10.7", - "num-bigint-dig", - "num-integer", - "num-iter", - "num-traits", - "pkcs1 0.4.1", - "pkcs8 0.9.0", - "rand_core 0.6.4", - "signature 1.6.4", - "smallvec", - "subtle", - "zeroize", -] - [[package]] name = "rsa" version = "0.9.6" @@ -7901,24 +8692,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" dependencies = [ "const-oid", - "digest 0.10.7", + "digest", "num-bigint-dig", "num-integer", "num-traits", - "pkcs1 0.7.5", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "signature 2.2.0", - "spki 0.7.3", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", "subtle", "zeroize", ] [[package]] name = "rusqlite" -version = "0.29.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ "bitflags 2.6.0", "fallible-iterator", @@ -7945,8 +8736,8 @@ version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "rust-embed-utils", "shellexpand", "syn 2.0.89", @@ -8035,7 +8826,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -8051,18 +8842,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.21.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" -dependencies = [ - "log", - "ring 0.17.8", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.22.4" @@ -8070,74 +8849,77 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", - "ring 0.17.8", + "ring", "rustls-pki-types", - "rustls-webpki 0.102.5", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls" -version = "0.23.11" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "log", "once_cell", - "ring 0.17.8", + "ring", "rustls-pki-types", - "rustls-webpki 0.102.5", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.6.3" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", "rustls-pemfile", + "rustls-pki-types", "schannel", "security-framework", ] [[package]] name = "rustls-pemfile" -version = "1.0.4" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.21.7", + "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" [[package]] -name = "rustls-webpki" -version = "0.101.7" +name = "rustls-tokio-stream" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "22557157d7395bc30727745b365d923f1ecc230c4c80b176545f3f4f08c46e33" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "futures", + "rustls 0.23.20", + "socket2", + "tokio", ] [[package]] name = "rustls-webpki" -version = "0.102.5" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ - "ring 0.17.8", + "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -8163,18 +8945,56 @@ dependencies = [ "unicode-script", ] +[[package]] +name = "rustyline" +version = "13.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "clipboard-win 5.3.1", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix 0.27.1", + "radix_trie", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + [[package]] name = "ryu" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "ryu-js" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad97d4ce1560a5e27cec89519dc8300d1aa6035b099821261c651486a19e44d5" + [[package]] name = "ryu_floating_decimal" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "700de91d5fd6091442d00fdd9ee790af6d4f0f480562b0f5a1e8f59e90aafe73" +[[package]] +name = "saffron" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fb9a628596fc7590eb7edbf7b0613287be78df107f5f97b118aad59fb2eea9" +dependencies = [ + "chrono", + "nom 5.1.3", +] + [[package]] name = "salsa20" version = "0.10.2" @@ -8202,7 +9022,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "tonic", + "tonic 0.11.0", "utils", ] @@ -8239,16 +9059,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - [[package]] name = "sctk-adwaita" version = "0.9.1" @@ -8262,53 +9072,21 @@ dependencies = [ "tiny-skia", ] -[[package]] -name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct 0.1.1", - "der 0.6.1", - "generic-array 0.14.7", - "pkcs8 0.9.0", - "subtle", - "zeroize", -] - [[package]] name = "sec1" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct 0.2.0", - "der 0.7.9", - "generic-array 0.14.7", - "pkcs8 0.10.2", + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", "subtle", "zeroize", ] -[[package]] -name = "secp256k1" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" -dependencies = [ - "rand", - "secp256k1-sys", -] - -[[package]] -name = "secp256k1-sys" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" -dependencies = [ - "cc", -] - [[package]] name = "security-framework" version = "2.10.0" @@ -8396,8 +9174,8 @@ version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -8419,8 +9197,8 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -8447,15 +9225,12 @@ dependencies = [ [[package]] name = "serde_v8" -version = "0.115.0" +version = "0.230.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f6cc041512391aabdae4dd11d51e370824ea35bfe896fb2585b6792e28c9bf" +checksum = "b5a783242d2af51d6955cc04bf2b64adb643ab588b61e9573c908a69dabf8c2f" dependencies = [ - "bytes", - "derive_more", "num-bigint", "serde", - "serde_bytes", "smallvec", "thiserror", "v8", @@ -8476,7 +9251,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_with_macros", - "time 0.3.36", + "time", ] [[package]] @@ -8486,23 +9261,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "server" version = "0.0.0" dependencies = [ "anyhow", "arboard", - "async-channel", "async-stream", "bytes", "cacao", "client", "common", + "common_plugin_runtime", "component_model", "deno_core", "deno_runtime", @@ -8533,8 +9318,8 @@ dependencies = [ "thiserror", "tokio", "tokio-util", - "toml 0.8.12", - "tonic", + "toml", + "tonic 0.11.0", "tracing", "tracing-subscriber", "typed-path", @@ -8544,18 +9329,6 @@ dependencies = [ "vergen-gitcl", "vergen-pretty", "walkdir", - "zstd-sys", -] - -[[package]] -name = "sha-1" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", ] [[package]] @@ -8566,7 +9339,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] @@ -8577,7 +9350,17 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", ] [[package]] @@ -8598,6 +9381,22 @@ dependencies = [ "dirs 5.0.1", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -8607,24 +9406,23 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - [[package]] name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", + "digest", + "rand_core", +] + +[[package]] +name = "simd-abstraction" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987" +dependencies = [ + "outref 0.1.0", ] [[package]] @@ -8633,13 +9431,28 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd-json" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2bcf6c6e164e81bc7a5d49fc6988b3d515d9e8c07457d7b74ffb9324b9cd40" +dependencies = [ + "getrandom", + "halfbrown", + "ref-cast", + "serde", + "serde_json", + "simdutf8", + "value-trait", +] + [[package]] name = "simd_helpers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" dependencies = [ - "quote 1.0.37", + "quote", ] [[package]] @@ -8706,11 +9519,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "sm3" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" +dependencies = [ + "digest", +] + [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "smartstring" @@ -8837,17 +9662,39 @@ dependencies = [ [[package]] name = "sourcemap" -version = "6.4.1" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4cbf65ca7dc576cf50e21f8d0712d96d4fcfd797389744b7b222a85cdf5bd90" +checksum = "208d40b9e8cad9f93613778ea295ed8f3c2b1824217c6cfc7219d3f6f45b96d4" dependencies = [ + "base64-simd 0.7.0", + "bitvec", "data-encoding", "debugid", "if_chain", + "rustc-hash 1.1.0", "rustc_version 0.2.3", "serde", "serde_json", - "unicode-id", + "unicode-id-start", + "url", +] + +[[package]] +name = "sourcemap" +version = "9.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c4ea7042fd1a155ad95335b5d505ab00d5124ea0332a06c8390d200bb1a76a" +dependencies = [ + "base64-simd 0.7.0", + "bitvec", + "data-encoding", + "debugid", + "if_chain", + "rustc-hash 1.1.0", + "rustc_version 0.2.3", + "serde", + "serde_json", + "unicode-id-start", "url", ] @@ -8875,16 +9722,6 @@ dependencies = [ "bitflags 2.6.0", ] -[[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der 0.6.1", -] - [[package]] name = "spki" version = "0.7.3" @@ -8892,7 +9729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der 0.7.9", + "der", ] [[package]] @@ -8902,15 +9739,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" dependencies = [ "itertools 0.12.1", - "nom", + "nom 7.1.3", "unicode_categories", ] [[package]] name = "sqlx" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" dependencies = [ "sqlx-core", "sqlx-macros", @@ -8921,24 +9758,23 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" dependencies = [ - "ahash 0.8.11", "atoi", "byteorder", "bytes", "crc", "crossbeam-queue", - "dotenvy", "either", - "event-listener 2.5.3", + "event-listener", "futures-channel", "futures-core", "futures-intrusive", "futures-io", "futures-util", + "hashbrown 0.14.5", "hashlink", "hex", "indexmap 2.2.6", @@ -8961,37 +9797,38 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.109", + "syn 2.0.89", ] [[package]] name = "sqlx-macros-core" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" dependencies = [ "dotenvy", "either", - "heck 0.4.1", + "heck 0.5.0", "hex", "once_cell", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "serde", "serde_json", "sha2", "sqlx-core", "sqlx-mysql", + "sqlx-postgres", "sqlx-sqlite", - "syn 1.0.109", + "syn 2.0.89", "tempfile", "tokio", "url", @@ -8999,24 +9836,24 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" dependencies = [ "atoi", - "base64 0.21.7", + "base64 0.22.0", "bitflags 2.6.0", "byteorder", "bytes", "crc", - "digest 0.10.7", + "digest", "dotenvy", "either", "futures-channel", "futures-core", "futures-io", "futures-util", - "generic-array 0.14.7", + "generic-array", "hex", "hkdf", "hmac", @@ -9027,7 +9864,7 @@ dependencies = [ "once_cell", "percent-encoding", "rand", - "rsa 0.9.6", + "rsa", "serde", "sha1", "sha2", @@ -9041,12 +9878,12 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" dependencies = [ "atoi", - "base64 0.21.7", + "base64 0.22.0", "bitflags 2.6.0", "byteorder", "crc", @@ -9068,7 +9905,6 @@ dependencies = [ "rand", "serde", "serde_json", - "sha1", "sha2", "smallvec", "sqlx-core", @@ -9080,9 +9916,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" dependencies = [ "atoi", "flume", @@ -9095,6 +9931,7 @@ dependencies = [ "log", "percent-encoding", "serde", + "serde_urlencoded", "sqlx-core", "tracing", "url", @@ -9143,48 +9980,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" dependencies = [ - "float-cmp", -] - -[[package]] -name = "string_cache" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" -dependencies = [ - "new_debug_unreachable", - "once_cell", - "parking_lot 0.12.1", - "phf_shared 0.10.0", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro2 1.0.92", - "quote 1.0.37", + "float-cmp 0.9.0", ] [[package]] name = "string_enum" -version = "0.4.1" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fa4d4f81d7c05b9161f8de839975d3326328b8ba2831164b465524cc2f55252" +checksum = "05e383308aebc257e7d7920224fa055c632478d92744eca77f99be8fa1545b90" dependencies = [ - "pmutil", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "swc_macros_common", "syn 2.0.89", ] +[[package]] +name = "stringcase" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04028eeb851ed08af6aba5caa29f2d59a13ed168cee4d6bd753aeefcf1d636b0" + [[package]] name = "stringprep" version = "0.1.4" @@ -9218,8 +10034,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "rustversion", "syn 2.0.89", ] @@ -9258,24 +10074,49 @@ dependencies = [ ] [[package]] -name = "swc_atoms" -version = "0.5.8" +name = "swc_allocator" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8066e17abb484602da673e2d35138ab32ce53f26368d9c92113510e1659220b" +checksum = "76aa0eb65c0f39f9b6d82a7e5192c30f7ac9a78f084a21f270de1d8c600ca388" dependencies = [ - "once_cell", + "bumpalo", + "hashbrown 0.14.5", + "ptr_meta", "rustc-hash 1.1.0", - "serde", - "string_cache", - "string_cache_codegen", "triomphe", ] [[package]] -name = "swc_common" -version = "0.31.21" +name = "swc_atoms" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de5823ef063f116ad281cde9700f5be6dfb182e543ce3f62c42cee1c03ffbc6b" +checksum = "bb6567e4e67485b3e7662b486f1565bdae54bd5b9d6b16b2ba1a9babb1e42125" +dependencies = [ + "hstr", + "once_cell", + "rustc-hash 1.1.0", + "serde", +] + +[[package]] +name = "swc_cached" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83406221c501860fce9c27444f44125eafe9e598b8b81be7563d7036784cd05c" +dependencies = [ + "ahash 0.8.11", + "anyhow", + "dashmap", + "once_cell", + "regex", + "serde", +] + +[[package]] +name = "swc_common" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d0a8eaaf1606c9207077d75828008cb2dfb51b095a766bd2b72ef893576e31" dependencies = [ "ast_node", "better_scoped_tls", @@ -9288,8 +10129,8 @@ dependencies = [ "rustc-hash 1.1.0", "serde", "siphasher 0.3.11", - "sourcemap", - "string_cache", + "sourcemap 9.1.2", + "swc_allocator", "swc_atoms", "swc_eq_ignore_macros", "swc_visit", @@ -9300,58 +10141,60 @@ dependencies = [ [[package]] name = "swc_config" -version = "0.1.7" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba1c7a40d38f9dd4e9a046975d3faf95af42937b34b2b963be4d8f01239584b" +checksum = "4740e53eaf68b101203c1df0937d5161a29f3c13bceed0836ddfe245b72dd000" dependencies = [ - "indexmap 1.9.3", + "anyhow", + "indexmap 2.2.6", "serde", "serde_json", + "swc_cached", "swc_config_macro", ] [[package]] name = "swc_config_macro" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5b5aaca9a0082be4515f0fbbecc191bf5829cd25b5b9c0a2810f6a2bb0d6829" +checksum = "7c5f56139042c1a95b54f5ca48baa0e0172d369bcc9d3d473dad1de36bae8399" dependencies = [ - "pmutil", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "swc_macros_common", "syn 2.0.89", ] [[package]] name = "swc_ecma_ast" -version = "0.107.7" +version = "0.118.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7191c8c57af059b75a2aadc927a2608c3962d19e4d09ce8f9c3f03739ddf833" +checksum = "a6f866d12e4d519052b92a0a86d1ac7ff17570da1272ca0c89b3d6f802cd79df" dependencies = [ "bitflags 2.6.0", "is-macro", "num-bigint", + "phf", "scoped-tls", "serde", "string_enum", "swc_atoms", "swc_common", - "unicode-id", + "unicode-id-start", ] [[package]] name = "swc_ecma_codegen" -version = "0.142.17" +version = "0.155.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4e3ee8a1f0bfaf630febbe0f6a03f2c28d66d373a9bbdb3f500f6bfb536b43" +checksum = "cc7641608ef117cfbef9581a99d02059b522fcca75e5244fa0cbbd8606689c6f" dependencies = [ "memchr", "num-bigint", "once_cell", - "rustc-hash 1.1.0", "serde", - "sourcemap", + "sourcemap 9.1.2", + "swc_allocator", "swc_atoms", "swc_common", "swc_ecma_ast", @@ -9361,39 +10204,41 @@ dependencies = [ [[package]] name = "swc_ecma_codegen_macros" -version = "0.7.3" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcdff076dccca6cc6a0e0b2a2c8acfb066014382bc6df98ec99e755484814384" +checksum = "859fabde36db38634f3fad548dd5e3410c1aebba1b67a3c63e67018fa57a0bca" dependencies = [ - "pmutil", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "swc_macros_common", "syn 2.0.89", ] [[package]] name = "swc_ecma_loader" -version = "0.43.23" +version = "0.49.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f47bb1ab686f603da93a8b6e559d69b42369ab47d5dee6bdda38ae5902dc2a" +checksum = "55fa3d55045b97894bfb04d38aff6d6302ac8a6a38e3bb3dfb0d20475c4974a9" dependencies = [ "anyhow", "pathdiff", "serde", + "swc_atoms", "swc_common", "tracing", ] [[package]] name = "swc_ecma_parser" -version = "0.137.15" +version = "0.149.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c0d554865a63bfa58cf1c433fa91d7d4adf40030fa8e4530e8065d0578166a" +checksum = "683dada14722714588b56481399c699378b35b2ba4deb5c4db2fb627a97fb54b" dependencies = [ "either", + "new_debug_unreachable", "num-bigint", "num-traits", + "phf", "serde", "smallvec", "smartstring", @@ -9407,15 +10252,15 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_base" -version = "0.130.24" +version = "0.145.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8d8ca5dd849cea79e6a9792d725f4082ad3ade7a9541fba960c42d55ae778f2" +checksum = "65f21494e75d0bd8ef42010b47cabab9caaed8f2207570e809f6f4eb51a710d1" dependencies = [ "better_scoped_tls", "bitflags 2.6.0", - "indexmap 1.9.3", + "indexmap 2.2.6", "once_cell", - "phf 0.10.1", + "phf", "rustc-hash 1.1.0", "serde", "smallvec", @@ -9430,9 +10275,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_classes" -version = "0.119.24" +version = "0.134.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a09d0e350963d4fb14bf9dc31c85eb28e58a88614e779c75f49296710f9cb381" +checksum = "3c3d884594385bea9405a2e1721151470d9a14d3ceec5dd773c0ca6894791601" dependencies = [ "swc_atoms", "swc_common", @@ -9444,22 +10289,21 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_macros" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59c4b6ed5d78d3ad9fc7c6f8ab4f85bba99573d31d9a2c0a712077a6b45efd2" +checksum = "500a1dadad1e0e41e417d633b3d6d5de677c9e0d3159b94ba3348436cdb15aab" dependencies = [ - "pmutil", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "swc_macros_common", "syn 2.0.89", ] [[package]] name = "swc_ecma_transforms_proposal" -version = "0.164.30" +version = "0.179.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62d3a04de35f6c79d8f343822138e7313934d3530cc4e4f891a079f7e2415c1a" +checksum = "79938ff510fc647febd8c6c3ef4143d099fdad87a223680e632623d056dae2dd" dependencies = [ "either", "rustc-hash 1.1.0", @@ -9477,17 +10321,18 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_react" -version = "0.176.34" +version = "0.191.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "607017e6fbfe3229b69ffce7b47383eb9b62025ea93a50cd1cc1788d2a29a4ca" +checksum = "76c76d8b9792ce51401d38da0fa62158d61f6d80d16d68fe5b03ce4bf5fba383" dependencies = [ - "base64 0.13.1", + "base64 0.21.7", "dashmap", - "indexmap 1.9.3", + "indexmap 2.2.6", "once_cell", "serde", - "sha-1", + "sha1", "string_enum", + "swc_allocator", "swc_atoms", "swc_common", "swc_config", @@ -9501,10 +10346,11 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_typescript" -version = "0.180.33" +version = "0.198.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea349e787a62af0dcf1b8b52d507045345871571c18cb78a2f892912f7d6b753" +checksum = "15455da4768f97186c40523e83600495210c11825d3a44db43383fd81eace88d" dependencies = [ + "ryu-js", "serde", "swc_atoms", "swc_common", @@ -9517,14 +10363,15 @@ dependencies = [ [[package]] name = "swc_ecma_utils" -version = "0.120.19" +version = "0.134.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb60e20e1eb9e9f7c88d99ac8659fd0561d70abd27853f550fbd907a448c878" +checksum = "029eec7dd485923a75b5a45befd04510288870250270292fc2c1b3a9e7547408" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.2.6", "num_cpus", "once_cell", "rustc-hash 1.1.0", + "ryu-js", "swc_atoms", "swc_common", "swc_ecma_ast", @@ -9535,10 +10382,11 @@ dependencies = [ [[package]] name = "swc_ecma_visit" -version = "0.93.7" +version = "0.104.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb23a48abd9f5731b6275dbf4ea89f6e03dc60b7c8e3e1e383bb4a6c39fd7e25" +checksum = "5b1c6802e68e51f336e8bc9644e9ff9da75d7da9c1a6247d532f2e908aa33e81" dependencies = [ + "new_debug_unreachable", "num-bigint", "swc_atoms", "swc_common", @@ -9549,71 +10397,57 @@ dependencies = [ [[package]] name = "swc_eq_ignore_macros" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a95d367e228d52484c53336991fdcf47b6b553ef835d9159db4ba40efb0ee8" +checksum = "63db0adcff29d220c3d151c5b25c0eabe7e32dd936212b84cdaa1392e3130497" dependencies = [ - "pmutil", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] [[package]] name = "swc_macros_common" -version = "0.3.8" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a273205ccb09b51fabe88c49f3b34c5a4631c4c00a16ae20e03111d6a42e832" +checksum = "f486687bfb7b5c560868f69ed2d458b880cebc9babebcb67e49f31b55c5bf847" dependencies = [ - "pmutil", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] [[package]] name = "swc_visit" -version = "0.5.7" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c337fbb2d191bf371173dea6a957f01899adb8f189c6c31b122a6cfc98fc3" +checksum = "1ceb044142ba2719ef9eb3b6b454fce61ab849eb696c34d190f04651955c613d" dependencies = [ "either", - "swc_visit_macros", + "new_debug_unreachable", ] [[package]] name = "swc_visit_macros" -version = "0.5.8" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f322730fb82f3930a450ac24de8c98523af7d34ab8cb2f46bcb405839891a99" +checksum = "92807d840959f39c60ce8a774a3f83e8193c658068e6d270dbe0a05e40e90b41" dependencies = [ "Inflector", - "pmutil", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "swc_macros_common", "syn 2.0.89", ] -[[package]] -name = "syn" -version = "0.15.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "unicode-xid 0.1.0", -] - [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "unicode-ident", ] @@ -9623,8 +10457,8 @@ version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "unicode-ident", ] @@ -9634,16 +10468,33 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "synstructure" version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 1.0.109", - "unicode-xid 0.2.6", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", ] [[package]] @@ -9655,27 +10506,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "system-deps" version = "6.2.2" @@ -9685,7 +10515,7 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.12", + "toml", "version-compare", ] @@ -9735,7 +10565,7 @@ dependencies = [ "tantivy-tokenizer-api", "tempfile", "thiserror", - "time 0.3.36", + "time", "uuid", "winapi", ] @@ -9775,7 +10605,7 @@ dependencies = [ "byteorder", "ownedbytes", "serde", - "time 0.3.36", + "time", ] [[package]] @@ -9795,7 +10625,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -9830,6 +10660,12 @@ dependencies = [ "serde", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-lexicon" version = "0.12.14" @@ -9882,8 +10718,8 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -9908,17 +10744,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - [[package]] name = "time" version = "0.3.36" @@ -10000,6 +10825,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -10026,7 +10861,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2", @@ -10034,6 +10869,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "tokio-eld" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9166030f05d6bc5642bdb8f8c2be31eb3c02cd465d662bcdc2df82d4aa41a584" +dependencies = [ + "hdrhistogram", + "tokio", +] + [[package]] name = "tokio-io-timeout" version = "1.2.0" @@ -10050,8 +10895,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -10069,11 +10914,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.21.10", + "rustls 0.23.20", "tokio", ] @@ -10091,9 +10936,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -10108,20 +10953,15 @@ checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", + "futures-util", + "hashbrown 0.14.5", "pin-project-lite", + "slab", "tokio", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "0.8.12" @@ -10197,14 +11037,14 @@ checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.6.20", "base64 0.21.7", "bytes", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", - "hyper-timeout", + "hyper-timeout 0.4.1", "percent-encoding", "pin-project", "prost 0.12.4", @@ -10216,16 +11056,46 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum 0.7.5", + "base64 0.22.0", + "bytes", + "h2 0.4.7", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.1", + "hyper-timeout 0.5.1", + "hyper-util", + "percent-encoding", + "pin-project", + "prost 0.13.4", + "socket2", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tonic-build" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4ef6dd70a610078cb4e338a0f79d06bc759ff1b22d2120c2ff02ae264ba9c2" dependencies = [ - "prettyplease 0.2.19", - "proc-macro2 1.0.92", + "prettyplease", + "proc-macro2", "prost-build 0.12.4", - "quote 1.0.37", + "quote", "syn 2.0.89", ] @@ -10250,10 +11120,30 @@ dependencies = [ ] [[package]] -name = "tower-layer" -version = "0.3.2" +name = "tower-http" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +dependencies = [ + "async-compression", + "bitflags 2.6.0", + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" @@ -10279,8 +11169,8 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -10352,68 +11242,21 @@ dependencies = [ "fnv", "home", "memchr", - "nom", + "nom 7.1.3", "once_cell", "petgraph", ] [[package]] name = "triomphe" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" dependencies = [ "serde", "stable_deref_trait", ] -[[package]] -name = "trust-dns-proto" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna 0.2.3", - "ipnet", - "lazy_static", - "rand", - "serde", - "smallvec", - "thiserror", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "trust-dns-resolver" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" -dependencies = [ - "cfg-if", - "futures-util", - "ipconfig", - "lazy_static", - "lru-cache", - "parking_lot 0.12.1", - "resolv-conf", - "serde", - "smallvec", - "thiserror", - "tokio", - "tracing", - "trust-dns-proto", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -10432,6 +11275,17 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "rand", + "static_assertions", +] + [[package]] name = "typed-arena" version = "2.0.2" @@ -10541,6 +11395,12 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" +[[package]] +name = "unicode-id-start" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f322b60f6b9736017344fa0635d64be2f458fbc04eef65f6be22976dd1ffd5b" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -10582,15 +11442,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" @@ -10614,12 +11468,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -10636,31 +11484,30 @@ dependencies = [ "flate2", "log", "once_cell", - "rustls 0.23.11", + "rustls 0.23.20", "rustls-pki-types", "url", - "webpki-roots 0.26.3", + "webpki-roots", ] [[package]] name = "url" -version = "2.4.1" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 0.4.0", + "idna", "percent-encoding", "serde", ] [[package]] name = "urlpattern" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9bd5ff03aea02fa45b13a7980151fe45009af1980ba69f651ec367121a31609" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" dependencies = [ - "derive_more", "regex", "serde", "unic-ucd-ident", @@ -10695,12 +11542,24 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8-ranges" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.1" @@ -10721,20 +11580,40 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom 0.2.14", + "getrandom", "serde", ] [[package]] name = "v8" -version = "0.74.3" +version = "130.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eedac634b8dd39b889c5b62349cbc55913780226239166435c5cf66771792ea" +checksum = "2ee0be58935708fa4d7efb970c6cf9f2d9511d24ee24246481a65b6ee167348d" dependencies = [ - "bitflags 1.3.2", + "bindgen", + "bitflags 2.6.0", "fslock", + "gzip-header", + "home", + "miniz_oxide 0.7.2", "once_cell", - "which", + "paste", + "which 6.0.3", +] + +[[package]] +name = "v8_valueserializer" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97599c400fc79925922b58303e98fcb8fa88f573379a08ddb652e72cbd2e70f6" +dependencies = [ + "bitflags 2.6.0", + "encoding_rs", + "indexmap 2.2.6", + "num-bigint", + "serde", + "thiserror", + "wtf8", ] [[package]] @@ -10754,6 +11633,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "value-trait" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9170e001f458781e92711d2ad666110f153e4e50bfd5cbd02db6547625714187" +dependencies = [ + "float-cmp 0.10.0", + "halfbrown", + "itoa", + "ryu", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -10771,7 +11662,7 @@ dependencies = [ "derive_builder", "regex", "rustversion", - "time 0.3.36", + "time", "vergen-lib", ] @@ -10784,7 +11675,7 @@ dependencies = [ "anyhow", "derive_builder", "rustversion", - "time 0.3.36", + "time", "vergen", "vergen-lib", ] @@ -10807,7 +11698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9579e7d75e528471646aa8c9bf11392154f3f8e0b02d94211bc5d57701d010f0" dependencies = [ "anyhow", - "convert_case 0.6.0", + "convert_case", "derive_builder", "rustversion", ] @@ -10830,27 +11721,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" -[[package]] -name = "vte" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" -dependencies = [ - "arrayvec", - "utf8parse", - "vte_generate_state_changes", -] - -[[package]] -name = "vte_generate_state_changes" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" -dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -10870,18 +11740,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -10914,8 +11772,8 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", "wasm-bindgen-shared", ] @@ -10938,7 +11796,7 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ - "quote 1.0.37", + "quote", "wasm-bindgen-macro-support", ] @@ -10948,8 +11806,8 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", "wasm-bindgen-backend", "wasm-bindgen-shared", @@ -10961,19 +11819,6 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" -[[package]] -name = "wasm-streams" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "wasm-timer" version = "0.2.5" @@ -10989,6 +11834,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm_dep_analyzer" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f270206a91783fd90625c8bb0d8fbd459d0b1d1bf209b656f713f01ae7c04b8" +dependencies = [ + "thiserror", +] + [[package]] name = "waycrate_xkbkeycode" version = "0.13.99" @@ -11173,8 +12027,8 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "xml-rs", ] @@ -11184,9 +12038,9 @@ version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ - "proc-macro2 1.0.92", + "proc-macro2", "quick-xml 0.36.2", - "quote 1.0.37", + "quote", ] [[package]] @@ -11231,10 +12085,13 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.25.4" +name = "webpki-root-certs" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "9cd5da49bdf1f30054cfe0b8ce2958b8fbeb67c4d82c8967a598af481bef255c" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "webpki-roots" @@ -11262,8 +12119,8 @@ dependencies = [ "document-features", "js-sys", "log", - "naga", - "parking_lot 0.12.1", + "naga 23.0.0", + "parking_lot 0.12.3", "profiling", "raw-window-handle", "smallvec", @@ -11271,9 +12128,38 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", + "wgpu-core 23.0.0", + "wgpu-hal 23.0.0", + "wgpu-types 23.0.0", +] + +[[package]] +name = "wgpu-core" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50819ab545b867d8a454d1d756b90cd5f15da1f2943334ca314af10583c9d39" +dependencies = [ + "arrayvec", + "bit-vec 0.6.3", + "bitflags 2.6.0", + "cfg_aliases 0.1.1", + "codespan-reporting", + "document-features", + "indexmap 2.2.6", + "log", + "naga 0.20.0", + "once_cell", + "parking_lot 0.12.3", + "profiling", + "raw-window-handle", + "ron", + "rustc-hash 1.1.0", + "serde", + "smallvec", + "thiserror", + "web-sys", + "wgpu-hal 0.21.1", + "wgpu-types 0.20.0", ] [[package]] @@ -11283,22 +12169,64 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e0c68e7b6322a03ee5b83fcd92caeac5c2a932f6457818179f4652ad2a9c065" dependencies = [ "arrayvec", - "bit-vec", + "bit-vec 0.8.0", "bitflags 2.6.0", "cfg_aliases 0.1.1", "document-features", "indexmap 2.2.6", "log", - "naga", + "naga 23.0.0", "once_cell", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "profiling", "raw-window-handle", "rustc-hash 1.1.0", "smallvec", "thiserror", - "wgpu-hal", - "wgpu-types", + "wgpu-hal 23.0.0", + "wgpu-types 23.0.0", +] + +[[package]] +name = "wgpu-hal" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172e490a87295564f3fcc0f165798d87386f6231b04d4548bca458cbbfd63222" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash 0.37.3+1.3.251", + "bit-set 0.5.3", + "bitflags 2.6.0", + "block", + "cfg_aliases 0.1.1", + "core-graphics-types 0.1.3", + "d3d12", + "glow 0.13.1", + "glutin_wgl_sys 0.5.0", + "gpu-alloc", + "gpu-descriptor", + "js-sys", + "khronos-egl", + "libc", + "libloading 0.8.3", + "log", + "metal 0.28.0", + "naga 0.20.0", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot 0.12.3", + "profiling", + "range-alloc", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror", + "wasm-bindgen", + "web-sys", + "wgpu-types 0.20.0", + "winapi", ] [[package]] @@ -11309,15 +12237,15 @@ checksum = "de6e7266b869de56c7e3ed72a954899f71d14fec6cc81c102b7530b92947601b" dependencies = [ "android_system_properties", "arrayvec", - "ash", - "bit-set", + "ash 0.38.0+1.3.281", + "bit-set 0.8.0", "bitflags 2.6.0", "block", "bytemuck", "cfg_aliases 0.1.1", "core-graphics-types 0.1.3", - "glow", - "glutin_wgl_sys", + "glow 0.14.2", + "glutin_wgl_sys 0.6.0", "gpu-alloc", "gpu-allocator", "gpu-descriptor", @@ -11326,12 +12254,12 @@ dependencies = [ "libc", "libloading 0.8.3", "log", - "metal", - "naga", + "metal 0.29.0", + "naga 23.0.0", "ndk-sys 0.5.0+25.2.9519653", "objc", "once_cell", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "profiling", "range-alloc", "raw-window-handle", @@ -11341,11 +12269,23 @@ dependencies = [ "thiserror", "wasm-bindgen", "web-sys", - "wgpu-types", + "wgpu-types 23.0.0", "windows 0.58.0", "windows-core 0.58.0", ] +[[package]] +name = "wgpu-types" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1353d9a46bff7f955a680577f34c69122628cc2076e1d6f3a9be6ef00ae793ef" +dependencies = [ + "bitflags 2.6.0", + "js-sys", + "serde", + "web-sys", +] + [[package]] name = "wgpu-types" version = "23.0.0" @@ -11369,6 +12309,18 @@ dependencies = [ "rustix", ] +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix", + "winsafe", +] + [[package]] name = "whoami" version = "1.5.1" @@ -11487,8 +12439,8 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -11498,8 +12450,8 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -11825,13 +12777,10 @@ dependencies = [ ] [[package]] -name = "winres" -version = "0.1.12" +name = "winsafe" +version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" -dependencies = [ - "toml 0.5.11", -] +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wl-clipboard-rs" @@ -11851,6 +12800,33 @@ dependencies = [ "wayland-protocols 0.29.5", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wtf8" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01ae8492c38f52376efd3a17d0994b6bcf3df1e39c0226d458b7d81670b2a06" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -11911,8 +12887,8 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ - "curve25519-dalek 4.1.2", - "rand_core 0.6.4", + "curve25519-dalek", + "rand_core", "serde", "zeroize", ] @@ -11927,11 +12903,11 @@ dependencies = [ "data-encoding", "der-parser", "lazy_static", - "nom", + "nom 7.1.3", "oid-registry", "rusticata-macros", "thiserror", - "time 0.3.36", + "time", ] [[package]] @@ -11999,6 +12975,30 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", + "synstructure 0.13.1", +] + [[package]] name = "zbus" version = "4.4.0" @@ -12016,7 +13016,7 @@ dependencies = [ "async-trait", "blocking", "enumflags2", - "event-listener 5.3.1", + "event-listener", "futures-core", "futures-sink", "futures-util", @@ -12044,8 +13044,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" dependencies = [ "proc-macro-crate 3.1.0", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", "zvariant_utils", ] @@ -12082,11 +13082,32 @@ version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", + "synstructure 0.13.1", +] + [[package]] name = "zeroize" version = "1.7.0" @@ -12102,8 +13123,30 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", "syn 2.0.89", ] @@ -12179,8 +13222,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" dependencies = [ "proc-macro-crate 3.1.0", - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", "zvariant_utils", ] @@ -12191,7 +13234,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.37", + "proc-macro2", + "quote", "syn 2.0.89", ] diff --git a/Cargo.toml b/Cargo.toml index e555f27..8224617 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "rust/server", "rust/common", "rust/common_ui", + "rust/common_plugin_runtime", "rust/utils", "rust/cli", "rust/component_model", diff --git a/rust/common_plugin_runtime/Cargo.toml b/rust/common_plugin_runtime/Cargo.toml new file mode 100644 index 0000000..08cdd9d --- /dev/null +++ b/rust/common_plugin_runtime/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "common_plugin_runtime" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +anyhow = "1.0.94" +common = { path = "../common" } diff --git a/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs b/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs new file mode 100644 index 0000000..8593a5b --- /dev/null +++ b/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs @@ -0,0 +1,22 @@ +use std::collections::HashMap; +use common::model::{EntrypointId, PhysicalKey}; +use crate::model::{AdditionalSearchItem, PreferenceUserData}; + +pub trait BackendForPluginRuntimeApi { + async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> ; + async fn get_asset_data(&self, path: &str) -> anyhow::Result>; + async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result>; + async fn get_action_id_for_shortcut( + &self, + entrypoint_id: &str, + key: PhysicalKey, + modifier_shift: bool, + modifier_control: bool, + modifier_alt: bool, + modifier_meta: bool + ) -> anyhow::Result>; + async fn get_plugin_preferences(&self) -> anyhow::Result>; + async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result>; + async fn plugin_preferences_required(&self) -> anyhow::Result; + async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result; +} \ No newline at end of file diff --git a/rust/common_plugin_runtime/src/lib.rs b/rust/common_plugin_runtime/src/lib.rs new file mode 100644 index 0000000..1d557a6 --- /dev/null +++ b/rust/common_plugin_runtime/src/lib.rs @@ -0,0 +1,2 @@ +pub mod backend_for_plugin_runtime_api; +pub mod model; diff --git a/rust/common_plugin_runtime/src/model.rs b/rust/common_plugin_runtime/src/model.rs new file mode 100644 index 0000000..3969ca3 --- /dev/null +++ b/rust/common_plugin_runtime/src/model.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize)] +pub struct AdditionalSearchItem { + pub entrypoint_name: String, + pub generator_entrypoint_id: String, + pub entrypoint_id: String, + pub entrypoint_uuid: String, + pub entrypoint_icon: Option>, + pub entrypoint_actions: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct AdditionalSearchItemAction { + pub id: Option, + pub label: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum PreferenceUserData { + Number(f64), + String(String), + Bool(bool), + ListOfStrings(Vec), + ListOfNumbers(Vec), +} \ No newline at end of file diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 2ae2e1f..24687fe 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -22,6 +22,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } sqlx = { version = "0.8.2", features = [ "runtime-tokio", "json", "sqlite" ] } common = { path = "../common" } +common_plugin_runtime = { path = "../common_plugin_runtime" } utils = { path = "../utils" } component_model = { path = "../component_model" } indexmap = { version = "2.1.0", features = ["serde"] } diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 6f24a21..2eeba9e 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -149,16 +149,6 @@ pub enum IntermediateUiEvent { RefreshSearchIndex, } -#[derive(Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub enum PreferenceUserData { - Number(f64), - String(String), - Bool(bool), - ListOfStrings(Vec), - ListOfNumbers(Vec), -} - pub enum ActionShortcutKey { Num0, Num1, diff --git a/rust/server/src/plugins/js/assets.rs b/rust/server/src/plugins/js/assets.rs index 327ef28..f3b770d 100644 --- a/rust/server/src/plugins/js/assets.rs +++ b/rust/server/src/plugins/js/assets.rs @@ -1,55 +1,46 @@ use crate::plugins::data_db_repository::DataDbRepository; -use crate::plugins::js::PluginData; +use crate::plugins::js::{BackendForPluginRuntimeApiImpl, PluginData}; use deno_core::futures::executor::block_on; use deno_core::{op2, OpState}; use std::cell::RefCell; use std::rc::Rc; +use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; #[op2(async)] #[buffer] pub async fn asset_data(state: Rc>, #[string] path: String) -> anyhow::Result> { - let (plugin_id, repository) = { + let api = { let state = state.borrow(); - let plugin_id = state - .borrow::() - .plugin_id() + let api = state + .borrow::() .clone(); - let repository = state - .borrow::() - .clone(); - - (plugin_id, repository) + api }; tracing::trace!(target = "renderer_rs", "Fetching asset data {:?}", path); - repository.get_asset_data(&plugin_id.to_string(), &path).await + api.get_asset_data(&path).await } #[op2] #[buffer] pub fn asset_data_blocking(state: Rc>, #[string] path: String) -> anyhow::Result> { - let (plugin_id, repository) = { + let api = { let state = state.borrow(); - let plugin_id = state - .borrow::() - .plugin_id() + let api = state + .borrow::() .clone(); - let repository = state - .borrow::() - .clone(); - - (plugin_id, repository) + api }; tracing::trace!(target = "renderer_rs", "Fetching asset data blocking {:?}", path); block_on(async { - let data = repository.get_asset_data(&plugin_id.to_string(), &path).await?; + let data = api.get_asset_data(&path).await?; Ok(data) }) diff --git a/rust/server/src/plugins/js/command_generators.rs b/rust/server/src/plugins/js/command_generators.rs index 78acd03..c5b91a1 100644 --- a/rust/server/src/plugins/js/command_generators.rs +++ b/rust/server/src/plugins/js/command_generators.rs @@ -1,34 +1,21 @@ -use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginEntrypointType}; -use crate::plugins::js::PluginData; +use crate::plugins::js::BackendForPluginRuntimeApiImpl; +use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; use deno_core::{op2, OpState}; use std::cell::RefCell; use std::rc::Rc; - #[op2(async)] #[serde] pub async fn get_command_generator_entrypoint_ids(state: Rc>) -> anyhow::Result> { - let (plugin_id, repository) = { + let api = { let state = state.borrow(); - let plugin_id = state - .borrow::() - .plugin_id() + let api = state + .borrow::() .clone(); - let repository = state - .borrow::() - .clone(); - - (plugin_id, repository) + api }; - let result = repository.get_entrypoints_by_plugin_id(&plugin_id.to_string()).await? - .into_iter() - .filter(|entrypoint| entrypoint.enabled) - .filter(|entrypoint| matches!(db_entrypoint_from_str(&entrypoint.entrypoint_type), DbPluginEntrypointType::CommandGenerator)) - .map(|entrypoint| entrypoint.id) - .collect::>(); - - Ok(result) + api.get_command_generator_entrypoint_ids().await } diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 7063fc2..d48bf6c 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -32,9 +32,11 @@ use tokio_util::sync::CancellationToken; use common::dirs::Dirs; use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; use common::rpc::frontend_api::FrontendApi; +use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; +use common_plugin_runtime::model::{AdditionalSearchItem, PreferenceUserData}; use component_model::{create_component_model, Children, Component, Property, PropertyType, SharedType}; -use crate::model::{IntermediateUiEvent, JsKeyboardEventOrigin, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData, PreferenceUserData}; +use crate::model::{IntermediateUiEvent, JsKeyboardEventOrigin, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData}; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; @@ -50,7 +52,7 @@ use crate::plugins::js::preferences::{entrypoint_preferences_required, get_entry use crate::plugins::js::search::reload_search_index; use crate::plugins::js::ui::{clear_inline_view, fetch_action_id_for_shortcut, op_component_model, op_inline_view_endpoint_id, op_react_replace_view, show_hud, show_plugin_error_view, show_preferences_required_view, update_loading_bar}; use crate::plugins::run_status::RunStatusGuard; -use crate::search::{SearchIndex, SearchIndexItem}; +use crate::search::{SearchIndex, SearchIndexItem, SearchIndexItemAction}; mod ui; mod plugins; @@ -348,8 +350,8 @@ async fn start_js_runtime( gauntlet::init_ops( EventReceiver::new(event_stream), PluginData::new( - plugin_id, - plugin_uuid, + plugin_id.clone(), + plugin_uuid.clone(), plugin_name, entrypoint_names, plugin_cache_dir, @@ -359,9 +361,13 @@ async fn start_js_runtime( ), frontend_api, ComponentModel::new(component_model), - repository, - search_index, - icon_cache, + BackendForPluginRuntimeApiImpl::new( + icon_cache, + repository, + search_index, + plugin_uuid, + plugin_id + ), numbat_context, clipboard, ), @@ -593,9 +599,7 @@ deno_core::extension!( plugin_data: PluginData, frontend_api: FrontendApi, component_model: ComponentModel, - db_repository: DataDbRepository, - search_index: SearchIndex, - icon_cache: IconCache, + backend_api: BackendForPluginRuntimeApiImpl, numbat_context: Option, clipboard: Clipboard, }, @@ -604,9 +608,7 @@ deno_core::extension!( state.put(options.plugin_data); state.put(options.frontend_api); state.put(options.component_model); - state.put(options.db_repository); - state.put(options.search_index); - state.put(options.icon_cache); + state.put(options.backend_api); state.put(options.numbat_context); state.put(options.clipboard); }, @@ -966,3 +968,300 @@ impl EventReceiver { } } } + +#[derive(Clone)] +pub struct BackendForPluginRuntimeApiImpl { + icon_cache: IconCache, + repository: DataDbRepository, + search_index: SearchIndex, + plugin_uuid: String, + plugin_id: PluginId, +} + +impl BackendForPluginRuntimeApiImpl { + fn new( + icon_cache: IconCache, + repository: DataDbRepository, + search_index: SearchIndex, + plugin_uuid: String, + plugin_id: PluginId, + ) -> Self { + Self { + icon_cache, + repository, + search_index, + plugin_uuid, + plugin_id, + } + } +} + +impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { + async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { + self.icon_cache.clear_plugin_icon_cache_dir(&self.plugin_uuid) + .context("error when clearing up icon cache before recreating it")?; + + let DbReadPlugin { name, .. } = self.repository.get_plugin_by_id(&self.plugin_id.to_string()) + .await + .context("error when getting plugin by id")?; + + let entrypoints = self.repository.get_entrypoints_by_plugin_id(&self.plugin_id.to_string()) + .await + .context("error when getting entrypoints by plugin id")?; + + let frecency_map = self.repository.get_frecency_for_plugin(&self.plugin_id.to_string()) + .await + .context("error when getting frecency for plugin")?; + + let mut shortcuts = HashMap::new(); + + for DbReadPluginEntrypoint { id, .. } in &entrypoints { + let entrypoint_shortcuts = self.repository.action_shortcuts(&self.plugin_id.to_string(), id).await?; + shortcuts.insert(id.clone(), entrypoint_shortcuts); + } + + let mut plugins_search_items = generated_commands.into_iter() + .map(|item| { + let entrypoint_icon_path = match item.entrypoint_icon { + None => None, + Some(data) => Some(self.icon_cache.save_entrypoint_icon_to_cache(&self.plugin_uuid, &item.entrypoint_uuid, &data)?), + }; + + let entrypoint_frecency = frecency_map.get(&item.entrypoint_id).cloned().unwrap_or(0.0); + + let shortcuts = shortcuts + .get(&item.generator_entrypoint_id); + + let entrypoint_actions = item.entrypoint_actions.iter() + .map(|action| { + let shortcut = match (shortcuts, &action.id) { + (Some(shortcuts), Some(id)) => { + shortcuts.get(id).cloned() + } + _ => None + }; + + SearchIndexItemAction { + label: action.label.clone(), + shortcut, + } + }) + .collect(); + + Ok(SearchIndexItem { + entrypoint_type: SearchResultEntrypointType::GeneratedCommand, + entrypoint_id: EntrypointId::from_string(item.entrypoint_id), + entrypoint_name: item.entrypoint_name, + entrypoint_icon_path, + entrypoint_frecency, + entrypoint_actions, + }) + }) + .collect::>>()?; + + let mut icon_asset_data = HashMap::new(); + + for entrypoint in &entrypoints { + if let Some(path_to_asset) = &entrypoint.icon_path { + let result = self.repository.get_asset_data(&self.plugin_id.to_string(), path_to_asset) + .await; + + if let Ok(data) = result { + icon_asset_data.insert((entrypoint.id.clone(), path_to_asset.clone()), data); + } + } + } + + let mut builtin_search_items = entrypoints.into_iter() + .filter(|entrypoint| entrypoint.enabled) + .map(|entrypoint| { + let entrypoint_type = db_entrypoint_from_str(&entrypoint.entrypoint_type); + let entrypoint_id = entrypoint.id.to_string(); + + let entrypoint_frecency = frecency_map.get(&entrypoint_id).cloned().unwrap_or(0.0); + + let entrypoint_icon_path = match entrypoint.icon_path { + None => None, + Some(path_to_asset) => { + match icon_asset_data.get(&(entrypoint.id, path_to_asset)) { + None => None, + Some(data) => Some(self.icon_cache.save_entrypoint_icon_to_cache(&self.plugin_uuid, &entrypoint.uuid, data)?) + } + }, + }; + + let entrypoint_id = EntrypointId::from_string(entrypoint_id); + + match &entrypoint_type { + DbPluginEntrypointType::Command => { + Ok(Some(SearchIndexItem { + entrypoint_type: SearchResultEntrypointType::Command, + entrypoint_name: entrypoint.name, + entrypoint_id, + entrypoint_icon_path, + entrypoint_frecency, + entrypoint_actions: vec![], + })) + }, + DbPluginEntrypointType::View => { + Ok(Some(SearchIndexItem { + entrypoint_type: SearchResultEntrypointType::View, + entrypoint_name: entrypoint.name, + entrypoint_id, + entrypoint_icon_path, + entrypoint_frecency, + entrypoint_actions: vec![], + })) + }, + DbPluginEntrypointType::CommandGenerator | DbPluginEntrypointType::InlineView => { + Ok(None) + } + } + }) + .collect::>>()? + .into_iter() + .flat_map(|item| item) + .collect::>(); + + plugins_search_items.append(&mut builtin_search_items); + + self.search_index.save_for_plugin(self.plugin_id.clone(), name, plugins_search_items, refresh_search_list) + .context("error when updating search index")?; + + Ok(()) + } + + async fn get_asset_data(&self, path: &str) -> anyhow::Result> { + let data = self.repository.get_asset_data(&self.plugin_id.to_string(), &path) + .await?; + + Ok(data) + } + + async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result> { + let result = self.repository.get_entrypoints_by_plugin_id(&self.plugin_id.to_string()).await? + .into_iter() + .filter(|entrypoint| entrypoint.enabled) + .filter(|entrypoint| matches!(db_entrypoint_from_str(&entrypoint.entrypoint_type), DbPluginEntrypointType::CommandGenerator)) + .map(|entrypoint| entrypoint.id) + .collect::>(); + + Ok(result) + } + + async fn get_action_id_for_shortcut(&self, entrypoint_id: &str, key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> anyhow::Result> { + let result = self.repository.get_action_id_for_shortcut( + &self.plugin_id.to_string(), + &entrypoint_id, + key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + ).await?; + + Ok(result) + } + + async fn get_plugin_preferences(&self) -> anyhow::Result> { + let DbReadPlugin { preferences, preferences_user_data, .. } = self.repository + .get_plugin_by_id(&self.plugin_id.to_string()) + .await?; + + Ok(preferences_to_js(preferences, preferences_user_data)) + } + + async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result> { + let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = self.repository + .get_entrypoint_by_id(&self.plugin_id.to_string(), &entrypoint_id.to_string()) + .await?; + + Ok(preferences_to_js(preferences, preferences_user_data)) + } + + async fn plugin_preferences_required(&self) -> anyhow::Result { + let DbReadPlugin { preferences, preferences_user_data, .. } = self.repository + .get_plugin_by_id(&self.plugin_id.to_string()).await?; + + Ok(any_preferences_missing_value(preferences, preferences_user_data)) + } + + async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result { + let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = self.repository + .get_entrypoint_by_id(&self.plugin_id.to_string(), &entrypoint_id.to_string()).await?; + + Ok(any_preferences_missing_value(preferences, preferences_user_data)) + } +} + + +fn preferences_to_js( + preferences: HashMap, + mut preferences_user_data: HashMap +) -> HashMap { + preferences.into_iter() + .map(|(name, preference)| { + let user_data = match preferences_user_data.remove(&name) { + None => match preference { + DbPluginPreference::Number { default, .. } => PreferenceUserData::Number(default.expect("at this point preference should always have value")), + DbPluginPreference::String { default, .. } => PreferenceUserData::String(default.expect("at this point preference should always have value")), + DbPluginPreference::Enum { default, .. } => PreferenceUserData::String(default.expect("at this point preference should always have value")), + DbPluginPreference::Bool { default, .. } => PreferenceUserData::Bool(default.expect("at this point preference should always have value")), + DbPluginPreference::ListOfStrings { default, .. } => PreferenceUserData::ListOfStrings(default.expect("at this point preference should always have value")), + DbPluginPreference::ListOfNumbers { default, .. } => PreferenceUserData::ListOfNumbers(default.expect("at this point preference should always have value")), + DbPluginPreference::ListOfEnums { default, .. } => PreferenceUserData::ListOfStrings(default.expect("at this point preference should always have value")), + } + Some(user_data) => match user_data { + DbPluginPreferenceUserData::Number { value } => PreferenceUserData::Number(value.expect("at this point preference should always have value")), + DbPluginPreferenceUserData::String { value } => PreferenceUserData::String(value.expect("at this point preference should always have value")), + DbPluginPreferenceUserData::Enum { value } => PreferenceUserData::String(value.expect("at this point preference should always have value")), + DbPluginPreferenceUserData::Bool { value } => PreferenceUserData::Bool(value.expect("at this point preference should always have value")), + DbPluginPreferenceUserData::ListOfStrings { value } => PreferenceUserData::ListOfStrings(value.expect("at this point preference should always have value")), + DbPluginPreferenceUserData::ListOfNumbers { value } => PreferenceUserData::ListOfNumbers(value.expect("at this point preference should always have value")), + DbPluginPreferenceUserData::ListOfEnums { value } => PreferenceUserData::ListOfStrings(value.expect("at this point preference should always have value")), + } + }; + + (name, user_data) + }) + .collect() +} + +fn any_preferences_missing_value(preferences: HashMap, preferences_user_data: HashMap) -> bool { + for (name, preference) in preferences { + match preferences_user_data.get(&name) { + None => { + let no_default = match preference { + DbPluginPreference::Number { default, .. } => default.is_none(), + DbPluginPreference::String { default, .. } => default.is_none(), + DbPluginPreference::Enum { default, .. } => default.is_none(), + DbPluginPreference::Bool { default, .. } => default.is_none(), + DbPluginPreference::ListOfStrings { default, .. } => default.is_none(), + DbPluginPreference::ListOfNumbers { default, .. } => default.is_none(), + DbPluginPreference::ListOfEnums { default, .. } => default.is_none(), + }; + + if no_default { + return true + } + } + Some(preference) => { + let no_value = match preference { + DbPluginPreferenceUserData::Number { value } => value.is_none(), + DbPluginPreferenceUserData::String { value } => value.is_none(), + DbPluginPreferenceUserData::Enum { value } => value.is_none(), + DbPluginPreferenceUserData::Bool { value } => value.is_none(), + DbPluginPreferenceUserData::ListOfStrings { value } => value.is_none(), + DbPluginPreferenceUserData::ListOfNumbers { value } => value.is_none(), + DbPluginPreferenceUserData::ListOfEnums { value } => value.is_none(), + }; + + if no_value { + return true + } + } + } + } + + false +} diff --git a/rust/server/src/plugins/js/preferences.rs b/rust/server/src/plugins/js/preferences.rs index e0b1c32..3ba37d3 100644 --- a/rust/server/src/plugins/js/preferences.rs +++ b/rust/server/src/plugins/js/preferences.rs @@ -1,183 +1,77 @@ +use crate::plugins::js::BackendForPluginRuntimeApiImpl; +use common::model::EntrypointId; +use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; +use common_plugin_runtime::model::PreferenceUserData; +use deno_core::futures::executor::block_on; +use deno_core::{op2, OpState}; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use deno_core::{op2, OpState}; -use deno_core::futures::executor::block_on; -use crate::model::PreferenceUserData; -use crate::plugins::data_db_repository::{DataDbRepository, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; -use crate::plugins::js::PluginData; #[op2] #[serde] pub fn get_plugin_preferences(state: Rc>) -> anyhow::Result> { - let (plugin_id, repository) = { + let api = { let state = state.borrow(); - let plugin_id = state - .borrow::() - .plugin_id() + let api = state + .borrow::() .clone(); - let repository = state - .borrow::() - .clone(); - - (plugin_id, repository) + api }; block_on(async { - let DbReadPlugin { preferences, preferences_user_data, .. } = repository - .get_plugin_by_id(&plugin_id.to_string()) - .await?; - - Ok(preferences_to_js(preferences, preferences_user_data)) + api.get_plugin_preferences().await }) } #[op2] #[serde] pub fn get_entrypoint_preferences(state: Rc>, #[string] entrypoint_id: &str) -> anyhow::Result> { - let (plugin_id, repository) = { + let api = { let state = state.borrow(); - let plugin_id = state - .borrow::() - .plugin_id() + let api = state + .borrow::() .clone(); - let repository = state - .borrow::() - .clone(); - - (plugin_id, repository) + api }; block_on(async { - let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = repository - .get_entrypoint_by_id(&plugin_id.to_string(), entrypoint_id) - .await?; - - Ok(preferences_to_js(preferences, preferences_user_data)) + api.get_entrypoint_preferences(EntrypointId::from_string(entrypoint_id)).await }) } #[op2(async)] pub async fn plugin_preferences_required(state: Rc>) -> anyhow::Result { - let (plugin_id, repository) = { + let api = { let state = state.borrow(); - let plugin_id = state - .borrow::() - .plugin_id() + let api = state + .borrow::() .clone(); - let repository = state - .borrow::() - .clone(); - - (plugin_id, repository) + api }; - let DbReadPlugin { preferences, preferences_user_data, .. } = repository - .get_plugin_by_id(&plugin_id.to_string()).await?; - - Ok(any_preferences_missing_value(preferences, preferences_user_data)) + api.plugin_preferences_required().await } #[op2(async)] pub async fn entrypoint_preferences_required(state: Rc>, #[string] entrypoint_id: String) -> anyhow::Result { - let (plugin_id, repository) = { + let api = { let state = state.borrow(); - let plugin_id = state - .borrow::() - .plugin_id() + let api = state + .borrow::() .clone(); - let repository = state - .borrow::() - .clone(); - - (plugin_id, repository) + api }; - let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = repository - .get_entrypoint_by_id(&plugin_id.to_string(), &entrypoint_id).await?; - - Ok(any_preferences_missing_value(preferences, preferences_user_data)) -} - - -fn any_preferences_missing_value(preferences: HashMap, preferences_user_data: HashMap) -> bool { - for (name, preference) in preferences { - match preferences_user_data.get(&name) { - None => { - let no_default = match preference { - DbPluginPreference::Number { default, .. } => default.is_none(), - DbPluginPreference::String { default, .. } => default.is_none(), - DbPluginPreference::Enum { default, .. } => default.is_none(), - DbPluginPreference::Bool { default, .. } => default.is_none(), - DbPluginPreference::ListOfStrings { default, .. } => default.is_none(), - DbPluginPreference::ListOfNumbers { default, .. } => default.is_none(), - DbPluginPreference::ListOfEnums { default, .. } => default.is_none(), - }; - - if no_default { - return true - } - } - Some(preference) => { - let no_value = match preference { - DbPluginPreferenceUserData::Number { value } => value.is_none(), - DbPluginPreferenceUserData::String { value } => value.is_none(), - DbPluginPreferenceUserData::Enum { value } => value.is_none(), - DbPluginPreferenceUserData::Bool { value } => value.is_none(), - DbPluginPreferenceUserData::ListOfStrings { value } => value.is_none(), - DbPluginPreferenceUserData::ListOfNumbers { value } => value.is_none(), - DbPluginPreferenceUserData::ListOfEnums { value } => value.is_none(), - }; - - if no_value { - return true - } - } - } - } - - false -} - - -fn preferences_to_js( - preferences: HashMap, - mut preferences_user_data: HashMap -) -> HashMap { - preferences.into_iter() - .map(|(name, preference)| { - let user_data = match preferences_user_data.remove(&name) { - None => match preference { - DbPluginPreference::Number { default, .. } => PreferenceUserData::Number(default.expect("at this point preference should always have value")), - DbPluginPreference::String { default, .. } => PreferenceUserData::String(default.expect("at this point preference should always have value")), - DbPluginPreference::Enum { default, .. } => PreferenceUserData::String(default.expect("at this point preference should always have value")), - DbPluginPreference::Bool { default, .. } => PreferenceUserData::Bool(default.expect("at this point preference should always have value")), - DbPluginPreference::ListOfStrings { default, .. } => PreferenceUserData::ListOfStrings(default.expect("at this point preference should always have value")), - DbPluginPreference::ListOfNumbers { default, .. } => PreferenceUserData::ListOfNumbers(default.expect("at this point preference should always have value")), - DbPluginPreference::ListOfEnums { default, .. } => PreferenceUserData::ListOfStrings(default.expect("at this point preference should always have value")), - } - Some(user_data) => match user_data { - DbPluginPreferenceUserData::Number { value } => PreferenceUserData::Number(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::String { value } => PreferenceUserData::String(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::Enum { value } => PreferenceUserData::String(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::Bool { value } => PreferenceUserData::Bool(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::ListOfStrings { value } => PreferenceUserData::ListOfStrings(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::ListOfNumbers { value } => PreferenceUserData::ListOfNumbers(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::ListOfEnums { value } => PreferenceUserData::ListOfStrings(value.expect("at this point preference should always have value")), - } - }; - - (name, user_data) - }) - .collect() + api.entrypoint_preferences_required(EntrypointId::from_string(entrypoint_id)).await } diff --git a/rust/server/src/plugins/js/search.rs b/rust/server/src/plugins/js/search.rs index 193d44a..de1ce15 100644 --- a/rust/server/src/plugins/js/search.rs +++ b/rust/server/src/plugins/js/search.rs @@ -1,190 +1,23 @@ -use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginEntrypointType, DbReadPlugin, DbReadPluginEntrypoint}; -use crate::plugins::icon_cache::IconCache; -use crate::plugins::js::PluginData; -use crate::search::{SearchIndex, SearchIndexItem, SearchIndexItemAction}; -use anyhow::Context; -use common::model::{EntrypointId, SearchResultEntrypointType}; +use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; +use common_plugin_runtime::model::AdditionalSearchItem; use deno_core::{op2, OpState}; -use serde::Deserialize; use std::cell::RefCell; -use std::collections::HashMap; use std::rc::Rc; +use crate::plugins::js::BackendForPluginRuntimeApiImpl; #[op2(async)] pub async fn reload_search_index(state: Rc>, #[serde] generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { - let (plugin_id, plugin_uuid, repository, mut search_index, icon_cache) = { + let api = { let state = state.borrow(); - let plugin_data = state.borrow::(); - - let plugin_id = plugin_data - .plugin_id() + let api = state + .borrow::() .clone(); - let plugin_uuid = plugin_data - .plugin_uuid() - .to_owned(); - - let repository = state - .borrow::() - .clone(); - - let search_index = state - .borrow::() - .clone(); - - let icon_cache = state - .borrow::() - .clone(); - - (plugin_id, plugin_uuid, repository, search_index, icon_cache) + api }; - icon_cache.clear_plugin_icon_cache_dir(&plugin_uuid) - .context("error when clearing up icon cache before recreating it")?; - - let DbReadPlugin { name, .. } = repository.get_plugin_by_id(&plugin_id.to_string()) - .await - .context("error when getting plugin by id")?; - - let entrypoints = repository.get_entrypoints_by_plugin_id(&plugin_id.to_string()) - .await - .context("error when getting entrypoints by plugin id")?; - - let frecency_map = repository.get_frecency_for_plugin(&plugin_id.to_string()) - .await - .context("error when getting frecency for plugin")?; - - let mut shortcuts = HashMap::new(); - - for DbReadPluginEntrypoint { id, .. } in &entrypoints { - let entrypoint_shortcuts = repository.action_shortcuts(&plugin_id.to_string(), id).await?; - shortcuts.insert(id.clone(), entrypoint_shortcuts); - } - - let mut plugins_search_items = generated_commands.into_iter() - .map(|item| { - let entrypoint_icon_path = match item.entrypoint_icon { - None => None, - Some(data) => Some(icon_cache.save_entrypoint_icon_to_cache(&plugin_uuid, &item.entrypoint_uuid, &data)?), - }; - - let entrypoint_frecency = frecency_map.get(&item.entrypoint_id).cloned().unwrap_or(0.0); - - let shortcuts = shortcuts - .get(&item.generator_entrypoint_id); - - let entrypoint_actions = item.entrypoint_actions.iter() - .map(|action| { - let shortcut = match (shortcuts, &action.id) { - (Some(shortcuts), Some(id)) => { - shortcuts.get(id).cloned() - } - _ => None - }; - - SearchIndexItemAction { - label: action.label.clone(), - shortcut, - } - }) - .collect(); - - Ok(SearchIndexItem { - entrypoint_type: SearchResultEntrypointType::GeneratedCommand, - entrypoint_id: EntrypointId::from_string(item.entrypoint_id), - entrypoint_name: item.entrypoint_name, - entrypoint_icon_path, - entrypoint_frecency, - entrypoint_actions, - }) - }) - .collect::>>()?; - - let mut icon_asset_data = HashMap::new(); - - for entrypoint in &entrypoints { - if let Some(path_to_asset) = &entrypoint.icon_path { - let result = repository.get_asset_data(&plugin_id.to_string(), path_to_asset) - .await; - - if let Ok(data) = result { - icon_asset_data.insert((entrypoint.id.clone(), path_to_asset.clone()), data); - } - } - } - - let mut builtin_search_items = entrypoints.into_iter() - .filter(|entrypoint| entrypoint.enabled) - .map(|entrypoint| { - let entrypoint_type = db_entrypoint_from_str(&entrypoint.entrypoint_type); - let entrypoint_id = entrypoint.id.to_string(); - - let entrypoint_frecency = frecency_map.get(&entrypoint_id).cloned().unwrap_or(0.0); - - let entrypoint_icon_path = match entrypoint.icon_path { - None => None, - Some(path_to_asset) => { - match icon_asset_data.get(&(entrypoint.id, path_to_asset)) { - None => None, - Some(data) => Some(icon_cache.save_entrypoint_icon_to_cache(&plugin_uuid, &entrypoint.uuid, data)?) - } - }, - }; - - let entrypoint_id = EntrypointId::from_string(entrypoint_id); - - match &entrypoint_type { - DbPluginEntrypointType::Command => { - Ok(Some(SearchIndexItem { - entrypoint_type: SearchResultEntrypointType::Command, - entrypoint_name: entrypoint.name, - entrypoint_id, - entrypoint_icon_path, - entrypoint_frecency, - entrypoint_actions: vec![], - })) - }, - DbPluginEntrypointType::View => { - Ok(Some(SearchIndexItem { - entrypoint_type: SearchResultEntrypointType::View, - entrypoint_name: entrypoint.name, - entrypoint_id, - entrypoint_icon_path, - entrypoint_frecency, - entrypoint_actions: vec![], - })) - }, - DbPluginEntrypointType::CommandGenerator | DbPluginEntrypointType::InlineView => { - Ok(None) - } - } - }) - .collect::>>()? - .into_iter() - .flat_map(|item| item) - .collect::>(); - - plugins_search_items.append(&mut builtin_search_items); - - search_index.save_for_plugin(plugin_id, name, plugins_search_items, refresh_search_list) - .context("error when updating search index")?; + api.reload_search_index(generated_commands, refresh_search_list).await?; Ok(()) } - -#[derive(Debug, Deserialize)] -struct AdditionalSearchItem { - entrypoint_name: String, - generator_entrypoint_id: String, - entrypoint_id: String, - entrypoint_uuid: String, - entrypoint_icon: Option>, - entrypoint_actions: Vec, -} - -#[derive(Debug, Deserialize)] -pub struct AdditionalSearchItemAction { - id: Option, - label: String, -} \ No newline at end of file diff --git a/rust/server/src/plugins/js/ui.rs b/rust/server/src/plugins/js/ui.rs index e868a5d..0a1702e 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/server/src/plugins/js/ui.rs @@ -10,11 +10,12 @@ use indexmap::IndexMap; use serde::{de, Deserialize, Deserializer}; use serde::de::Error; use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, EntrypointId, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Image, ImageSource, ImageSourceAsset, ImageSourceUrl, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectItemWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiPropertyValue, UiWidgetId, WidgetVisitor}; +use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; use component_model::{Component, Property, PropertyType, SharedType}; use component_model::Component::Root; use crate::model::{JsUiRenderLocation, JsUiRequestData, JsUiResponseData}; use crate::plugins::data_db_repository::DataDbRepository; -use crate::plugins::js::{ComponentModel, make_request, PluginData}; +use crate::plugins::js::{ComponentModel, make_request, PluginData, BackendForPluginRuntimeApiImpl}; #[op2] pub fn show_plugin_error_view(state: Rc>, #[string] entrypoint_id: String, #[serde] render_location: JsUiRenderLocation) -> anyhow::Result<()> { @@ -145,23 +146,17 @@ pub async fn fetch_action_id_for_shortcut( modifier_alt: bool, modifier_meta: bool ) -> anyhow::Result> { - let (plugin_id, repository) = { + let api = { let state = state.borrow(); - let plugin_id = state - .borrow::() - .plugin_id() + let api = state + .borrow::() .clone(); - let repository = state - .borrow::() - .clone(); - - (plugin_id, repository) + api }; - let result = repository.get_action_id_for_shortcut( - &plugin_id.to_string(), + let result = api.get_action_id_for_shortcut( &entrypoint_id, PhysicalKey::from_value(key), modifier_shift, @@ -251,17 +246,12 @@ fn get_image_date(state: Rc>, source: &ImageSource) -> anyhow:: let bytes = { let state = state.borrow(); - let plugin_id = state - .borrow::() - .plugin_id() - .clone(); - - let repository = state - .borrow::() + let api = state + .borrow::() .clone(); block_on(async { - repository.get_asset_data(&plugin_id.to_string(), &asset).await + api.get_asset_data(&asset).await })? }; diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index 51cf117..27ad33a 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -108,7 +108,7 @@ impl SearchIndex { Ok(()) } - pub fn save_for_plugin(&mut self, plugin_id: PluginId, plugin_name: String, search_items: Vec, refresh_search_list: bool) -> tantivy::Result<()> { + pub fn save_for_plugin(&self, plugin_id: PluginId, plugin_name: String, search_items: Vec, refresh_search_list: bool) -> tantivy::Result<()> { tracing::debug!("Reloading search index for plugin {:?}", plugin_id); // writer panics if another writer exists From 37d8a218f7a5773952605845f876b2dbe8979224 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:34:18 +0100 Subject: [PATCH 208/540] Abstract clipboard inside plugin runtime behind backend api --- .../src/backend_for_plugin_runtime_api.rs | 9 +- rust/common_plugin_runtime/src/model.rs | 6 + rust/server/src/plugins/clipboard.rs | 147 +++++++++ rust/server/src/plugins/js/clipboard.rs | 293 +++--------------- rust/server/src/plugins/js/mod.rs | 102 +++++- rust/server/src/plugins/mod.rs | 3 +- 6 files changed, 293 insertions(+), 267 deletions(-) create mode 100644 rust/server/src/plugins/clipboard.rs diff --git a/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs b/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs index 8593a5b..1a80e79 100644 --- a/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs +++ b/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; +use crate::model::{AdditionalSearchItem, ClipboardData, PreferenceUserData}; use common::model::{EntrypointId, PhysicalKey}; -use crate::model::{AdditionalSearchItem, PreferenceUserData}; +use std::collections::HashMap; pub trait BackendForPluginRuntimeApi { async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> ; @@ -19,4 +19,9 @@ pub trait BackendForPluginRuntimeApi { async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result>; async fn plugin_preferences_required(&self) -> anyhow::Result; async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result; + async fn clipboard_read(&self) -> anyhow::Result; + async fn clipboard_read_text(&self) -> anyhow::Result>; + async fn clipboard_write(&self, data: ClipboardData) -> anyhow::Result<()>; + async fn clipboard_write_text(&self, data: String) -> anyhow::Result<()>; + async fn clipboard_clear(&self) -> anyhow::Result<()>; } \ No newline at end of file diff --git a/rust/common_plugin_runtime/src/model.rs b/rust/common_plugin_runtime/src/model.rs index 3969ca3..5d7c14f 100644 --- a/rust/common_plugin_runtime/src/model.rs +++ b/rust/common_plugin_runtime/src/model.rs @@ -24,4 +24,10 @@ pub enum PreferenceUserData { Bool(bool), ListOfStrings(Vec), ListOfNumbers(Vec), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ClipboardData { + pub text_data: Option, + pub png_data: Option> } \ No newline at end of file diff --git a/rust/server/src/plugins/clipboard.rs b/rust/server/src/plugins/clipboard.rs new file mode 100644 index 0000000..9191afa --- /dev/null +++ b/rust/server/src/plugins/clipboard.rs @@ -0,0 +1,147 @@ +use anyhow::{anyhow, Context, Error}; +use arboard::ImageData; +use image::RgbaImage; +use std::io::Cursor; +use std::sync::{Arc, RwLock}; +use common_plugin_runtime::model::ClipboardData; + +#[derive(Clone)] +pub struct Clipboard { + clipboard: Arc>, +} + +impl Clipboard { + pub fn new() -> anyhow::Result { + let clipboard = arboard::Clipboard::new() + .context("error while creating clipboard")?; + + Ok(Self { + clipboard: Arc::new(RwLock::new(clipboard)), + }) + } + + pub fn read(&self) -> anyhow::Result { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); + + let png_data = match clipboard.get_image() { + Ok(data) => { + let rgba_image = RgbaImage::from_raw(data.width as u32, data.height as u32, data.bytes.into()); + let rgba_image = image::DynamicImage::ImageRgba8(rgba_image.unwrap()); + + let mut result = Cursor::new(vec![]); + + rgba_image.write_to(&mut result, image::ImageFormat::Png) + .expect("should be able to convert to png"); + + Some(result.into_inner()) + }, + Err(err) => { + match err { + arboard::Error::ContentNotAvailable => None, + err @ _ => { + return Err(unknown_err_clipboard(err)); + }, + } + } + }; + + let text_data = match clipboard.get_text() { + Ok(data) => Some(data), + Err(err) => { + match err { + arboard::Error::ContentNotAvailable => None, + err @ _ => { + return Err(unknown_err_clipboard(err)); + }, + } + } + }; + + Ok(ClipboardData { + text_data, + png_data, + }) + } + + pub fn read_text(&self) -> anyhow::Result> { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); + + let data = match clipboard.get_text() { + Ok(data) => Some(data), + Err(err) => { + match err { + arboard::Error::ContentNotAvailable => None, + err @ _ => { + return Err(unknown_err_clipboard(err)); + }, + } + } + }; + + Ok(data) + } + + pub fn write(&self, data: ClipboardData) -> anyhow::Result<()> { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); + + if let Some(png_data) = data.png_data { + + let cursor = Cursor::new(&png_data); + + let mut reader = image::io::Reader::new(cursor); + reader.set_format(image::ImageFormat::Png); + + let image = reader.decode() + .map_err(|_err| unable_to_convert_image_err())? + .into_rgba8(); + + let (w, h) = image.dimensions(); + + let image_data = ImageData { + width: w as usize, + height: h as usize, + bytes: image.into_raw().into() + }; + + clipboard.set_image(image_data) + .map_err(|err| unknown_err_clipboard(err))?; + } + + if let Some(text_data) = data.text_data { + clipboard.set_text(text_data) + .map_err(|err| unknown_err_clipboard(err))?; + } + + Ok(()) + } + + pub fn write_text(&self, data: String) -> anyhow::Result<()> { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); + + clipboard.set_text(data) + .map_err(|err| unknown_err_clipboard(err))?; + + Ok(()) + } + + pub fn clear(&self) -> anyhow::Result<()> { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); + + clipboard.clear() + .map_err(|err| unknown_err_clipboard(err))?; + + Ok(()) + } +} + +fn unknown_err_clipboard(err: arboard::Error) -> Error { + anyhow!("UNKNOWN_ERROR: {:?}", err) +} + +fn unknown_err_image(err: image::ImageError) -> Error { + anyhow!("UNKNOWN_ERROR: {:?}", err) +} + +fn unable_to_convert_image_err() -> Error { + anyhow!("UNABLE_TO_CONVERT_IMAGE") +} \ No newline at end of file diff --git a/rust/server/src/plugins/js/clipboard.rs b/rust/server/src/plugins/js/clipboard.rs index 05ff427..8075a78 100644 --- a/rust/server/src/plugins/js/clipboard.rs +++ b/rust/server/src/plugins/js/clipboard.rs @@ -1,308 +1,101 @@ -use std::cell::RefCell; -use std::io::Cursor; -use std::rc::Rc; -use std::sync::{Arc, RwLock}; -use std::time::Duration; -use anyhow::{anyhow, Context, Error}; -use arboard::ImageData; +use crate::plugins::js::BackendForPluginRuntimeApiImpl; +use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; +use common_plugin_runtime::model::ClipboardData; use deno_core::{op2, OpState}; -use image::RgbaImage; use serde::{Deserialize, Serialize}; -use tokio::task::spawn_blocking; -use crate::plugins::js::permissions::PluginPermissionsClipboard; -use crate::plugins::js::{clipboard, PluginData}; - -#[derive(Clone)] -pub struct Clipboard { - clipboard: Arc>, -} - -impl Clipboard { - pub fn new() -> anyhow::Result { - let clipboard = arboard::Clipboard::new() - .context("error while creating clipboard")?; - - Ok(Self { - clipboard: Arc::new(RwLock::new(clipboard)), - }) - } - - fn read(&self) -> anyhow::Result { - let mut clipboard = self.clipboard.write().expect("lock is poisoned"); - - let png_data = match clipboard.get_image() { - Ok(data) => { - let rgba_image = RgbaImage::from_raw(data.width as u32, data.height as u32, data.bytes.into()); - let rgba_image = image::DynamicImage::ImageRgba8(rgba_image.unwrap()); - - let mut result = Cursor::new(vec![]); - - rgba_image.write_to(&mut result, image::ImageFormat::Png) - .expect("should be able to convert to png"); - - Some(result.into_inner()) - }, - Err(err) => { - match err { - arboard::Error::ContentNotAvailable => None, - err @ _ => { - return Err(unknown_err_clipboard(err)); - }, - } - } - }; - - let text_data = match clipboard.get_text() { - Ok(data) => Some(data), - Err(err) => { - match err { - arboard::Error::ContentNotAvailable => None, - err @ _ => { - return Err(unknown_err_clipboard(err)); - }, - } - } - }; - - Ok(ClipboardData { - text_data, - png_data, - }) - } - - fn read_text(&self) -> anyhow::Result> { - let mut clipboard = self.clipboard.write().expect("lock is poisoned"); - - let data = match clipboard.get_text() { - Ok(data) => Some(data), - Err(err) => { - match err { - arboard::Error::ContentNotAvailable => None, - err @ _ => { - return Err(unknown_err_clipboard(err)); - }, - } - } - }; - - Ok(data) - } - - fn write(&self, data: ClipboardData) -> anyhow::Result<()> { - let mut clipboard = self.clipboard.write().expect("lock is poisoned"); - - if let Some(png_data) = data.png_data { - - let cursor = Cursor::new(&png_data); - - let mut reader = image::io::Reader::new(cursor); - reader.set_format(image::ImageFormat::Png); - - let image = reader.decode() - .map_err(|_err| unable_to_convert_image_err())? - .into_rgba8(); - - let (w, h) = image.dimensions(); - - let image_data = ImageData { - width: w as usize, - height: h as usize, - bytes: image.into_raw().into() - }; - - clipboard.set_image(image_data) - .map_err(|err| unknown_err_clipboard(err))?; - } - - if let Some(text_data) = data.text_data { - clipboard.set_text(text_data) - .map_err(|err| unknown_err_clipboard(err))?; - } - - Ok(()) - } - - fn write_text(&self, data: String) -> anyhow::Result<()> { - let mut clipboard = self.clipboard.write().expect("lock is poisoned"); - - clipboard.set_text(data) - .map_err(|err| unknown_err_clipboard(err))?; - - Ok(()) - } - - fn clear(&self) -> anyhow::Result<()> { - let mut clipboard = self.clipboard.write().expect("lock is poisoned"); - - clipboard.clear() - .map_err(|err| unknown_err_clipboard(err))?; - - Ok(()) - } -} - -fn unknown_err_clipboard(err: arboard::Error) -> Error { - anyhow!("UNKNOWN_ERROR: {:?}", err) -} - -fn unknown_err_image(err: image::ImageError) -> Error { - anyhow!("UNKNOWN_ERROR: {:?}", err) -} - -fn unable_to_convert_image_err() -> Error { - anyhow!("UNABLE_TO_CONVERT_IMAGE") -} +use std::cell::RefCell; +use std::rc::Rc; #[derive(Debug, Serialize, Deserialize)] -struct ClipboardData { +struct JSClipboardData { text_data: Option, png_data: Option> } #[op2(async)] #[serde] -pub async fn clipboard_read(state: Rc>) -> anyhow::Result { - let clipboard = { +pub async fn clipboard_read(state: Rc>) -> anyhow::Result { + let api = { let state = state.borrow(); - let plugin_data = state - .borrow::(); - - let allow = plugin_data - .permissions() - .clipboard - .contains(&PluginPermissionsClipboard::Read); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); - } - - tracing::debug!("Reading from clipboard, plugin id: {:?}", plugin_data.plugin_id); - - let clipboard = state - .borrow::() + let api = state + .borrow::() .clone(); - clipboard + api }; - spawn_blocking(move || clipboard.read()).await? + let result = api.clipboard_read().await?; + + Ok(JSClipboardData { + text_data: result.text_data, + png_data: result.png_data, + }) } #[op2(async)] #[string] pub async fn clipboard_read_text(state: Rc>) -> anyhow::Result> { - let clipboard = { + let api = { let state = state.borrow(); - let plugin_data = state - .borrow::(); - - let allow = plugin_data - .permissions() - .clipboard - .contains(&PluginPermissionsClipboard::Read); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); - } - - tracing::debug!("Reading text from clipboard, plugin id: {:?}", plugin_data.plugin_id); - - let clipboard = state - .borrow::() + let api = state + .borrow::() .clone(); - clipboard + api }; - spawn_blocking(move || clipboard.read_text()).await? + api.clipboard_read_text().await } #[op2(async)] -pub async fn clipboard_write(state: Rc>, #[serde] data: ClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2 - let clipboard = { +pub async fn clipboard_write(state: Rc>, #[serde] data: JSClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2 + let api = { let state = state.borrow(); - let plugin_data = state - .borrow::(); - - let allow = plugin_data - .permissions() - .clipboard - .contains(&PluginPermissionsClipboard::Write); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); - } - - tracing::debug!("Writing to clipboard, plugin id: {:?}", plugin_data.plugin_id); - - let clipboard = state - .borrow::() + let api = state + .borrow::() .clone(); - clipboard + api }; - spawn_blocking(move || clipboard.write(data)).await? + let clipboard_data = ClipboardData { + text_data: data.text_data, + png_data: data.png_data, + }; + + api.clipboard_write(clipboard_data).await } #[op2(async)] pub async fn clipboard_write_text(state: Rc>, #[string] data: String) -> anyhow::Result<()> { - let clipboard = { + let api = { let state = state.borrow(); - let plugin_data = state - .borrow::(); - - let allow = plugin_data - .permissions() - .clipboard - .contains(&PluginPermissionsClipboard::Write); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); - } - - tracing::debug!("Writing text to clipboard, plugin id: {:?}", plugin_data.plugin_id); - - let clipboard = state - .borrow::() + let api = state + .borrow::() .clone(); - clipboard + api }; - spawn_blocking(move || clipboard.write_text(data)).await? + api.clipboard_write_text(data).await } #[op2(async)] pub async fn clipboard_clear(state: Rc>) -> anyhow::Result<()> { - let clipboard = { + let api = { let state = state.borrow(); - let plugin_data = state - .borrow::(); - - let allow = plugin_data - .permissions() - .clipboard - .contains(&PluginPermissionsClipboard::Clear); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'clear' permission for clipboard")); - } - - tracing::debug!("Clearing clipboard, plugin id: {:?}", plugin_data.plugin_id); - - let clipboard = state - .borrow::() + let api = state + .borrow::() .clone(); - clipboard + api }; - spawn_blocking(move || clipboard.clear()).await? + api.clipboard_clear().await } diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index d48bf6c..5913c92 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -27,20 +27,22 @@ use once_cell::sync::Lazy; use regex::Regex; use serde::{Deserialize, Serialize}; use tokio::net::TcpStream; +use tokio::task::spawn_blocking; use tokio_util::sync::CancellationToken; use common::dirs::Dirs; use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; use common::rpc::frontend_api::FrontendApi; use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; -use common_plugin_runtime::model::{AdditionalSearchItem, PreferenceUserData}; +use common_plugin_runtime::model::{AdditionalSearchItem, ClipboardData, PreferenceUserData}; use component_model::{create_component_model, Children, Component, Property, PropertyType, SharedType}; use crate::model::{IntermediateUiEvent, JsKeyboardEventOrigin, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData}; +use crate::plugins::clipboard::Clipboard; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; -use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text, Clipboard}; +use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; use crate::plugins::js::command_generators::{get_command_generator_entrypoint_ids}; use crate::plugins::js::environment::{environment_gauntlet_version, environment_is_development, environment_plugin_cache_dir, environment_plugin_data_dir}; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; @@ -357,7 +359,6 @@ async fn start_js_runtime( plugin_cache_dir, plugin_data_dir, inline_view_entrypoint_id, - runtime_permissions, ), frontend_api, ComponentModel::new(component_model), @@ -365,11 +366,12 @@ async fn start_js_runtime( icon_cache, repository, search_index, + clipboard, plugin_uuid, - plugin_id + plugin_id, + runtime_permissions, ), numbat_context, - clipboard, ), gauntlet_esm, ]; @@ -601,7 +603,6 @@ deno_core::extension!( component_model: ComponentModel, backend_api: BackendForPluginRuntimeApiImpl, numbat_context: Option, - clipboard: Clipboard, }, state = |state, options| { state.put(options.event_receiver); @@ -610,7 +611,6 @@ deno_core::extension!( state.put(options.component_model); state.put(options.backend_api); state.put(options.numbat_context); - state.put(options.clipboard); }, ); @@ -882,7 +882,6 @@ pub struct PluginData { plugin_cache_dir: String, plugin_data_dir: String, inline_view_entrypoint_id: Option, - permissions: PluginRuntimePermissions } impl PluginData { @@ -894,7 +893,6 @@ impl PluginData { plugin_cache_dir: String, plugin_data_dir: String, inline_view_entrypoint_id: Option, - permissions: PluginRuntimePermissions ) -> Self { Self { plugin_id, @@ -904,7 +902,6 @@ impl PluginData { plugin_cache_dir, plugin_data_dir, inline_view_entrypoint_id, - permissions } } @@ -931,10 +928,6 @@ impl PluginData { fn inline_view_entrypoint_id(&self) -> Option { self.inline_view_entrypoint_id.clone() } - - fn permissions(&self) -> &PluginRuntimePermissions { - &self.permissions - } } pub struct ComponentModel { @@ -974,8 +967,10 @@ pub struct BackendForPluginRuntimeApiImpl { icon_cache: IconCache, repository: DataDbRepository, search_index: SearchIndex, + clipboard: Clipboard, plugin_uuid: String, plugin_id: PluginId, + permissions: PluginRuntimePermissions } impl BackendForPluginRuntimeApiImpl { @@ -983,15 +978,19 @@ impl BackendForPluginRuntimeApiImpl { icon_cache: IconCache, repository: DataDbRepository, search_index: SearchIndex, + clipboard: Clipboard, plugin_uuid: String, plugin_id: PluginId, + permissions: PluginRuntimePermissions ) -> Self { Self { icon_cache, repository, search_index, + clipboard, plugin_uuid, plugin_id, + permissions } } } @@ -1192,6 +1191,81 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { Ok(any_preferences_missing_value(preferences, preferences_user_data)) } + + async fn clipboard_read(&self) -> anyhow::Result { + let allow = self + .permissions + .clipboard + .contains(&PluginPermissionsClipboard::Read); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); + } + + tracing::debug!("Reading from clipboard, plugin id: {:?}", self.plugin_id); + + self.clipboard.read() + } + + async fn clipboard_read_text(&self) -> anyhow::Result> { + let allow = self + .permissions + .clipboard + .contains(&PluginPermissionsClipboard::Read); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); + } + + tracing::debug!("Reading text from clipboard, plugin id: {:?}", self.plugin_id); + + self.clipboard.read_text() + } + + async fn clipboard_write(&self, data: ClipboardData) -> anyhow::Result<()> { + let allow = self + .permissions + .clipboard + .contains(&PluginPermissionsClipboard::Write); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); + } + + tracing::debug!("Writing to clipboard, plugin id: {:?}", self.plugin_id); + + self.clipboard.write(data) + } + + async fn clipboard_write_text(&self, data: String) -> anyhow::Result<()> { + let allow = self + .permissions + .clipboard + .contains(&PluginPermissionsClipboard::Write); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); + } + + tracing::debug!("Writing text to clipboard, plugin id: {:?}", self.plugin_id); + + self.clipboard.write_text(data) + } + + async fn clipboard_clear(&self) -> anyhow::Result<()> { + let allow = self + .permissions + .clipboard + .contains(&PluginPermissionsClipboard::Clear); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'clear' permission for clipboard")); + } + + tracing::debug!("Clearing clipboard, plugin id: {:?}", self.plugin_id); + + self.clipboard.clear() + } } diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 3405965..6268688 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -14,11 +14,11 @@ use common::{settings_env_data_to_string, SettingsEnvData}; use utils::channel::RequestSender; use common::dirs::Dirs; use crate::model::{ActionShortcutKey, JsKeyboardEventOrigin}; +use crate::plugins::clipboard::Clipboard; use crate::plugins::config_reader::ConfigReader; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginActionShortcutKind, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginMainSearchBarPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; use crate::plugins::js::{start_plugin_runtime, AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginRuntimeData}; -use crate::plugins::js::clipboard::Clipboard; use crate::plugins::js::permissions::{PluginPermissions, PluginPermissionsClipboard, PluginPermissionsExec, PluginPermissionsFileSystem, PluginPermissionsMainSearchBar}; use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; @@ -33,6 +33,7 @@ mod run_status; mod download_status; mod icon_cache; pub(super) mod frecency; +mod clipboard; static BUNDLED_PLUGINS: [(&str, Dir); 1] = [ ("gauntlet", include_dir!("$CARGO_MANIFEST_DIR/../../bundled_plugins/gauntlet/dist")), From 62a2ffc2b74b76afea3da1d36c9ce118806e6e03 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 13 Dec 2024 19:30:51 +0100 Subject: [PATCH 209/540] Abstract away usage of frontend api in plugin runtime behind backend api --- Cargo.lock | 2 + rust/common_plugin_runtime/Cargo.toml | 2 + .../src/backend_for_plugin_runtime_api.rs | 44 +++- rust/server/src/model.rs | 40 +-- rust/server/src/plugins/js/mod.rs | 245 +++++++++--------- rust/server/src/plugins/js/ui.rs | 181 +++++++------ 6 files changed, 246 insertions(+), 268 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 373be30..788c58e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1659,8 +1659,10 @@ name = "common_plugin_runtime" version = "0.1.0" dependencies = [ "anyhow", + "bytes", "common", "serde", + "serde-value", ] [[package]] diff --git a/rust/common_plugin_runtime/Cargo.toml b/rust/common_plugin_runtime/Cargo.toml index 08cdd9d..dfdc0c2 100644 --- a/rust/common_plugin_runtime/Cargo.toml +++ b/rust/common_plugin_runtime/Cargo.toml @@ -7,3 +7,5 @@ edition = "2021" serde = { version = "1.0", features = ["derive"] } anyhow = "1.0.94" common = { path = "../common" } +bytes = "1.6.0" +serde-value = "0.7.0" diff --git a/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs b/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs index 1a80e79..dca1a8b 100644 --- a/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs +++ b/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs @@ -1,20 +1,11 @@ use crate::model::{AdditionalSearchItem, ClipboardData, PreferenceUserData}; -use common::model::{EntrypointId, PhysicalKey}; +use common::model::{EntrypointId, RootWidget, UiRenderLocation, UiWidgetId}; use std::collections::HashMap; pub trait BackendForPluginRuntimeApi { async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> ; async fn get_asset_data(&self, path: &str) -> anyhow::Result>; async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result>; - async fn get_action_id_for_shortcut( - &self, - entrypoint_id: &str, - key: PhysicalKey, - modifier_shift: bool, - modifier_control: bool, - modifier_alt: bool, - modifier_meta: bool - ) -> anyhow::Result>; async fn get_plugin_preferences(&self) -> anyhow::Result>; async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result>; async fn plugin_preferences_required(&self) -> anyhow::Result; @@ -24,4 +15,37 @@ pub trait BackendForPluginRuntimeApi { async fn clipboard_write(&self, data: ClipboardData) -> anyhow::Result<()>; async fn clipboard_write_text(&self, data: String) -> anyhow::Result<()>; async fn clipboard_clear(&self) -> anyhow::Result<()>; + async fn ui_update_loading_bar(&self, entrypoint_id: EntrypointId, show: bool) -> anyhow::Result<()>; + async fn ui_show_hud(&self, display: String) -> anyhow::Result<()>; + async fn ui_get_action_id_for_shortcut( + &self, + entrypoint_id: EntrypointId, + key: String, + modifier_shift: bool, + modifier_control: bool, + modifier_alt: bool, + modifier_meta: bool + ) -> anyhow::Result>; + async fn ui_render( + &self, + entrypoint_id: EntrypointId, + render_location: UiRenderLocation, + top_level_view: bool, + container: RootWidget, + #[cfg(feature = "scenario_runner")] + container_value: serde_value::Value, + images: HashMap, + ) -> anyhow::Result<()>; + async fn ui_show_plugin_error_view( + &self, + entrypoint_id: EntrypointId, + render_location: UiRenderLocation + ) -> anyhow::Result<()>; + async fn ui_show_preferences_required_view( + &self, + entrypoint_id: EntrypointId, + plugin_preferences_required: bool, + entrypoint_preferences_required: bool + ) -> anyhow::Result<()>; + async fn ui_clear_inline_view(&self) -> anyhow::Result<()>; } \ No newline at end of file diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 2eeba9e..b27ad47 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -1,44 +1,6 @@ -use std::collections::HashMap; -use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, UiPropertyValue, UiWidgetId}; +use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, UiPropertyValue, UiWidgetId}; use serde::{Deserialize, Serialize}; -#[derive(Debug)] -pub enum JsUiResponseData { - Nothing -} - -#[derive(Debug)] -pub enum JsUiRequestData { - ReplaceView { - entrypoint_id: EntrypointId, - entrypoint_name: String, - render_location: JsUiRenderLocation, - top_level_view: bool, - container: RootWidget, - #[cfg(feature = "scenario_runner")] - container_value: serde_value::Value, - images: HashMap, - }, - ClearInlineView, - ShowPluginErrorView { - entrypoint_id: EntrypointId, - render_location: JsUiRenderLocation, - }, - ShowPreferenceRequiredView { - entrypoint_id: EntrypointId, - plugin_preferences_required: bool, - entrypoint_preferences_required: bool - }, - ShowHud { - display: String - }, - UpdateLoadingBar { - plugin_id: PluginId, - entrypoint_id: EntrypointId, - show: bool - }, -} - #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] pub enum JsUiRenderLocation { InlineView, diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 5913c92..6b924e3 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -11,6 +11,7 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, Context}; +use bytes::Bytes; use deno_core::futures::executor::block_on; use deno_core::futures::{FutureExt, Stream, StreamExt}; use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; @@ -26,18 +27,19 @@ use indexmap::IndexMap; use once_cell::sync::Lazy; use regex::Regex; use serde::{Deserialize, Serialize}; +use serde_value::Value; use tokio::net::TcpStream; use tokio::task::spawn_blocking; use tokio_util::sync::CancellationToken; use common::dirs::Dirs; -use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; +use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; use common::rpc::frontend_api::FrontendApi; use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; use common_plugin_runtime::model::{AdditionalSearchItem, ClipboardData, PreferenceUserData}; use component_model::{create_component_model, Children, Component, Property, PropertyType, SharedType}; -use crate::model::{IntermediateUiEvent, JsKeyboardEventOrigin, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData}; +use crate::model::{IntermediateUiEvent, JsKeyboardEventOrigin, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation}; use crate::plugins::clipboard::Clipboard; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; @@ -354,21 +356,21 @@ async fn start_js_runtime( PluginData::new( plugin_id.clone(), plugin_uuid.clone(), - plugin_name, - entrypoint_names, plugin_cache_dir, plugin_data_dir, inline_view_entrypoint_id, ), - frontend_api, ComponentModel::new(component_model), BackendForPluginRuntimeApiImpl::new( icon_cache, repository, search_index, clipboard, + frontend_api, plugin_uuid, plugin_id, + plugin_name, + entrypoint_names, runtime_permissions, ), numbat_context, @@ -599,7 +601,6 @@ deno_core::extension!( options = { event_receiver: EventReceiver, plugin_data: PluginData, - frontend_api: FrontendApi, component_model: ComponentModel, backend_api: BackendForPluginRuntimeApiImpl, numbat_context: Option, @@ -607,7 +608,6 @@ deno_core::extension!( state = |state, options| { state.put(options.event_receiver); state.put(options.plugin_data); - state.put(options.frontend_api); state.put(options.component_model); state.put(options.backend_api); state.put(options.numbat_context); @@ -727,101 +727,6 @@ pub async fn op_plugin_get_pending_event(state: Rc>) -> anyhow: Ok(from_intermediate_to_js_event(event)) } -fn make_request(state: &Rc>, data: JsUiRequestData) -> anyhow::Result { - let (plugin_id, plugin_name, mut frontend_api) = { - let state = state.borrow(); - - let plugin_id = state - .borrow::() - .plugin_id() - .clone(); - - let plugin_name = state - .borrow::() - .plugin_name() - .to_string(); - - let frontend_api = state - .borrow::() - .clone(); - - (plugin_id, plugin_name, frontend_api) - }; - - block_on(async { - make_request_async(plugin_id, plugin_name, &mut frontend_api, data).await - }) -} - -async fn make_request_async(plugin_id: PluginId, plugin_name: String, frontend_api: &mut FrontendApi, data: JsUiRequestData) -> anyhow::Result { - match data { - JsUiRequestData::ReplaceView { - entrypoint_id, - entrypoint_name, - render_location, - top_level_view, - container, - #[cfg(feature = "scenario_runner")] - container_value, - images - } => { - let render_location = match render_location { // TODO into? - JsUiRenderLocation::InlineView => UiRenderLocation::InlineView, - JsUiRenderLocation::View => UiRenderLocation::View, - }; - - frontend_api.replace_view( - plugin_id, - plugin_name, - entrypoint_id, - entrypoint_name, - render_location, - top_level_view, - container, - #[cfg(feature = "scenario_runner")] - container_value, - images - ).await?; - - Ok(JsUiResponseData::Nothing) - } - JsUiRequestData::ClearInlineView => { - - frontend_api.clear_inline_view(plugin_id).await?; - - Ok(JsUiResponseData::Nothing) - } - JsUiRequestData::ShowPreferenceRequiredView { plugin_preferences_required, entrypoint_preferences_required, entrypoint_id } => { - - frontend_api.show_preference_required_view(plugin_id, entrypoint_id, plugin_preferences_required, entrypoint_preferences_required).await?; - - Ok(JsUiResponseData::Nothing) - } - JsUiRequestData::ShowPluginErrorView { entrypoint_id, render_location } => { - let render_location = match render_location { // TODO into? - JsUiRenderLocation::InlineView => UiRenderLocation::InlineView, - JsUiRenderLocation::View => UiRenderLocation::View, - }; - - frontend_api.show_plugin_error_view(plugin_id, entrypoint_id, render_location).await?; - - Ok(JsUiResponseData::Nothing) - } - JsUiRequestData::ShowHud { display } => { - - frontend_api.show_hud(display).await?; - - Ok(JsUiResponseData::Nothing) - } - JsUiRequestData::UpdateLoadingBar { plugin_id, entrypoint_id, show } => { - - frontend_api.update_loading_bar(plugin_id, entrypoint_id, show).await?; - - Ok(JsUiResponseData::Nothing) - } - } -} - fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { match event { IntermediateUiEvent::OpenView { entrypoint_id } => JsUiEvent::OpenView { @@ -877,8 +782,6 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { pub struct PluginData { plugin_id: PluginId, plugin_uuid: String, - plugin_name: String, - entrypoint_names: HashMap, plugin_cache_dir: String, plugin_data_dir: String, inline_view_entrypoint_id: Option, @@ -888,8 +791,6 @@ impl PluginData { fn new( plugin_id: PluginId, plugin_uuid: String, - plugin_name: String, - entrypoint_names: HashMap, plugin_cache_dir: String, plugin_data_dir: String, inline_view_entrypoint_id: Option, @@ -897,8 +798,6 @@ impl PluginData { Self { plugin_id, plugin_uuid, - plugin_name, - entrypoint_names, plugin_cache_dir, plugin_data_dir, inline_view_entrypoint_id, @@ -913,10 +812,6 @@ impl PluginData { &self.plugin_uuid } - fn plugin_name(&self) -> &str { - &self.plugin_name - } - fn plugin_cache_dir(&self) -> &str { &self.plugin_cache_dir } @@ -968,8 +863,11 @@ pub struct BackendForPluginRuntimeApiImpl { repository: DataDbRepository, search_index: SearchIndex, clipboard: Clipboard, + frontend_api: FrontendApi, plugin_uuid: String, plugin_id: PluginId, + plugin_name: String, + entrypoint_names: HashMap, permissions: PluginRuntimePermissions } @@ -979,8 +877,11 @@ impl BackendForPluginRuntimeApiImpl { repository: DataDbRepository, search_index: SearchIndex, clipboard: Clipboard, + frontend_api: FrontendApi, plugin_uuid: String, plugin_id: PluginId, + plugin_name: String, + entrypoint_names: HashMap, permissions: PluginRuntimePermissions ) -> Self { Self { @@ -988,8 +889,11 @@ impl BackendForPluginRuntimeApiImpl { repository, search_index, clipboard, + frontend_api, plugin_uuid, plugin_id, + plugin_name, + entrypoint_names, permissions } } @@ -1148,20 +1052,6 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { Ok(result) } - async fn get_action_id_for_shortcut(&self, entrypoint_id: &str, key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> anyhow::Result> { - let result = self.repository.get_action_id_for_shortcut( - &self.plugin_id.to_string(), - &entrypoint_id, - key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta - ).await?; - - Ok(result) - } - async fn get_plugin_preferences(&self) -> anyhow::Result> { let DbReadPlugin { preferences, preferences_user_data, .. } = self.repository .get_plugin_by_id(&self.plugin_id.to_string()) @@ -1266,6 +1156,109 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { self.clipboard.clear() } + + async fn ui_update_loading_bar(&self, entrypoint_id: EntrypointId, show: bool) -> anyhow::Result<()> { + self.frontend_api.update_loading_bar(self.plugin_id.clone(), entrypoint_id, show).await?; + + Ok(()) + } + + async fn ui_show_hud(&self, display: String) -> anyhow::Result<()> { + self.frontend_api.show_hud(display).await?; + + Ok(()) + } + + async fn ui_get_action_id_for_shortcut( + &self, + entrypoint_id: EntrypointId, + key: String, + modifier_shift: bool, + modifier_control: bool, + modifier_alt: bool, + modifier_meta: bool + ) -> anyhow::Result> { + let result = self.repository.get_action_id_for_shortcut( + &self.plugin_id.to_string(), + &entrypoint_id.to_string(), + PhysicalKey::from_value(key), + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + ).await?; + + Ok(result) + } + + async fn ui_render( + &self, + entrypoint_id: EntrypointId, + render_location: UiRenderLocation, + top_level_view: bool, + container: RootWidget, + #[cfg(feature = "scenario_runner")] + container_value: serde_value::Value, + images: HashMap + ) -> anyhow::Result<()> { + + let entrypoint_name = self.entrypoint_names + .get(&entrypoint_id) + .expect("entrypoint name for id should always exist") + .to_string(); + + self.frontend_api.replace_view( + self.plugin_id.clone(), + self.plugin_name.clone(), + entrypoint_id, + entrypoint_name, + render_location, + top_level_view, + container, + #[cfg(feature = "scenario_runner")] + container_value, + images + ).await?; + + Ok(()) + } + + async fn ui_show_plugin_error_view( + &self, + entrypoint_id: EntrypointId, + render_location: UiRenderLocation + ) -> anyhow::Result<()> { + self.frontend_api.show_plugin_error_view( + self.plugin_id.clone(), + entrypoint_id, + render_location + ).await?; + + Ok(()) + } + + async fn ui_show_preferences_required_view( + &self, + entrypoint_id: EntrypointId, + plugin_preferences_required: bool, + entrypoint_preferences_required: bool + ) -> anyhow::Result<()> { + + self.frontend_api.show_preference_required_view( + self.plugin_id.clone(), + entrypoint_id, + plugin_preferences_required, + entrypoint_preferences_required + ).await?; + + Ok(()) + } + + async fn ui_clear_inline_view(&self) -> anyhow::Result<()> { + self.frontend_api.clear_inline_view(self.plugin_id.clone()).await?; + + Ok(()) + } } diff --git a/rust/server/src/plugins/js/ui.rs b/rust/server/src/plugins/js/ui.rs index 0a1702e..9211c9e 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/server/src/plugins/js/ui.rs @@ -9,58 +9,73 @@ use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; use indexmap::IndexMap; use serde::{de, Deserialize, Deserializer}; use serde::de::Error; -use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, EntrypointId, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Image, ImageSource, ImageSourceAsset, ImageSourceUrl, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectItemWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiPropertyValue, UiWidgetId, WidgetVisitor}; +use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, EntrypointId, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Image, ImageSource, ImageSourceAsset, ImageSourceUrl, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectItemWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiPropertyValue, UiRenderLocation, UiWidgetId, WidgetVisitor}; use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; use component_model::{Component, Property, PropertyType, SharedType}; use component_model::Component::Root; -use crate::model::{JsUiRenderLocation, JsUiRequestData, JsUiResponseData}; +use crate::model::JsUiRenderLocation; use crate::plugins::data_db_repository::DataDbRepository; -use crate::plugins::js::{ComponentModel, make_request, PluginData, BackendForPluginRuntimeApiImpl}; +use crate::plugins::js::{ComponentModel, PluginData, BackendForPluginRuntimeApiImpl}; #[op2] pub fn show_plugin_error_view(state: Rc>, #[string] entrypoint_id: String, #[serde] render_location: JsUiRenderLocation) -> anyhow::Result<()> { - let data = JsUiRequestData::ShowPluginErrorView { - entrypoint_id: EntrypointId::from_string(entrypoint_id), - render_location, + let api = { + let state = state.borrow(); + + let api = state + .borrow::() + .clone(); + + api }; - match make_request(&state, data).context("ClearInlineView frontend response")? { - JsUiResponseData::Nothing => { - tracing::trace!(target = "renderer_rs", "Calling show_plugin_error_view returned"); - Ok(()) - } - value @ _ => panic!("unsupported response type {:?}", value), - } + let render_location = match render_location { + JsUiRenderLocation::InlineView => UiRenderLocation::InlineView, + JsUiRenderLocation::View => UiRenderLocation::View, + }; + + block_on(async { + api.ui_show_plugin_error_view( + EntrypointId::from_string(entrypoint_id), + render_location, + ).await + }) } #[op2(fast)] pub fn show_preferences_required_view(state: Rc>, #[string] entrypoint_id: String, plugin_preferences_required: bool, entrypoint_preferences_required: bool) -> anyhow::Result<()> { - let data = JsUiRequestData::ShowPreferenceRequiredView { - entrypoint_id: EntrypointId::from_string(entrypoint_id), - plugin_preferences_required, - entrypoint_preferences_required + let api = { + let state = state.borrow(); + + let api = state + .borrow::() + .clone(); + + api }; - match make_request(&state, data).context("ShowPreferenceRequiredView frontend response")? { - JsUiResponseData::Nothing => { - tracing::trace!(target = "renderer_rs", "Calling show_preferences_required_view returned"); - Ok(()) - } - value @ _ => panic!("unsupported response type {:?}", value), - } + block_on(async { + api.ui_show_preferences_required_view( + EntrypointId::from_string(entrypoint_id), + plugin_preferences_required, + entrypoint_preferences_required + ).await + }) } #[op2(fast)] pub fn clear_inline_view(state: Rc>) -> anyhow::Result<()> { - let data = JsUiRequestData::ClearInlineView; + let api = { + let state = state.borrow(); - match make_request(&state, data).context("ClearInlineView frontend response")? { - JsUiResponseData::Nothing => { - tracing::trace!(target = "renderer_rs", "Calling clear_inline_view returned"); - Ok(()) - } - value @ _ => panic!("unsupported response type {:?}", value), - } + let api = state + .borrow::() + .clone(); + + api + }; + + block_on(async { api.ui_clear_inline_view().await }) } #[op2] @@ -83,21 +98,6 @@ pub fn op_react_replace_view<'a>( ) -> anyhow::Result<()> { tracing::trace!(target = "renderer_rs", "Calling op_react_replace_view..."); - let entrypoint_id = EntrypointId::from_string(entrypoint_id); - - let entrypoint_name = { - let comp_state = state.borrow(); - - let plugin_data = comp_state.borrow::(); - - let entrypoint_name = plugin_data.entrypoint_names - .get(&entrypoint_id) - .expect("entrypoint name for id should always exist") - .to_string(); - - entrypoint_name - }; - let mut deserializer = serde_v8::Deserializer::new(scope, container.v8_value, None); #[cfg(feature = "scenario_runner")] @@ -106,24 +106,32 @@ pub fn op_react_replace_view<'a>( let images = ImageGatherer::run_gatherer(state.clone(), &container)?; - let data = JsUiRequestData::ReplaceView { - entrypoint_id, - entrypoint_name, - render_location, - top_level_view, - container, - #[cfg(feature = "scenario_runner")] - container_value, - images, + let api = { + let state = state.borrow(); + + let api = state + .borrow::() + .clone(); + + api }; - match make_request(&state, data).context("ReplaceView frontend response")? { - JsUiResponseData::Nothing => { - tracing::trace!(target = "renderer_rs", "Calling op_react_replace_view returned"); - Ok(()) - } - value @ _ => panic!("unsupported response type {:?}", value), - } + let render_location = match render_location { + JsUiRenderLocation::InlineView => UiRenderLocation::InlineView, + JsUiRenderLocation::View => UiRenderLocation::View, + }; + + block_on(async { + api.ui_render( + EntrypointId::from_string(entrypoint_id), + render_location, + top_level_view, + container, + #[cfg(feature = "scenario_runner")] + container_value, + images + ).await + }) } #[op2] @@ -156,9 +164,9 @@ pub async fn fetch_action_id_for_shortcut( api }; - let result = api.get_action_id_for_shortcut( - &entrypoint_id, - PhysicalKey::from_value(key), + let result = api.ui_get_action_id_for_shortcut( + EntrypointId::from_string(entrypoint_id), + key, modifier_shift, modifier_control, modifier_alt, @@ -170,45 +178,32 @@ pub async fn fetch_action_id_for_shortcut( #[op2(async)] pub async fn show_hud(state: Rc>, #[string] display: String) -> anyhow::Result<()> { - let data = JsUiRequestData::ShowHud { - display + let api = { + let state = state.borrow(); + + let api = state + .borrow::() + .clone(); + + api }; - match make_request(&state, data).context("ShowHud frontend response")? { - JsUiResponseData::Nothing => { - tracing::trace!("Calling show_hud returned"); - Ok(()) - } - value @ _ => panic!("unsupported response type {:?}", value), - } + block_on(async { api.ui_show_hud(display).await }) } #[op2(async)] pub async fn update_loading_bar(state: Rc>, #[string] entrypoint_id: String, show: bool) -> anyhow::Result<()> { - let plugin_id = { + let api = { let state = state.borrow(); - let plugin_id = state - .borrow::() - .plugin_id() + let api = state + .borrow::() .clone(); - plugin_id + api }; - let data = JsUiRequestData::UpdateLoadingBar { - plugin_id: PluginId::from_string(plugin_id), - entrypoint_id: EntrypointId::from_string(entrypoint_id), - show, - }; - - match make_request(&state, data).context("UpdateLoadingBar response")? { - JsUiResponseData::Nothing => { - tracing::trace!("Calling update_loading_bar returned"); - Ok(()) - } - value @ _ => panic!("unsupported response type {:?}", value), - } + block_on(async { api.ui_update_loading_bar(EntrypointId::from_string(entrypoint_id), show).await }) } struct ImageGatherer { From f45c797cdccd9251180171149dd959689ba31e50 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:08:36 +0100 Subject: [PATCH 210/540] Move out dirs usages out of plugin runtime --- rust/common/src/dirs.rs | 10 +---- rust/server/src/plugins/data_db_repository.rs | 7 +++- rust/server/src/plugins/js/mod.rs | 23 +++++++++- rust/server/src/plugins/js/permissions.rs | 42 ++++++++++++------- .../src/plugins/js/plugins/applications.rs | 33 ++++++++++++--- .../plugins/js/plugins/applications/linux.rs | 11 ++--- 6 files changed, 88 insertions(+), 38 deletions(-) diff --git a/rust/common/src/dirs.rs b/rust/common/src/dirs.rs index 994bf9e..c17941d 100644 --- a/rust/common/src/dirs.rs +++ b/rust/common/src/dirs.rs @@ -26,15 +26,13 @@ impl Dirs { pub fn data_db_file(&self) -> anyhow::Result { let path = self.data_dir()?.join("data.db"); + Ok(path) } pub fn plugin_data(&self, plugin_uuid: &str) -> anyhow::Result { let plugin_data_dir = self.data_dir()?.join("plugins").join(&plugin_uuid); - std::fs::create_dir_all(&plugin_data_dir) - .context("Unable to create plugin data directory")?; - Ok(plugin_data_dir) } @@ -45,9 +43,6 @@ impl Dirs { Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../dev_data/data")).to_owned() }; - std::fs::create_dir_all(&data_dir) - .context("Unable to create data directory")?; - Ok(data_dir) } @@ -88,9 +83,6 @@ impl Dirs { pub fn plugin_cache(&self, plugin_uuid: &str) -> anyhow::Result { let plugin_cache_dir = self.cache_dir().join("plugins").join(&plugin_uuid); - std::fs::create_dir_all(&plugin_cache_dir) - .context("Unable to create plugin cache directory")?; - Ok(plugin_cache_dir) } diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index fc29f11..873e9ef 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -321,8 +321,13 @@ pub struct DbPluginEntrypointFrecencyStats { impl DataDbRepository { pub async fn new(dirs: Dirs) -> anyhow::Result { + let data_db_file = dirs.data_db_file()?; + + std::fs::create_dir_all(&data_db_file.parent().unwrap()) + .context("Unable to create data directory")?; + let conn = SqliteConnectOptions::new() - .filename(dirs.data_db_file()?) + .filename(data_db_file) .create_if_missing(true); let pool = SqlitePool::connect_with(conn) diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 6b924e3..5302646 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -324,10 +324,17 @@ async fn start_js_runtime( (StdioPipe::inherit(), StdioPipe::inherit()) }; + let home_dir = dirs.home_dir(); let local_storage_dir = dirs.plugin_local_storage(&plugin_uuid); let plugin_cache_dir = dirs.plugin_cache(&plugin_uuid)?.to_str().expect("non-uft8 paths are not supported").to_string(); let plugin_data_dir = dirs.plugin_data(&plugin_uuid)?.to_str().expect("non-uft8 paths are not supported").to_string(); + std::fs::create_dir_all(&plugin_cache_dir) + .context("Unable to create plugin cache directory")?; + + std::fs::create_dir_all(&plugin_data_dir) + .context("Unable to create plugin data directory")?; + let init_url: ModuleSpecifier = "gauntlet:init".parse().expect("should be valid"); let numbat_context = if plugin_id_str == "bundled://gauntlet" { @@ -338,7 +345,13 @@ async fn start_js_runtime( let fs: Arc = Arc::new(RealFs); - let permissions_container = permissions_to_deno(fs.clone(), &permissions, &dirs, &plugin_uuid)?; + let permissions_container = permissions_to_deno( + fs.clone(), + &permissions, + &home_dir, + &PathBuf::from(&plugin_data_dir), + &PathBuf::from(&plugin_cache_dir), + )?; let runtime_permissions = PluginRuntimePermissions { clipboard: permissions.clipboard, @@ -359,6 +372,7 @@ async fn start_js_runtime( plugin_cache_dir, plugin_data_dir, inline_view_entrypoint_id, + home_dir ), ComponentModel::new(component_model), BackendForPluginRuntimeApiImpl::new( @@ -785,6 +799,7 @@ pub struct PluginData { plugin_cache_dir: String, plugin_data_dir: String, inline_view_entrypoint_id: Option, + home_dir: PathBuf, } impl PluginData { @@ -794,6 +809,7 @@ impl PluginData { plugin_cache_dir: String, plugin_data_dir: String, inline_view_entrypoint_id: Option, + home_dir: PathBuf, ) -> Self { Self { plugin_id, @@ -801,6 +817,7 @@ impl PluginData { plugin_cache_dir, plugin_data_dir, inline_view_entrypoint_id, + home_dir } } @@ -823,6 +840,10 @@ impl PluginData { fn inline_view_entrypoint_id(&self) -> Option { self.inline_view_entrypoint_id.clone() } + + fn home_dir(&self) -> PathBuf { + self.home_dir.clone() + } } pub struct ComponentModel { diff --git a/rust/server/src/plugins/js/permissions.rs b/rust/server/src/plugins/js/permissions.rs index 2bf85ab..4f9c1f8 100644 --- a/rust/server/src/plugins/js/permissions.rs +++ b/rust/server/src/plugins/js/permissions.rs @@ -44,16 +44,22 @@ pub enum PluginPermissionsMainSearchBar { Read, } -pub fn permissions_to_deno(fs: FileSystemRc, permissions: &PluginPermissions, dirs: &Dirs, plugin_uuid: &str) -> anyhow::Result { +pub fn permissions_to_deno( + fs: FileSystemRc, + permissions: &PluginPermissions, + home_dir: &Path, + plugin_data_dir: &Path, + plugin_cache_dir: &Path, +) -> anyhow::Result { Ok(PermissionsContainer::new( Arc::new(RuntimePermissionDescriptorParser::new(fs)), Permissions { - read: path_permission(&permissions.filesystem.read, ReadDescriptor, dirs, plugin_uuid)?, - write: path_permission(&permissions.filesystem.write, WriteDescriptor, dirs, plugin_uuid)?, + read: path_permission(&permissions.filesystem.read, ReadDescriptor, home_dir, plugin_data_dir, plugin_cache_dir)?, + write: path_permission(&permissions.filesystem.write, WriteDescriptor, home_dir, plugin_data_dir, plugin_cache_dir)?, net: net_permission(&permissions.network), env: env_permission(&permissions.environment), sys: sys_permission(&permissions.system)?, - run: run_permission(&permissions.exec, dirs, plugin_uuid)?, + run: run_permission(&permissions.exec, home_dir, plugin_data_dir, plugin_cache_dir)?, ffi: Permissions::new_unary(None, None, false), import: UnaryPermission::default(), all: Permissions::new_all(false), @@ -64,13 +70,14 @@ pub fn permissions_to_deno(fs: FileSystemRc, permissions: &PluginPermissions, di fn path_permission + Hash>( paths: &[String], to_permission: fn(PathBuf) -> P, - dirs: &Dirs, - plugin_uuid: &str + home_dir: &Path, + plugin_data_dir: &Path, + plugin_cache_dir: &Path, ) -> anyhow::Result> { let allow_list = paths .into_iter() .map(|path| { - augment_path(path, dirs, plugin_uuid) + augment_path(path, home_dir, plugin_data_dir, plugin_cache_dir) .map(|path| path.map(|path| to_permission(path))) }) .collect::>>()? @@ -134,11 +141,16 @@ fn sys_permission(system: &[String]) -> anyhow::Result anyhow::Result> { +fn run_permission( + permissions: &PluginPermissionsExec, + home_dir: &Path, + plugin_data_dir: &Path, + plugin_cache_dir: &Path, +) -> anyhow::Result> { let granted_executable = permissions.executable .iter() .map(|path| { - augment_path(path, dirs, plugin_uuid) + augment_path(path, home_dir, plugin_data_dir, plugin_cache_dir) .map(|path| path.map(|path| AllowRunDescriptor(path))) }) .collect::>>()? @@ -164,7 +176,7 @@ fn run_permission(permissions: &PluginPermissionsExec, dirs: &Dirs, plugin_uuid: Ok(Permissions::new_unary(allow_list, None, false)) } -fn augment_path(path: &String, dirs: &Dirs, plugin_uuid: &str) -> anyhow::Result> { +fn augment_path(path: &String, home_dir: &Path, plugin_data_dir: &Path, plugin_cache_dir: &Path) -> anyhow::Result> { if let Some(matches) = VARIABLE_PATTERN.captures(path) { let namespace = &matches["namespace"]; let name = &matches["name"]; @@ -172,27 +184,27 @@ fn augment_path(path: &String, dirs: &Dirs, plugin_uuid: &str) -> anyhow::Result let replacement = match (namespace, name) { ("macos", "user-home") => { if cfg!(target_os = "macos") { - Some(dirs.home_dir()) + Some(home_dir) } else { None } }, ("linux", "user-home") => { if cfg!(target_os = "linux") { - Some(dirs.home_dir()) + Some(home_dir) } else { None } }, ("windows", "user-home") => { if cfg!(windows) { - Some(dirs.home_dir()) + Some(home_dir) } else { None } }, - ("common", "plugin-data") => Some(dirs.plugin_data(plugin_uuid)?), - ("common", "plugin-cache") => Some(dirs.plugin_cache(plugin_uuid)?), + ("common", "plugin-data") => Some(plugin_data_dir), + ("common", "plugin-cache") => Some(plugin_cache_dir), (_, _) => { Err(anyhow!("Trying to load plugin with unknown variable in path in manifest permissions: {}", path))? } diff --git a/rust/server/src/plugins/js/plugins/applications.rs b/rust/server/src/plugins/js/plugins/applications.rs index 1084bb1..ea57a69 100644 --- a/rust/server/src/plugins/js/plugins/applications.rs +++ b/rust/server/src/plugins/js/plugins/applications.rs @@ -1,9 +1,12 @@ -use deno_core::op2; +use std::cell::RefCell; +use deno_core::{op2, OpState}; use std::path::PathBuf; +use std::rc::Rc; use image::ImageFormat; use image::imageops::FilterType; use serde::Serialize; use tokio::task::spawn_blocking; +use crate::plugins::js::PluginData; #[cfg(target_os = "linux")] mod linux; @@ -72,15 +75,35 @@ pub fn current_os() -> &'static str { #[cfg(target_os = "linux")] #[op2(async)] #[serde] -pub async fn linux_app_from_path(#[string] path: String) -> anyhow::Result> { - Ok(spawn_blocking(|| linux::linux_app_from_path(PathBuf::from(path))).await?) +pub async fn linux_app_from_path(state: Rc>, #[string] path: String) -> anyhow::Result> { + let home_dir = { + let state = state.borrow(); + + let home_dir = state + .borrow::() + .home_dir(); + + home_dir + }; + + Ok(spawn_blocking(|| linux::linux_app_from_path(home_dir, PathBuf::from(path))).await?) } #[cfg(target_os = "linux")] #[op2] #[serde] -pub fn linux_application_dirs() -> Vec { - linux::linux_application_dirs() +pub fn linux_application_dirs(state: Rc>) -> Vec { + let home_dir = { + let state = state.borrow(); + + let home_dir = state + .borrow::() + .home_dir(); + + home_dir + }; + + linux::linux_application_dirs(home_dir) .into_iter() .map(|path| path.to_str().expect("non-utf8 paths are not supported").to_string()) .collect() diff --git a/rust/server/src/plugins/js/plugins/applications/linux.rs b/rust/server/src/plugins/js/plugins/applications/linux.rs index 3b9f01d..19577ca 100644 --- a/rust/server/src/plugins/js/plugins/applications/linux.rs +++ b/rust/server/src/plugins/js/plugins/applications/linux.rs @@ -4,22 +4,19 @@ use std::path::{Path, PathBuf}; use std::{env, fs}; use crate::plugins::js::plugins::applications::{resize_icon, DesktopApplication, DesktopPathAction}; -use common::dirs::Dirs; use freedesktop_entry_parser::parse_entry; use freedesktop_icons::lookup; use image::imageops::FilterType; use image::ImageFormat; use walkdir::WalkDir; -pub fn linux_application_dirs() -> Vec { +pub fn linux_application_dirs(home_dir: PathBuf) -> Vec { let data_home = match env::var_os("XDG_DATA_HOME") { Some(val) => { PathBuf::from(val) }, None => { - let dirs = Dirs::new(); - - dirs.home_dir() + home_dir .join(".local") .join("share") } @@ -53,8 +50,8 @@ pub fn linux_application_dirs() -> Vec { .collect() } -pub fn linux_app_from_path(path: PathBuf) -> Option { - let app_directories = linux_application_dirs(); +pub fn linux_app_from_path(home_dir: PathBuf, path: PathBuf) -> Option { + let app_directories = linux_application_dirs(home_dir); let relative_to_app_dir = app_directories .into_iter() From 49b52488f06bc06704d36a5834240a8e2c6e9459 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:18:47 +0100 Subject: [PATCH 211/540] Move ComponentModel to separate file --- js/react_renderer/src/renderer.ts | 3 +- rust/server/src/plugins/js/component_model.rs | 29 +++++++++++++++++++ rust/server/src/plugins/js/mod.rs | 27 ++--------------- rust/server/src/plugins/js/ui.rs | 2 +- 4 files changed, 35 insertions(+), 26 deletions(-) create mode 100644 rust/server/src/plugins/js/component_model.rs diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index 3ee53a0..92380a5 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -149,6 +149,8 @@ function createWidget(hostContext: HostContext, type: ComponentType, properties: return instance } +const componentModel = op_component_model(); + export const createHostConfig = (): HostConfig< ComponentType, PropsWithChildren, @@ -228,7 +230,6 @@ export const createHostConfig = (): HostConfig< return false; }, getRootHostContext: (_rootContainer: RootUiWidget): HostContext | null => { - const componentModel = op_component_model(); return new HostContext(1, componentModel); }, diff --git a/rust/server/src/plugins/js/component_model.rs b/rust/server/src/plugins/js/component_model.rs new file mode 100644 index 0000000..10edcb9 --- /dev/null +++ b/rust/server/src/plugins/js/component_model.rs @@ -0,0 +1,29 @@ +use std::collections::HashMap; +use component_model::{create_component_model, Component}; + +pub struct ComponentModel { + components: HashMap, +} + +impl ComponentModel { + pub fn new() -> Self { + let components = create_component_model() + .into_iter() + .filter_map(|component| { + match &component { + Component::Standard { internal_name, .. } => Some((format!("gauntlet:{}", internal_name), component)), + Component::Root { internal_name, .. } => Some((format!("gauntlet:{}", internal_name), component)), + Component::TextPart { internal_name, .. } => Some((format!("gauntlet:{}", internal_name), component)), + } + }) + .collect(); + + Self { + components + } + } + + pub fn components(&self) -> &HashMap { + &self.components + } +} \ No newline at end of file diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 5302646..99455aa 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -37,7 +37,6 @@ use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, Ro use common::rpc::frontend_api::FrontendApi; use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; use common_plugin_runtime::model::{AdditionalSearchItem, ClipboardData, PreferenceUserData}; -use component_model::{create_component_model, Children, Component, Property, PropertyType, SharedType}; use crate::model::{IntermediateUiEvent, JsKeyboardEventOrigin, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation}; use crate::plugins::clipboard::Clipboard; @@ -46,6 +45,7 @@ use crate::plugins::icon_cache::IconCache; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; use crate::plugins::js::command_generators::{get_command_generator_entrypoint_ids}; +use crate::plugins::js::component_model::ComponentModel; use crate::plugins::js::environment::{environment_gauntlet_version, environment_is_development, environment_plugin_cache_dir, environment_plugin_data_dir}; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; use crate::plugins::js::permissions::{permissions_to_deno, PluginPermissions, PluginPermissionsClipboard}; @@ -68,6 +68,7 @@ mod command_generators; pub mod clipboard; pub mod permissions; mod environment; +mod component_model; pub struct PluginRuntimeData { pub id: PluginId, @@ -145,8 +146,6 @@ pub enum AllPluginCommandData { } pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: RunStatusGuard) -> anyhow::Result<()> { - let component_model = create_component_model(); - let mut command_receiver = data.command_receiver; let command_stream = async_stream::stream! { loop { @@ -258,7 +257,6 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run data.inline_view_entrypoint_id, event_stream, data.frontend_api, - component_model, data.db_repository, data.search_index, data.icon_cache, @@ -300,7 +298,6 @@ async fn start_js_runtime( inline_view_entrypoint_id: Option, event_stream: Pin>>, frontend_api: FrontendApi, - component_model: Vec, repository: DataDbRepository, search_index: SearchIndex, icon_cache: IconCache, @@ -374,7 +371,7 @@ async fn start_js_runtime( inline_view_entrypoint_id, home_dir ), - ComponentModel::new(component_model), + ComponentModel::new(), BackendForPluginRuntimeApiImpl::new( icon_cache, repository, @@ -846,25 +843,7 @@ impl PluginData { } } -pub struct ComponentModel { - components: HashMap, -} -impl ComponentModel { - fn new(components: Vec) -> Self { - Self { - components: components.into_iter() - .filter_map(|component| { - match &component { - Component::Standard { internal_name, .. } => Some((format!("gauntlet:{}", internal_name), component)), - Component::Root { internal_name, .. } => Some((format!("gauntlet:{}", internal_name), component)), - Component::TextPart { internal_name, .. } => Some((format!("gauntlet:{}", internal_name), component)), - } - }) - .collect() - } - } -} pub struct EventReceiver { event_stream: Rc>>>>, diff --git a/rust/server/src/plugins/js/ui.rs b/rust/server/src/plugins/js/ui.rs index 9211c9e..2c1b826 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/server/src/plugins/js/ui.rs @@ -139,7 +139,7 @@ pub fn op_react_replace_view<'a>( pub fn op_component_model(state: Rc>) -> HashMap { state.borrow() .borrow::() - .components + .components() .clone() } From 337a8eaea49f84c051313075d5b2381b15e462d4 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:25:54 +0100 Subject: [PATCH 212/540] Move Numbat context to gauntlet_internal_all extension --- rust/server/src/plugins/js/mod.rs | 19 ++++++++----------- rust/server/src/plugins/js/plugins/numbat.rs | 6 +----- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 99455aa..9dad5a9 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -334,12 +334,6 @@ async fn start_js_runtime( let init_url: ModuleSpecifier = "gauntlet:init".parse().expect("should be valid"); - let numbat_context = if plugin_id_str == "bundled://gauntlet" { - Some(NumbatContext::new()) - } else { - None - }; - let fs: Arc = Arc::new(RealFs); let permissions_container = permissions_to_deno( @@ -384,13 +378,12 @@ async fn start_js_runtime( entrypoint_names, runtime_permissions, ), - numbat_context, ), gauntlet_esm, ]; if plugin_id_str == "bundled://gauntlet" { - extensions.push(gauntlet_internal_all::init_ops_and_esm()); + extensions.push(gauntlet_internal_all::init_ops_and_esm(NumbatContext::new())); #[cfg(target_os = "macos")] extensions.push(gauntlet_internal_macos::init_ops_and_esm()); @@ -614,14 +607,12 @@ deno_core::extension!( plugin_data: PluginData, component_model: ComponentModel, backend_api: BackendForPluginRuntimeApiImpl, - numbat_context: Option, }, state = |state, options| { state.put(options.event_receiver); state.put(options.plugin_data); state.put(options.component_model); state.put(options.backend_api); - state.put(options.numbat_context); }, ); @@ -676,7 +667,13 @@ deno_core::extension!( esm = [ "ext:gauntlet/internal-all/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-all-bootstrap.js", "ext:gauntlet/internal-all.js" = "../../js/core/dist/internal-all.js", - ] + ], + options = { + numbat_context: NumbatContext, + }, + state = |state, options| { + state.put(options.numbat_context); + }, ); #[cfg(target_os = "linux")] diff --git a/rust/server/src/plugins/js/plugins/numbat.rs b/rust/server/src/plugins/js/plugins/numbat.rs index c7507d2..132dbf7 100644 --- a/rust/server/src/plugins/js/plugins/numbat.rs +++ b/rust/server/src/plugins/js/plugins/numbat.rs @@ -41,16 +41,12 @@ pub fn run_numbat(state: Rc>, #[string] input: String) -> anyho let state = state.borrow(); let context = state - .borrow::>() + .borrow::() .clone(); context }; - let Some(context) = context else { - return Err(anyhow!("plugin id is not equal to 'bundled://gauntlet'")) - }; - let mut context = context.0.borrow_mut(); let (statements, result) = context.interpret(&input, CodeSource::Text)?; From 0b67e67d0bc35faa99dc172309cf2fafb0d9a8dd Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 16 Dec 2024 22:36:38 +0100 Subject: [PATCH 213/540] Each JS runtime is in separate process --- Cargo.lock | 145 +- Cargo.toml | 2 +- package-lock.json | 1 + rust/client/src/ui/client_context.rs | 2 +- rust/client/src/ui/mod.rs | 4 +- rust/client/src/ui/widget.rs | 4 +- rust/client/src/ui/widget_container.rs | 4 +- rust/common/Cargo.toml | 2 +- rust/common/build.rs | 373 +++-- rust/common/src/dirs.rs | 13 + rust/common/src/model.rs | 242 ++- rust/common/src/rpc/backend_api.rs | 7 +- rust/common/src/rpc/frontend_api.rs | 7 +- rust/common/src/scenario_model.rs | 12 +- rust/common_plugin_runtime/Cargo.toml | 11 - .../src/backend_for_plugin_runtime_api.rs | 51 - rust/common_plugin_runtime/src/lib.rs | 2 - rust/common_plugin_runtime/src/model.rs | 33 - rust/plugin_runtime/Cargo.toml | 46 + rust/plugin_runtime/src/api.rs | 287 ++++ .../js => plugin_runtime/src}/assets.rs | 19 +- .../js => plugin_runtime/src}/clipboard.rs | 17 +- .../src}/command_generators.rs | 5 +- .../src}/component_model.rs | 0 rust/plugin_runtime/src/deno.rs | 451 ++++++ .../js => plugin_runtime/src}/environment.rs | 2 +- rust/plugin_runtime/src/events.rs | 113 ++ rust/plugin_runtime/src/lib.rs | 295 ++++ .../plugins/js => plugin_runtime/src}/logs.rs | 2 +- rust/plugin_runtime/src/model.rs | 204 +++ .../js => plugin_runtime/src}/permissions.rs | 45 +- rust/plugin_runtime/src/plugin_data.rs | 55 + .../src}/plugins/applications.rs | 4 +- .../src}/plugins/applications/linux.rs | 2 +- .../src}/plugins/applications/macos.rs | 2 +- .../js => plugin_runtime/src}/plugins/mod.rs | 0 .../src}/plugins/numbat.rs | 0 .../src}/plugins/settings.rs | 0 .../js => plugin_runtime/src}/preferences.rs | 18 +- .../js => plugin_runtime/src}/search.rs | 9 +- .../plugins/js => plugin_runtime/src}/ui.rs | 143 +- rust/server/Cargo.toml | 33 +- rust/server/src/lib.rs | 10 +- rust/server/src/model.rs | 76 - rust/server/src/plugins/clipboard.rs | 8 +- rust/server/src/plugins/data_db_repository.rs | 8 +- rust/server/src/plugins/image_gatherer.rs | 58 + rust/server/src/plugins/js.rs | 1094 ++++++++++++++ rust/server/src/plugins/js/mod.rs | 1331 ----------------- rust/server/src/plugins/loader.rs | 10 +- rust/server/src/plugins/mod.rs | 19 +- rust/server/src/plugins/runtime.rs | 0 rust/utils/Cargo.toml | 1 + rust/utils/src/channel.rs | 15 +- 54 files changed, 3247 insertions(+), 2050 deletions(-) delete mode 100644 rust/common_plugin_runtime/Cargo.toml delete mode 100644 rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs delete mode 100644 rust/common_plugin_runtime/src/lib.rs delete mode 100644 rust/common_plugin_runtime/src/model.rs create mode 100644 rust/plugin_runtime/Cargo.toml create mode 100644 rust/plugin_runtime/src/api.rs rename rust/{server/src/plugins/js => plugin_runtime/src}/assets.rs (68%) rename rust/{server/src/plugins/js => plugin_runtime/src}/clipboard.rs (78%) rename rust/{server/src/plugins/js => plugin_runtime/src}/command_generators.rs (65%) rename rust/{server/src/plugins/js => plugin_runtime/src}/component_model.rs (100%) create mode 100644 rust/plugin_runtime/src/deno.rs rename rust/{server/src/plugins/js => plugin_runtime/src}/environment.rs (95%) create mode 100644 rust/plugin_runtime/src/events.rs create mode 100644 rust/plugin_runtime/src/lib.rs rename rust/{server/src/plugins/js => plugin_runtime/src}/logs.rs (97%) create mode 100644 rust/plugin_runtime/src/model.rs rename rust/{server/src/plugins/js => plugin_runtime/src}/permissions.rs (86%) create mode 100644 rust/plugin_runtime/src/plugin_data.rs rename rust/{server/src/plugins/js => plugin_runtime/src}/plugins/applications.rs (97%) rename rust/{server/src/plugins/js => plugin_runtime/src}/plugins/applications/linux.rs (98%) rename rust/{server/src/plugins/js => plugin_runtime/src}/plugins/applications/macos.rs (98%) rename rust/{server/src/plugins/js => plugin_runtime/src}/plugins/mod.rs (100%) rename rust/{server/src/plugins/js => plugin_runtime/src}/plugins/numbat.rs (100%) rename rust/{server/src/plugins/js => plugin_runtime/src}/plugins/settings.rs (100%) rename rust/{server/src/plugins/js => plugin_runtime/src}/preferences.rs (74%) rename rust/{server/src/plugins/js => plugin_runtime/src}/search.rs (50%) rename rust/{server/src/plugins/js => plugin_runtime/src}/ui.rs (65%) create mode 100644 rust/server/src/plugins/image_gatherer.rs create mode 100644 rust/server/src/plugins/js.rs delete mode 100644 rust/server/src/plugins/js/mod.rs create mode 100644 rust/server/src/plugins/runtime.rs diff --git a/Cargo.lock b/Cargo.lock index 788c58e..446b5d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,9 +338,9 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" dependencies = [ "serde", ] @@ -873,6 +873,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" +dependencies = [ + "bincode_derive", + "serde", +] + +[[package]] +name = "bincode_derive" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c" +dependencies = [ + "virtue", +] + [[package]] name = "bindgen" version = "0.70.1" @@ -1626,6 +1645,7 @@ version = "0.0.0" dependencies = [ "anyhow", "base64 0.22.0", + "bincode 2.0.0-rc.3", "bytes", "component_model", "convert_case", @@ -1635,7 +1655,6 @@ dependencies = [ "itertools 0.10.5", "prost 0.12.4", "serde", - "serde-value", "serde_json", "thiserror", "tokio", @@ -1654,17 +1673,6 @@ dependencies = [ "iced_fonts", ] -[[package]] -name = "common_plugin_runtime" -version = "0.1.0" -dependencies = [ - "anyhow", - "bytes", - "common", - "serde", - "serde-value", -] - [[package]] name = "component_model" version = "0.0.0" @@ -2199,7 +2207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd2a54cda74cdc187d5fc2d23370a45cf09f912caf566dd1cd24a50157d809c7" dependencies = [ "anyhow", - "bincode", + "bincode 1.3.3", "bit-set 0.5.3", "bit-vec 0.6.3", "bytes", @@ -3215,6 +3223,12 @@ dependencies = [ "const-random", ] +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "document-features" version = "0.2.10" @@ -5624,6 +5638,21 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "interprocess" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "894148491d817cb36b6f778017b8ac46b17408d522dd90f539d677ea938362eb" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -5893,9 +5922,9 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" +checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" dependencies = [ "arrayvec", "smallvec", @@ -7888,6 +7917,43 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "plugin_runtime" +version = "0.1.0" +dependencies = [ + "anyhow", + "bincode 2.0.0-rc.3", + "bytes", + "cacao", + "common", + "component_model", + "deno_core", + "deno_runtime", + "freedesktop-icons", + "freedesktop_entry_parser", + "futures", + "icns", + "image 0.25.1", + "indexmap 2.2.6", + "interprocess", + "libc", + "numbat", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "plist", + "regex", + "resvg", + "serde", + "tokio", + "tokio-util", + "tracing", + "typed-path", + "utils", + "walkdir", +] + [[package]] name = "png" version = "0.16.8" @@ -8449,6 +8515,12 @@ dependencies = [ "font-types", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -8509,9 +8581,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -9286,34 +9358,21 @@ dependencies = [ "arboard", "async-stream", "bytes", - "cacao", "client", "common", - "common_plugin_runtime", - "component_model", - "deno_core", - "deno_runtime", - "freedesktop-icons", - "freedesktop_entry_parser", + "futures", "git2", - "icns", "image 0.25.1", "include_dir", "indexmap 2.2.6", + "interprocess", "itertools 0.10.5", - "libc", - "numbat", - "objc2", - "objc2-app-kit", - "objc2-foundation", "once_cell", "open", - "plist", + "plugin_runtime", "regex", - "resvg", "scenario_runner", "serde", - "serde-value", "sqlx", "tantivy", "tempfile", @@ -9326,6 +9385,7 @@ dependencies = [ "tracing-subscriber", "typed-path", "ureq", + "url", "utils", "uuid", "vergen-gitcl", @@ -10056,11 +10116,11 @@ checksum = "f83ba502a3265efb76efb89b0a2f7782ad6f2675015d4ce37e4b547dda42b499" [[package]] name = "svgtypes" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97ca9a891c9c70da8139ac9d8e8ea36a210fa21bb50eccd75d4a9561c83e87f" +checksum = "794de53cc48eaabeed0ab6a3404a65f40b3e38c067e4435883a65d2aa4ca000e" dependencies = [ - "kurbo 0.11.0", + "kurbo 0.11.1", "siphasher 1.0.1", ] @@ -11526,7 +11586,7 @@ dependencies = [ "data-url", "flate2", "imagesize", - "kurbo 0.11.0", + "kurbo 0.11.1", "log", "pico-args", "roxmltree", @@ -11572,6 +11632,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" name = "utils" version = "0.0.0" dependencies = [ + "log", "thiserror", "tokio", ] @@ -11717,6 +11778,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "virtue" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" + [[package]] name = "vsimd" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 8224617..24e5748 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,11 @@ members = [ "rust/server", "rust/common", "rust/common_ui", - "rust/common_plugin_runtime", "rust/utils", "rust/cli", "rust/component_model", "rust/scenario_runner", + "rust/plugin_runtime", ] [workspace.dependencies] #iced = { version = "0.13.99", features = ["tokio", "lazy", "advanced", "image"] } diff --git a/package-lock.json b/package-lock.json index 2d58a9e..5b6cd4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -150,6 +150,7 @@ } }, "js/bridge_build": { + "name": "@project-gauntlet/bridge-build", "version": "0.1.0", "devDependencies": { "@types/node": "^18.19.67", diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index 2dd6f51..89589d9 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -79,7 +79,7 @@ impl ClientContext { &mut self, render_location: UiRenderLocation, container: RootWidget, - images: HashMap, + images: HashMap>, plugin_id: &PluginId, plugin_name: &str, entrypoint_id: &EntrypointId, diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index b1e969e..318a943 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -420,7 +420,6 @@ fn new( let mut context = ClientContext::new(); let render_location = ui_render_location_from_scenario(render_location); - let container = RootWidget::deserialize(container).expect("should always be valid"); let has_children = container.content.is_some(); // ignore commands because screenshots are non-interactive @@ -942,6 +941,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { &mut state.global_state, match err { BackendForFrontendApiError::TimeoutError => ErrorViewData::BackendTimeout, + BackendForFrontendApiError::Internal { display } => ErrorViewData::UnknownError { display } } ) } @@ -2154,8 +2154,6 @@ async fn request_loop( render_location, top_level_view, container, - #[cfg(feature = "scenario_runner")] - container_value: _, images } => { let has_children = container.content.is_some(); diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index e5a93b0..28045f0 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -38,14 +38,14 @@ use std::fmt::{Debug, Display}; pub struct ComponentWidgets<'b> { root_widget: &'b mut Option, state: &'b mut HashMap, - images: &'b HashMap + images: &'b HashMap> } impl<'b> ComponentWidgets<'b> { pub fn new( root_widget: &'b mut Option, state: &'b mut HashMap, - images: &'b HashMap + images: &'b HashMap> ) -> ComponentWidgets<'b> { Self { root_widget, diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 555e0b6..9146fdd 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -14,7 +14,7 @@ use crate::ui::AppMsg; pub struct PluginWidgetContainer { root_widget: Arc>>, state: Arc>>, - images: HashMap, + images: HashMap>, plugin_id: Option, plugin_name: Option, entrypoint_id: Option, @@ -45,7 +45,7 @@ impl PluginWidgetContainer { pub fn replace_view( &mut self, container: RootWidget, - images: HashMap, + images: HashMap>, plugin_id: &PluginId, plugin_name: &str, entrypoint_id: &EntrypointId, diff --git a/rust/common/Cargo.toml b/rust/common/Cargo.toml index 87d2238..49c5b08 100644 --- a/rust/common/Cargo.toml +++ b/rust/common/Cargo.toml @@ -9,13 +9,13 @@ tonic = "0.11.0" prost = "0.12.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -serde-value = "0.7.0" tokio = "1.37.0" base64 = "0.22" utils = { path = "../utils" } bytes = "1.6.0" thiserror = "1" directories = "5.0" +bincode = { version = "2.0.0-rc.3", features = ["serde"] } [build-dependencies] tonic-build = "0.11.0" diff --git a/rust/common/build.rs b/rust/common/build.rs index 8c37c59..179c2bb 100644 --- a/rust/common/build.rs +++ b/rust/common/build.rs @@ -56,7 +56,7 @@ fn component_model_generator() -> Result<(), Box> { }; if !ordered_members.is_empty() { - output.push_str("#[derive(Debug)]\n"); + output.push_str("#[derive(Debug, Encode, Decode)]\n"); output.push_str(&format!("pub enum {}WidgetOrderedMembers {{\n", name)); let unique_component_refs = ordered_members @@ -74,7 +74,7 @@ fn component_model_generator() -> Result<(), Box> { if has_content { { - output.push_str("#[derive(Debug)]\n"); + output.push_str("#[derive(Debug, Encode, Decode)]\n"); output.push_str(&format!("pub struct {}WidgetContent {{\n", name)); for prop in props { @@ -119,12 +119,6 @@ fn component_model_generator() -> Result<(), Box> { } { - output.push_str(&format!("impl<'de> Deserialize<'de> for {}WidgetContent {{\n", name)); - output.push_str(&format!(" fn deserialize(deserializer: D) -> Result\n")); - output.push_str(&format!(" where\n")); - output.push_str(&format!(" D: Deserializer<'de>,\n")); - output.push_str(&format!(" {{\n")); - let unique_ordered_component_refs = ordered_members .iter() .map(|(_member_name, component_ref)| component_ref) @@ -190,154 +184,273 @@ fn component_model_generator() -> Result<(), Box> { component_refs.extend(prop_other_component_refs.values()); { - output.push_str(" #[derive(Debug, Deserialize)]\n"); - output.push_str(" #[serde(tag = \"__type__\")]\n"); - output.push_str(&format!(" enum {}WidgetMembers {{\n", name)); + output.push_str(&format!("impl<'de> Deserialize<'de> for {}WidgetContent {{\n", name)); + output.push_str(&format!(" fn deserialize(deserializer: D) -> Result\n")); + output.push_str(&format!(" where\n")); + output.push_str(&format!(" D: Deserializer<'de>,\n")); + output.push_str(&format!(" {{\n")); - for (_, prop_union_component_refs) in &prop_union_component_refs { - for prop_union_component_ref in prop_union_component_refs { - output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", prop_union_component_ref.component_internal_name)); - output.push_str(&format!(" {}({}Widget),\n", prop_union_component_ref.component_name, prop_union_component_ref.component_name)); + { + output.push_str(" #[derive(Debug, Deserialize)]\n"); + output.push_str(" #[serde(tag = \"__type__\")]\n"); + output.push_str(&format!(" enum {}WidgetMembersOwned {{\n", name)); + + for (_, prop_union_component_refs) in &prop_union_component_refs { + for prop_union_component_ref in prop_union_component_refs { + output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", prop_union_component_ref.component_internal_name)); + output.push_str(&format!(" {}({}Widget),\n", prop_union_component_ref.component_name, prop_union_component_ref.component_name)); + } } - } - for component_ref in &component_refs { - output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", component_ref.component_internal_name)); - output.push_str(&format!(" {}({}Widget),\n", component_ref.component_name, component_ref.component_name)); - } - - if has_text { - output.push_str(&format!(" #[serde(rename = \"gauntlet:text_part\")]\n")); - output.push_str(&format!(" Text {{\n")); - output.push_str(&format!(" value: String\n")); - output.push_str(&format!(" }},\n")); - } - - output.push_str(" }\n"); - } - - - output.push_str(&format!(" let mut members = Vec::<{}WidgetMembers>::deserialize(deserializer)?;\n", name)); - output.push_str("\n"); - - for (prop_name, _) in &prop_other_component_refs { - output.push_str(&format!(" let mut {}: Option<_> = None;\n", prop_name)); - } - - for (prop_name, _) in &prop_union_component_refs { - output.push_str(&format!(" let mut {}: Vec<_> = vec![];\n", prop_name)); - } - - for per_type_component_ref in &per_type_component_refs { - output.push_str(&format!(" let mut {}: Option<_> = None;\n", per_type_component_ref.component_internal_name)); - } - - if !ordered_members.is_empty() { - output.push_str(" let mut ordered_members = vec![];\n"); - } - - if has_text { - output.push_str(" let mut text = vec![];\n"); - } - - output.push_str("\n"); - - if has_content { - output.push_str(" while let Some(member) = members.pop() {\n"); - output.push_str(" match member {\n"); - - for (prop_name, prop_union_component_refs) in &prop_union_component_refs { - for (index, prop_union_component_ref) in prop_union_component_refs.iter().enumerate() { - output.push_str(&format!(" {}WidgetMembers::{}(widget) => {{\n", name, prop_union_component_ref.component_name)); - output.push_str(&format!(" {}.push({}{}::_{}(widget));\n", prop_name, name, prop_name.to_case(Case::Pascal), index)); - output.push_str(&format!(" }}\n")); + for component_ref in &component_refs { + output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", component_ref.component_internal_name)); + output.push_str(&format!(" {}({}Widget),\n", component_ref.component_name, component_ref.component_name)); } + + if has_text { + output.push_str(&format!(" #[serde(rename = \"gauntlet:text_part\")]\n")); + output.push_str(&format!(" Text {{\n")); + output.push_str(&format!(" value: String\n")); + output.push_str(&format!(" }},\n")); + } + + output.push_str(" }\n"); } - for (prop_name, prop_other_component_refs) in &prop_other_component_refs { - output.push_str(&format!(" {}WidgetMembers::{}(widget) => {{\n", name, prop_other_component_refs.component_name)); - output.push_str(&format!(" if let Some(_) = {} {{\n", prop_name)); - output.push_str(&format!(" return Err(Error::custom(\"Only one {} is allowed\"))\n", prop_other_component_refs.component_name)); - output.push_str(&format!(" }}\n")); - output.push_str(&format!(" {} = Some(widget);\n", prop_name)); - output.push_str(&format!(" }}\n")); + output.push_str(&format!(" let mut members = Vec::<{}WidgetMembersOwned>::deserialize(deserializer)?;\n", name)); + output.push_str("\n"); + + for (prop_name, _) in &prop_other_component_refs { + output.push_str(&format!(" let mut {}: Option<_> = None;\n", prop_name)); + } + + for (prop_name, _) in &prop_union_component_refs { + output.push_str(&format!(" let mut {}: Vec<_> = vec![];\n", prop_name)); } for per_type_component_ref in &per_type_component_refs { - output.push_str(&format!(" {}WidgetMembers::{}(widget) => {{\n", name, per_type_component_ref.component_name)); - output.push_str(&format!(" if let Some(_) = {} {{\n", per_type_component_ref.component_internal_name)); - output.push_str(&format!(" return Err(Error::custom(\"Only one {} is allowed\"))\n", per_type_component_ref.component_name)); - output.push_str(&format!(" }}\n")); - output.push_str(&format!(" {} = Some(widget);\n", per_type_component_ref.component_internal_name)); - output.push_str(&format!(" }}\n")); + output.push_str(&format!(" let mut {}: Option<_> = None;\n", per_type_component_ref.component_internal_name)); } - for ordered_component_ref in unique_ordered_component_refs { - output.push_str(&format!(" {}WidgetMembers::{}(widget) => {{\n", name, ordered_component_ref.component_name)); - output.push_str(&format!(" ordered_members.insert(0, {}WidgetOrderedMembers::{}(widget));\n", name, ordered_component_ref.component_name)); - output.push_str(&format!(" }}\n")); + if !ordered_members.is_empty() { + output.push_str(" let mut ordered_members = vec![];\n"); } if has_text { - output.push_str(&format!(" {}WidgetMembers::Text {{ value }} => {{\n", name)); - output.push_str(&format!(" text.insert(0, value);\n")); - output.push_str(&format!(" }}\n")); + output.push_str(" let mut text = vec![];\n"); } - output.push_str(" }\n"); - output.push_str(" }\n"); - } + output.push_str("\n"); - output.push_str("\n"); - output.push_str(&format!(" Ok({}WidgetContent {{\n", name)); + if has_content { + output.push_str(" while let Some(member) = members.pop() {\n"); + output.push_str(" match member {\n"); - for (prop_name, _) in &prop_union_component_refs { - output.push_str(&format!(" {},\n", prop_name)); - } - - for per_type_component_ref in per_type_component_refs { - match per_type_component_ref.arity { - Arity::ZeroOrOne => { - output.push_str(&format!(" {},\n", per_type_component_ref.component_internal_name)); + for (prop_name, prop_union_component_refs) in &prop_union_component_refs { + for (index, prop_union_component_ref) in prop_union_component_refs.iter().enumerate() { + output.push_str(&format!(" {}WidgetMembersOwned::{}(widget) => {{\n", name, prop_union_component_ref.component_name)); + output.push_str(&format!(" {}.push({}{}::_{}(widget));\n", prop_name, name, prop_name.to_case(Case::Pascal), index)); + output.push_str(&format!(" }}\n")); + } } - Arity::One => { - output.push_str(&format!(" {}: {}.ok_or(Error::custom(\"{} is required\"))?,\n", per_type_component_ref.component_internal_name, per_type_component_ref.component_internal_name, per_type_component_ref.component_name)); + + for (prop_name, prop_other_component_refs) in &prop_other_component_refs { + output.push_str(&format!(" {}WidgetMembersOwned::{}(widget) => {{\n", name, prop_other_component_refs.component_name)); + output.push_str(&format!(" if let Some(_) = {} {{\n", prop_name)); + output.push_str(&format!(" return Err(Error::custom(\"Only one {} is allowed\"))\n", prop_other_component_refs.component_name)); + output.push_str(&format!(" }}\n")); + output.push_str(&format!(" {} = Some(widget);\n", prop_name)); + output.push_str(&format!(" }}\n")); } - Arity::ZeroOrMore => { - todo!() + + for per_type_component_ref in &per_type_component_refs { + output.push_str(&format!(" {}WidgetMembersOwned::{}(widget) => {{\n", name, per_type_component_ref.component_name)); + output.push_str(&format!(" if let Some(_) = {} {{\n", per_type_component_ref.component_internal_name)); + output.push_str(&format!(" return Err(Error::custom(\"Only one {} is allowed\"))\n", per_type_component_ref.component_name)); + output.push_str(&format!(" }}\n")); + output.push_str(&format!(" {} = Some(widget);\n", per_type_component_ref.component_internal_name)); + output.push_str(&format!(" }}\n")); + } + + for ordered_component_ref in &unique_ordered_component_refs { + output.push_str(&format!(" {}WidgetMembersOwned::{}(widget) => {{\n", name, ordered_component_ref.component_name)); + output.push_str(&format!(" ordered_members.insert(0, {}WidgetOrderedMembers::{}(widget));\n", name, ordered_component_ref.component_name)); + output.push_str(&format!(" }}\n")); + } + + if has_text { + output.push_str(&format!(" {}WidgetMembersOwned::Text {{ value }} => {{\n", name)); + output.push_str(&format!(" text.insert(0, value);\n")); + output.push_str(&format!(" }}\n")); + } + + output.push_str(" }\n"); + output.push_str(" }\n"); + } + + output.push_str("\n"); + output.push_str(&format!(" Ok({}WidgetContent {{\n", name)); + + for (prop_name, _) in &prop_union_component_refs { + output.push_str(&format!(" {},\n", prop_name)); + } + + for per_type_component_ref in &per_type_component_refs { + match per_type_component_ref.arity { + Arity::ZeroOrOne => { + output.push_str(&format!(" {},\n", per_type_component_ref.component_internal_name)); + } + Arity::One => { + output.push_str(&format!(" {}: {}.ok_or(Error::custom(\"{} is required\"))?,\n", per_type_component_ref.component_internal_name, per_type_component_ref.component_internal_name, per_type_component_ref.component_name)); + } + Arity::ZeroOrMore => { + todo!() + } } } - } - for (prop_name, prop_other_component_ref) in &prop_other_component_refs { - match prop_other_component_ref.arity { - Arity::ZeroOrOne => { - output.push_str(&format!(" {},\n", prop_name)); - } - Arity::One => { - output.push_str(&format!(" {}: {}.ok_or(Error::custom(\"{} is required\"))?,\n", prop_name, prop_other_component_ref.component_internal_name, prop_other_component_ref.component_name)); - } - Arity::ZeroOrMore => { - todo!() + for (prop_name, prop_other_component_ref) in &prop_other_component_refs { + match prop_other_component_ref.arity { + Arity::ZeroOrOne => { + output.push_str(&format!(" {},\n", prop_name)); + } + Arity::One => { + output.push_str(&format!(" {}: {}.ok_or(Error::custom(\"{} is required\"))?,\n", prop_name, prop_other_component_ref.component_internal_name, prop_other_component_ref.component_name)); + } + Arity::ZeroOrMore => { + todo!() + } } } + + if !ordered_members.is_empty() { + output.push_str(" ordered_members\n"); + } + + if has_text { + output.push_str(" text\n"); + } + + output.push_str(&format!(" }})\n")); + output.push_str(&format!(" }}\n")); + output.push_str(&format!("}}\n")); } - if !ordered_members.is_empty() { - output.push_str(" ordered_members\n"); - } + { - if has_text { - output.push_str(" text\n"); - } + output.push_str(&format!("impl Serialize for {}WidgetContent {{\n", name)); + output.push_str(&format!(" fn serialize(&self, serializer: S) -> Result\n")); + output.push_str(&format!(" where\n")); + output.push_str(&format!(" S: Serializer\n")); + output.push_str(&format!(" {{\n")); - output.push_str(&format!(" }})\n")); - output.push_str(&format!(" }}\n")); - output.push_str(&format!("}}\n")); + { + output.push_str(" #[derive(Debug, Serialize)]\n"); + output.push_str(" #[serde(tag = \"__type__\")]\n"); + output.push_str(&format!(" enum {}WidgetMembersRef<'a> {{\n", name)); + + for (_, prop_union_component_refs) in &prop_union_component_refs { + for prop_union_component_ref in prop_union_component_refs { + output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", prop_union_component_ref.component_internal_name)); + output.push_str(&format!(" {}(&'a {}Widget),\n", prop_union_component_ref.component_name, prop_union_component_ref.component_name)); + } + } + + for component_ref in &component_refs { + output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", component_ref.component_internal_name)); + output.push_str(&format!(" {}(&'a {}Widget),\n", component_ref.component_name, component_ref.component_name)); + } + + if has_text { + output.push_str(&format!(" #[serde(rename = \"gauntlet:text_part\")]\n")); + output.push_str(&format!(" Text {{\n")); + output.push_str(&format!(" value: &'a String\n")); + output.push_str(&format!(" }},\n")); + } + + output.push_str(" }\n"); + } + + output.push_str(&format!(" let mut members = Vec::<{}WidgetMembersRef>::new();\n", name)); + output.push_str("\n"); + + + for (prop_name, prop_union_component_refs) in &prop_union_component_refs { + output.push_str(&format!(" for item in &self.{} {{\n", prop_name)); + output.push_str(&format!(" match item {{\n")); + + for (index, prop_union_component_ref) in prop_union_component_refs.iter().enumerate() { + output.push_str(&format!(" {}{}::_{}(widget) => {{\n", name, prop_name.to_case(Case::Pascal), index)); + output.push_str(&format!(" members.push({}WidgetMembersRef::{}(widget));\n", name, prop_union_component_ref.component_name)); + output.push_str(&format!(" }}\n")); + } + + output.push_str(&format!(" }}\n")); + output.push_str(&format!(" }}\n")); + } + + for (prop_name, prop_other_component_refs) in &prop_other_component_refs { + match prop_other_component_refs.arity { + Arity::ZeroOrOne => { + output.push_str(&format!(" if let Some({}) = &self.{} {{\n", prop_name, prop_name)); + output.push_str(&format!(" members.push({}WidgetMembersRef::{}({}))\n", name, prop_other_component_refs.component_name, prop_name)); + output.push_str(&format!(" }}\n")); + } + Arity::One => { + output.push_str(&format!(" members.push({}WidgetMembersRef::{}(&self.{}))\n", name, prop_other_component_refs.component_name, prop_name)); + } + Arity::ZeroOrMore => { + todo!() + } + } + } + + for per_type_component_ref in &per_type_component_refs { + match per_type_component_ref.arity { + Arity::ZeroOrOne => { + output.push_str(&format!(" if let Some(item) = &self.{} {{\n", per_type_component_ref.component_internal_name)); + output.push_str(&format!(" members.push({}WidgetMembersRef::{}(item))\n", name, per_type_component_ref.component_name)); + output.push_str(&format!(" }}\n")); + } + Arity::One => { + output.push_str(&format!(" members.push({}WidgetMembersRef::{}(&self.{}));\n", name, per_type_component_ref.component_name, per_type_component_ref.component_internal_name)); + } + Arity::ZeroOrMore => { + todo!() + } + } + } + + if !unique_ordered_component_refs.is_empty() { + output.push_str(&format!(" for member in &self.ordered_members {{\n")); + output.push_str(&format!(" match member {{\n")); + + for ordered_component_ref in &unique_ordered_component_refs { + output.push_str(&format!(" {}WidgetOrderedMembers::{}(widget) => {{\n", name, ordered_component_ref.component_name)); + output.push_str(&format!(" members.push({}WidgetMembersRef::{}(widget))\n", name, ordered_component_ref.component_name)); + output.push_str(&format!(" }}\n")); + } + + output.push_str(&format!(" }}\n")); + output.push_str(&format!(" }}\n")); + } + + if has_text { + output.push_str(&format!(" for value in &self.text {{\n")); + output.push_str(&format!(" members.push({}WidgetMembersRef::Text {{ value }});\n", name)); + output.push_str(&format!(" }}\n")); + } + + output.push_str("\n"); + output.push_str(&format!(" Vec::<{}WidgetMembersRef>::serialize(&members, serializer)\n", name)); + + output.push_str(&format!(" }}\n")); + output.push_str(&format!("}}\n")); + } } } - output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str("#[derive(Debug, Serialize, Deserialize, Encode, Decode)]\n"); output.push_str(&format!("pub struct {}Widget {{\n", name)); output.push_str(" #[serde(rename = \"__id__\")]\n"); output.push_str(" pub __id__: UiWidgetId,\n"); @@ -356,7 +469,7 @@ fn component_model_generator() -> Result<(), Box> { output.push_str("}\n"); let generate_union = |output: &mut String, items: &Vec, prop_name: &String| { - output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str("#[derive(Debug, Encode, Decode)]\n"); output.push_str(&format!("pub enum {}{} {{\n", name, prop_name.to_case(Case::Pascal))); for (index, property_type) in items.iter().enumerate() { @@ -388,7 +501,7 @@ fn component_model_generator() -> Result<(), Box> { for (type_name, shared_type) in shared_types { match shared_type { SharedType::Enum { items } => { - output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str("#[derive(Debug, Serialize, Deserialize, Encode, Decode)]\n"); output.push_str(&format!("pub enum {} {{\n", type_name)); for item in items { @@ -400,7 +513,7 @@ fn component_model_generator() -> Result<(), Box> { output.push_str("\n"); } SharedType::Object { items } => { - output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str("#[derive(Debug, Serialize, Deserialize, Encode, Decode)]\n"); output.push_str(&format!("pub struct {} {{\n", type_name)); for (property_name, property_type) in items { @@ -411,7 +524,7 @@ fn component_model_generator() -> Result<(), Box> { output.push_str("\n"); } SharedType::Union { items } => { - output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str("#[derive(Debug, Serialize, Deserialize, Encode, Decode)]\n"); output.push_str("#[serde(untagged)]\n"); output.push_str(&format!("pub enum {} {{\n", type_name)); @@ -432,7 +545,7 @@ fn component_model_generator() -> Result<(), Box> { } } - output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str("#[derive(Debug, Serialize, Deserialize, Encode, Decode)]\n"); output.push_str("#[serde(tag = \"__type__\")]\n"); output.push_str("pub enum RootWidgetMembers {\n"); @@ -443,7 +556,7 @@ fn component_model_generator() -> Result<(), Box> { output.push_str("}\n"); - output.push_str("#[derive(Debug, Deserialize)]\n"); + output.push_str("#[derive(Debug, Serialize, Deserialize, Encode, Decode)]\n"); output.push_str("pub struct RootWidget {\n"); output.push_str(" #[serde(default, deserialize_with = \"array_to_option\")]\n"); output.push_str(" pub content: Option\n"); diff --git a/rust/common/src/dirs.rs b/rust/common/src/dirs.rs index c17941d..51c15fe 100644 --- a/rust/common/src/dirs.rs +++ b/rust/common/src/dirs.rs @@ -123,4 +123,17 @@ impl Dirs { state_dir } + + pub fn plugin_uds_socket(&self, plugin_uuid: &str) -> PathBuf { + let state_dir = if cfg!(feature = "release") || cfg!(feature = "scenario_runner") { + self.inner.runtime_dir() + .unwrap_or_else(|| Path::new("/tmp")) + .to_path_buf() + } else { + Path::new("/tmp").to_owned() + }; + + state_dir.join(format!("project-gauntlet-{}.sock", plugin_uuid)) + } + } \ No newline at end of file diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 4216c84..18fa0da 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -3,12 +3,13 @@ use std::path::PathBuf; use std::sync::Arc; use anyhow::anyhow; +use bincode::{Decode, Encode}; use gix_url::Scheme; use gix_url::Url; -use serde::{Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::Error; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Encode, Decode)] pub struct PluginId(Arc); impl PluginId { @@ -58,7 +59,7 @@ impl ToString for PluginId { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Encode, Decode)] pub struct EntrypointId(Arc); impl EntrypointId { @@ -147,9 +148,7 @@ pub enum UiRequestData { render_location: UiRenderLocation, top_level_view: bool, container: RootWidget, - #[cfg(feature = "scenario_runner")] - container_value: serde_value::Value, - images: HashMap, + images: HashMap>, }, ShowPreferenceRequiredView { plugin_id: PluginId, @@ -265,250 +264,247 @@ include!(concat!(env!("OUT_DIR"), "/components.rs")); // TODO generate this +#[allow(async_fn_in_trait)] pub trait WidgetVisitor { - fn action_widget(&mut self, _widget: &ActionWidget) { + async fn action_widget(&mut self, _widget: &ActionWidget) { } - fn action_panel_section_widget(&mut self, widget: &ActionPanelSectionWidget) { + async fn action_panel_section_widget(&mut self, widget: &ActionPanelSectionWidget) { for members in &widget.content.ordered_members { match members { - ActionPanelSectionWidgetOrderedMembers::Action(widget) => self.action_widget(widget) + ActionPanelSectionWidgetOrderedMembers::Action(widget) => self.action_widget(widget).await } } } - fn action_panel_widget(&mut self, widget: &ActionPanelWidget) { + async fn action_panel_widget(&mut self, widget: &ActionPanelWidget) { for members in &widget.content.ordered_members { match members { - ActionPanelWidgetOrderedMembers::Action(widget) => self.action_widget(widget), - ActionPanelWidgetOrderedMembers::ActionPanelSection(widget) => self.action_panel_section_widget(widget) + ActionPanelWidgetOrderedMembers::Action(widget) => self.action_widget(widget).await, + ActionPanelWidgetOrderedMembers::ActionPanelSection(widget) => self.action_panel_section_widget(widget).await } } } - fn metadata_link_widget(&mut self, _widget: &MetadataLinkWidget) {} - fn metadata_tag_item_widget(&mut self, _widget: &MetadataTagItemWidget) {} - fn metadata_tag_list_widget(&mut self, widget: &MetadataTagListWidget) { + async fn metadata_link_widget(&mut self, _widget: &MetadataLinkWidget) {} + async fn metadata_tag_item_widget(&mut self, _widget: &MetadataTagItemWidget) {} + async fn metadata_tag_list_widget(&mut self, widget: &MetadataTagListWidget) { for members in &widget.content.ordered_members { match members { - MetadataTagListWidgetOrderedMembers::MetadataTagItem(widget) => self.metadata_tag_item_widget(widget) + MetadataTagListWidgetOrderedMembers::MetadataTagItem(widget) => self.metadata_tag_item_widget(widget).await } } } - fn metadata_separator_widget(&mut self, _widget: &MetadataSeparatorWidget) {} - fn metadata_value_widget(&mut self, _widget: &MetadataValueWidget) {} - fn metadata_icon_widget(&mut self, _widget: &MetadataIconWidget) {} - fn metadata_widget(&mut self, widget: &MetadataWidget) { + async fn metadata_separator_widget(&mut self, _widget: &MetadataSeparatorWidget) {} + async fn metadata_value_widget(&mut self, _widget: &MetadataValueWidget) {} + async fn metadata_icon_widget(&mut self, _widget: &MetadataIconWidget) {} + async fn metadata_widget(&mut self, widget: &MetadataWidget) { for members in &widget.content.ordered_members { match members { - MetadataWidgetOrderedMembers::MetadataTagList(widget) => self.metadata_tag_list_widget(widget), - MetadataWidgetOrderedMembers::MetadataLink(widget) => self.metadata_link_widget(widget), - MetadataWidgetOrderedMembers::MetadataValue(widget) => self.metadata_value_widget(widget), - MetadataWidgetOrderedMembers::MetadataIcon(widget) => self.metadata_icon_widget(widget), - MetadataWidgetOrderedMembers::MetadataSeparator(widget) => self.metadata_separator_widget(widget), + MetadataWidgetOrderedMembers::MetadataTagList(widget) => self.metadata_tag_list_widget(widget).await, + MetadataWidgetOrderedMembers::MetadataLink(widget) => self.metadata_link_widget(widget).await, + MetadataWidgetOrderedMembers::MetadataValue(widget) => self.metadata_value_widget(widget).await, + MetadataWidgetOrderedMembers::MetadataIcon(widget) => self.metadata_icon_widget(widget).await, + MetadataWidgetOrderedMembers::MetadataSeparator(widget) => self.metadata_separator_widget(widget).await, } } } - fn image(&mut self, _widget_id: UiWidgetId, _widget: &Image) { + async fn image(&mut self, _widget_id: UiWidgetId, _widget: &Image) { } - fn image_widget(&mut self, widget: &ImageWidget) { - self.image(widget.__id__, &widget.source) + async fn image_widget(&mut self, widget: &ImageWidget) { + self.image(widget.__id__, &widget.source).await } - fn h1_widget(&mut self, _widget: &H1Widget) {} - fn h2_widget(&mut self, _widget: &H2Widget) {} - fn h3_widget(&mut self, _widget: &H3Widget) {} - fn h4_widget(&mut self, _widget: &H4Widget) {} - fn h5_widget(&mut self, _widget: &H5Widget) {} - fn h6_widget(&mut self, _widget: &H6Widget) {} - fn horizontal_break_widget(&mut self, _widget: &HorizontalBreakWidget) {} - fn code_block_widget(&mut self, _widget: &CodeBlockWidget) {} - fn paragraph_widget(&mut self, _widget: &ParagraphWidget) { - } - fn content_widget(&mut self, widget: &ContentWidget) { + async fn h1_widget(&mut self, _widget: &H1Widget) {} + async fn h2_widget(&mut self, _widget: &H2Widget) {} + async fn h3_widget(&mut self, _widget: &H3Widget) {} + async fn h4_widget(&mut self, _widget: &H4Widget) {} + async fn h5_widget(&mut self, _widget: &H5Widget) {} + async fn h6_widget(&mut self, _widget: &H6Widget) {} + async fn horizontal_break_widget(&mut self, _widget: &HorizontalBreakWidget) {} + async fn code_block_widget(&mut self, _widget: &CodeBlockWidget) {} + async fn paragraph_widget(&mut self, _widget: &ParagraphWidget) {} + async fn content_widget(&mut self, widget: &ContentWidget) { for members in &widget.content.ordered_members { match members { - ContentWidgetOrderedMembers::Paragraph(widget) => self.paragraph_widget(widget), - ContentWidgetOrderedMembers::Image(widget) => self.image_widget(widget), - ContentWidgetOrderedMembers::H1(widget) => self.h1_widget(widget), - ContentWidgetOrderedMembers::H2(widget) => self.h2_widget(widget), - ContentWidgetOrderedMembers::H3(widget) => self.h3_widget(widget), - ContentWidgetOrderedMembers::H4(widget) => self.h4_widget(widget), - ContentWidgetOrderedMembers::H5(widget) => self.h5_widget(widget), - ContentWidgetOrderedMembers::H6(widget) => self.h6_widget(widget), - ContentWidgetOrderedMembers::HorizontalBreak(widget) => self.horizontal_break_widget(widget), - ContentWidgetOrderedMembers::CodeBlock(widget) => self.code_block_widget(widget), + ContentWidgetOrderedMembers::Paragraph(widget) => self.paragraph_widget(widget).await, + ContentWidgetOrderedMembers::Image(widget) => self.image_widget(widget).await, + ContentWidgetOrderedMembers::H1(widget) => self.h1_widget(widget).await, + ContentWidgetOrderedMembers::H2(widget) => self.h2_widget(widget).await, + ContentWidgetOrderedMembers::H3(widget) => self.h3_widget(widget).await, + ContentWidgetOrderedMembers::H4(widget) => self.h4_widget(widget).await, + ContentWidgetOrderedMembers::H5(widget) => self.h5_widget(widget).await, + ContentWidgetOrderedMembers::H6(widget) => self.h6_widget(widget).await, + ContentWidgetOrderedMembers::HorizontalBreak(widget) => self.horizontal_break_widget(widget).await, + ContentWidgetOrderedMembers::CodeBlock(widget) => self.code_block_widget(widget).await, } } } - fn detail_widget(&mut self, widget: &DetailWidget) { + async fn detail_widget(&mut self, widget: &DetailWidget) { if let Some(widget) = &widget.content.actions { - self.action_panel_widget(widget) + self.action_panel_widget(widget).await } if let Some(widget) = &widget.content.metadata { - self.metadata_widget(widget) + self.metadata_widget(widget).await } if let Some(widget) = &widget.content.content { - self.content_widget(widget) + self.content_widget(widget).await } } - fn text_field_widget(&mut self, _widget: &TextFieldWidget) {} - fn password_field_widget(&mut self, _widget: &PasswordFieldWidget) {} - fn checkbox_widget(&mut self, _widget: &CheckboxWidget) {} - fn date_picker_widget(&mut self, _widget: &DatePickerWidget) {} - fn select_item_widget(&mut self, _widget: &SelectItemWidget) { - } - fn select_widget(&mut self, widget: &SelectWidget) { + async fn text_field_widget(&mut self, _widget: &TextFieldWidget) {} + async fn password_field_widget(&mut self, _widget: &PasswordFieldWidget) {} + async fn checkbox_widget(&mut self, _widget: &CheckboxWidget) {} + async fn date_picker_widget(&mut self, _widget: &DatePickerWidget) {} + async fn select_item_widget(&mut self, _widget: &SelectItemWidget) {} + async fn select_widget(&mut self, widget: &SelectWidget) { for members in &widget.content.ordered_members { match members { - SelectWidgetOrderedMembers::SelectItem(widget) => self.select_item_widget(widget) + SelectWidgetOrderedMembers::SelectItem(widget) => self.select_item_widget(widget).await } } } - fn separator_widget(&mut self, _widget: &SeparatorWidget) { - } - fn form_widget(&mut self, widget: &FormWidget) { + async fn separator_widget(&mut self, _widget: &SeparatorWidget) {} + async fn form_widget(&mut self, widget: &FormWidget) { if let Some(widget) = &widget.content.actions { - self.action_panel_widget(widget) + self.action_panel_widget(widget).await } for members in &widget.content.ordered_members { match members { - FormWidgetOrderedMembers::TextField(widget) => self.text_field_widget(widget), - FormWidgetOrderedMembers::PasswordField(widget) => self.password_field_widget(widget), - FormWidgetOrderedMembers::Checkbox(widget) => self.checkbox_widget(widget), - FormWidgetOrderedMembers::DatePicker(widget) => self.date_picker_widget(widget), - FormWidgetOrderedMembers::Select(widget) => self.select_widget(widget), - FormWidgetOrderedMembers::Separator(widget) => self.separator_widget(widget), + FormWidgetOrderedMembers::TextField(widget) => self.text_field_widget(widget).await, + FormWidgetOrderedMembers::PasswordField(widget) => self.password_field_widget(widget).await, + FormWidgetOrderedMembers::Checkbox(widget) => self.checkbox_widget(widget).await, + FormWidgetOrderedMembers::DatePicker(widget) => self.date_picker_widget(widget).await, + FormWidgetOrderedMembers::Select(widget) => self.select_widget(widget).await, + FormWidgetOrderedMembers::Separator(widget) => self.separator_widget(widget).await, } } } - fn inline_separator_widget(&mut self, _widget: &InlineSeparatorWidget) { - } + async fn inline_separator_widget(&mut self, _widget: &InlineSeparatorWidget) {} - fn inline_widget(&mut self, widget: &InlineWidget) { + async fn inline_widget(&mut self, widget: &InlineWidget) { if let Some(widget) = &widget.content.actions { - self.action_panel_widget(widget) + self.action_panel_widget(widget).await } for members in &widget.content.ordered_members { match members { - InlineWidgetOrderedMembers::Content(widget) => self.content_widget(widget), - InlineWidgetOrderedMembers::InlineSeparator(widget) => self.inline_separator_widget(widget) + InlineWidgetOrderedMembers::Content(widget) => self.content_widget(widget).await, + InlineWidgetOrderedMembers::InlineSeparator(widget) => self.inline_separator_widget(widget).await } } } - fn empty_view_widget(&mut self, widget: &EmptyViewWidget) { + async fn empty_view_widget(&mut self, widget: &EmptyViewWidget) { if let Some(image) = &widget.image { - self.image(widget.__id__, image) + self.image(widget.__id__, image).await } } - fn icon_accessory_widget(&mut self, widget: &IconAccessoryWidget) { - self.image(widget.__id__, &widget.icon) + async fn icon_accessory_widget(&mut self, widget: &IconAccessoryWidget) { + self.image(widget.__id__, &widget.icon).await } - fn text_accessory_widget(&mut self, widget: &TextAccessoryWidget) { + async fn text_accessory_widget(&mut self, widget: &TextAccessoryWidget) { if let Some(image) = &widget.icon { - self.image(widget.__id__, image) + self.image(widget.__id__, image).await } } - fn search_bar_widget(&mut self, _widget: &SearchBarWidget) {} + async fn search_bar_widget(&mut self, _widget: &SearchBarWidget) {} - fn list_item_widget(&mut self, widget: &ListItemWidget) { + async fn list_item_widget(&mut self, widget: &ListItemWidget) { if let Some(image) = &widget.icon { - self.image(widget.__id__, image) + self.image(widget.__id__, image).await } for accessories in &widget.content.accessories { match accessories { - ListItemAccessories::_0(widget) => self.text_accessory_widget(widget), - ListItemAccessories::_1(widget) => self.icon_accessory_widget(widget) + ListItemAccessories::_0(widget) => self.text_accessory_widget(widget).await, + ListItemAccessories::_1(widget) => self.icon_accessory_widget(widget).await } } } - fn list_section_widget(&mut self, widget: &ListSectionWidget) { + async fn list_section_widget(&mut self, widget: &ListSectionWidget) { for members in &widget.content.ordered_members { match members { - ListSectionWidgetOrderedMembers::ListItem(widget) => self.list_item_widget(widget) + ListSectionWidgetOrderedMembers::ListItem(widget) => self.list_item_widget(widget).await } } } - fn list_widget(&mut self, widget: &ListWidget) { + async fn list_widget(&mut self, widget: &ListWidget) { if let Some(widget) = &widget.content.actions { - self.action_panel_widget(widget) + self.action_panel_widget(widget).await } if let Some(widget) = &widget.content.search_bar { - self.search_bar_widget(widget) + self.search_bar_widget(widget).await } if let Some(widget) = &widget.content.empty_view { - self.empty_view_widget(widget) + self.empty_view_widget(widget).await } if let Some(widget) = &widget.content.detail { - self.detail_widget(widget) + self.detail_widget(widget).await } for members in &widget.content.ordered_members { match members { - ListWidgetOrderedMembers::ListItem(widget) => self.list_item_widget(widget), - ListWidgetOrderedMembers::ListSection(widget) => self.list_section_widget(widget), + ListWidgetOrderedMembers::ListItem(widget) => self.list_item_widget(widget).await, + ListWidgetOrderedMembers::ListSection(widget) => self.list_section_widget(widget).await, } } } - fn grid_item_widget(&mut self, widget: &GridItemWidget) { + async fn grid_item_widget(&mut self, widget: &GridItemWidget) { if let Some(widget) = &widget.content.accessory { - self.icon_accessory_widget(widget) + self.icon_accessory_widget(widget).await } for members in &widget.content.content.content.ordered_members { match members { - ContentWidgetOrderedMembers::Paragraph(widget) => self.paragraph_widget(widget), - ContentWidgetOrderedMembers::Image(widget) => self.image_widget(widget), - ContentWidgetOrderedMembers::H1(widget) => self.h1_widget(widget), - ContentWidgetOrderedMembers::H2(widget) => self.h2_widget(widget), - ContentWidgetOrderedMembers::H3(widget) => self.h3_widget(widget), - ContentWidgetOrderedMembers::H4(widget) => self.h4_widget(widget), - ContentWidgetOrderedMembers::H5(widget) => self.h5_widget(widget), - ContentWidgetOrderedMembers::H6(widget) => self.h6_widget(widget), - ContentWidgetOrderedMembers::HorizontalBreak(widget) => self.horizontal_break_widget(widget), - ContentWidgetOrderedMembers::CodeBlock(widget) => self.code_block_widget(widget), + ContentWidgetOrderedMembers::Paragraph(widget) => self.paragraph_widget(widget).await, + ContentWidgetOrderedMembers::Image(widget) => self.image_widget(widget).await, + ContentWidgetOrderedMembers::H1(widget) => self.h1_widget(widget).await, + ContentWidgetOrderedMembers::H2(widget) => self.h2_widget(widget).await, + ContentWidgetOrderedMembers::H3(widget) => self.h3_widget(widget).await, + ContentWidgetOrderedMembers::H4(widget) => self.h4_widget(widget).await, + ContentWidgetOrderedMembers::H5(widget) => self.h5_widget(widget).await, + ContentWidgetOrderedMembers::H6(widget) => self.h6_widget(widget).await, + ContentWidgetOrderedMembers::HorizontalBreak(widget) => self.horizontal_break_widget(widget).await, + ContentWidgetOrderedMembers::CodeBlock(widget) => self.code_block_widget(widget).await, } } } - fn grid_section_widget(&mut self, widget: &GridSectionWidget) { + async fn grid_section_widget(&mut self, widget: &GridSectionWidget) { for members in &widget.content.ordered_members { match members { - GridSectionWidgetOrderedMembers::GridItem(widget) => self.grid_item_widget(widget) + GridSectionWidgetOrderedMembers::GridItem(widget) => self.grid_item_widget(widget).await } } } - fn grid_widget(&mut self, widget: &GridWidget) { + async fn grid_widget(&mut self, widget: &GridWidget) { if let Some(widget) = &widget.content.actions { - self.action_panel_widget(widget) + self.action_panel_widget(widget).await } if let Some(widget) = &widget.content.search_bar { - self.search_bar_widget(widget) + self.search_bar_widget(widget).await } if let Some(widget) = &widget.content.empty_view { - self.empty_view_widget(widget) + self.empty_view_widget(widget).await } for members in &widget.content.ordered_members { match members { - GridWidgetOrderedMembers::GridItem(widget) => self.grid_item_widget(widget), - GridWidgetOrderedMembers::GridSection(widget) => self.grid_section_widget(widget) + GridWidgetOrderedMembers::GridItem(widget) => self.grid_item_widget(widget).await, + GridWidgetOrderedMembers::GridSection(widget) => self.grid_section_widget(widget).await } } } - fn root_widget(&mut self, root_widget: &RootWidget) { + async fn root_widget(&mut self, root_widget: &RootWidget) { if let Some(members) = &root_widget.content { match members { - RootWidgetMembers::Detail(widget) => self.detail_widget(widget), - RootWidgetMembers::Form(widget) => self.form_widget(widget), - RootWidgetMembers::Inline(widget) => self.inline_widget(widget), - RootWidgetMembers::List(widget) => self.list_widget(widget), - RootWidgetMembers::Grid(widget) => self.grid_widget(widget), + RootWidgetMembers::Detail(widget) => self.detail_widget(widget).await, + RootWidgetMembers::Form(widget) => self.form_widget(widget).await, + RootWidgetMembers::Inline(widget) => self.inline_widget(widget).await, + RootWidgetMembers::List(widget) => self.list_widget(widget).await, + RootWidgetMembers::Grid(widget) => self.grid_widget(widget).await, } } } diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index 9321b5b..c3d980f 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; - +use anyhow::anyhow; use thiserror::Error; use tonic::{Code, Request}; use tonic::transport::Channel; @@ -15,12 +15,17 @@ use crate::rpc::grpc_convert::{plugin_preference_from_rpc, plugin_preference_use pub enum BackendForFrontendApiError { #[error("Frontend wasn't able to process request in a timely manner")] TimeoutError, + #[error("Internal Error: {display:?}")] + Internal { + display: String + }, } impl From for BackendForFrontendApiError { fn from(error: RequestError) -> BackendForFrontendApiError { match error { RequestError::TimeoutError => BackendForFrontendApiError::TimeoutError, + RequestError::OtherSideWasDropped => BackendForFrontendApiError::Internal { display: "other side was dropped".to_string() } } } } diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index 2aa901a..83d07eb 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -17,6 +17,7 @@ impl From for FrontendApiError { fn from(error: RequestError) -> FrontendApiError { match error { RequestError::TimeoutError => FrontendApiError::TimeoutError, + RequestError::OtherSideWasDropped => FrontendApiError::OtherError(anyhow!("other side was dropped")) } } } @@ -48,9 +49,7 @@ impl FrontendApi { render_location: UiRenderLocation, top_level_view: bool, container: RootWidget, - #[cfg(feature = "scenario_runner")] - container_value: serde_value::Value, - images: HashMap, + images: HashMap>, ) -> Result<(), FrontendApiError> { let request = UiRequestData::ReplaceView { plugin_id, @@ -60,8 +59,6 @@ impl FrontendApi { render_location, top_level_view, container, - #[cfg(feature = "scenario_runner")] - container_value, images, }; diff --git a/rust/common/src/scenario_model.rs b/rust/common/src/scenario_model.rs index 899a2ff..aa67d3d 100644 --- a/rust/common/src/scenario_model.rs +++ b/rust/common/src/scenario_model.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use crate::model::UiWidgetId; +use crate::model::{RootWidget, UiWidgetId}; #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] pub enum ScenarioUiRenderLocation { @@ -15,9 +15,9 @@ pub enum ScenarioFrontendEvent { entrypoint_id: String, render_location: ScenarioUiRenderLocation, top_level_view: bool, - container: serde_value::Value, + container: RootWidget, #[serde(with="base64")] - images: HashMap, + images: HashMap>, }, ShowPreferenceRequiredView { entrypoint_id: String, @@ -39,7 +39,7 @@ mod base64 { use base64::engine::general_purpose::STANDARD; use crate::model::UiWidgetId; - pub fn serialize(v: &HashMap, s: S) -> Result { + pub fn serialize(v: &HashMap>, s: S) -> Result { let map = v.iter() .map(|(key, value)| (key.to_string(), STANDARD.encode(value))) .collect(); @@ -47,13 +47,13 @@ mod base64 { HashMap::::serialize(&map, s) } - pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result>, D::Error> { HashMap::::deserialize(d)? .into_iter() .map(|(key, value)| { STANDARD.decode(value.as_bytes()) .map_err(|e| serde::de::Error::custom(e)) - .map(|vec| (UiWidgetId::from_str(&key).expect("should not fail"), bytes::Bytes::from(vec))) + .map(|vec| (UiWidgetId::from_str(&key).expect("should not fail"), vec)) }) .collect() diff --git a/rust/common_plugin_runtime/Cargo.toml b/rust/common_plugin_runtime/Cargo.toml deleted file mode 100644 index dfdc0c2..0000000 --- a/rust/common_plugin_runtime/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "common_plugin_runtime" -version = "0.1.0" -edition = "2021" - -[dependencies] -serde = { version = "1.0", features = ["derive"] } -anyhow = "1.0.94" -common = { path = "../common" } -bytes = "1.6.0" -serde-value = "0.7.0" diff --git a/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs b/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs deleted file mode 100644 index dca1a8b..0000000 --- a/rust/common_plugin_runtime/src/backend_for_plugin_runtime_api.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::model::{AdditionalSearchItem, ClipboardData, PreferenceUserData}; -use common::model::{EntrypointId, RootWidget, UiRenderLocation, UiWidgetId}; -use std::collections::HashMap; - -pub trait BackendForPluginRuntimeApi { - async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> ; - async fn get_asset_data(&self, path: &str) -> anyhow::Result>; - async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result>; - async fn get_plugin_preferences(&self) -> anyhow::Result>; - async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result>; - async fn plugin_preferences_required(&self) -> anyhow::Result; - async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result; - async fn clipboard_read(&self) -> anyhow::Result; - async fn clipboard_read_text(&self) -> anyhow::Result>; - async fn clipboard_write(&self, data: ClipboardData) -> anyhow::Result<()>; - async fn clipboard_write_text(&self, data: String) -> anyhow::Result<()>; - async fn clipboard_clear(&self) -> anyhow::Result<()>; - async fn ui_update_loading_bar(&self, entrypoint_id: EntrypointId, show: bool) -> anyhow::Result<()>; - async fn ui_show_hud(&self, display: String) -> anyhow::Result<()>; - async fn ui_get_action_id_for_shortcut( - &self, - entrypoint_id: EntrypointId, - key: String, - modifier_shift: bool, - modifier_control: bool, - modifier_alt: bool, - modifier_meta: bool - ) -> anyhow::Result>; - async fn ui_render( - &self, - entrypoint_id: EntrypointId, - render_location: UiRenderLocation, - top_level_view: bool, - container: RootWidget, - #[cfg(feature = "scenario_runner")] - container_value: serde_value::Value, - images: HashMap, - ) -> anyhow::Result<()>; - async fn ui_show_plugin_error_view( - &self, - entrypoint_id: EntrypointId, - render_location: UiRenderLocation - ) -> anyhow::Result<()>; - async fn ui_show_preferences_required_view( - &self, - entrypoint_id: EntrypointId, - plugin_preferences_required: bool, - entrypoint_preferences_required: bool - ) -> anyhow::Result<()>; - async fn ui_clear_inline_view(&self) -> anyhow::Result<()>; -} \ No newline at end of file diff --git a/rust/common_plugin_runtime/src/lib.rs b/rust/common_plugin_runtime/src/lib.rs deleted file mode 100644 index 1d557a6..0000000 --- a/rust/common_plugin_runtime/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod backend_for_plugin_runtime_api; -pub mod model; diff --git a/rust/common_plugin_runtime/src/model.rs b/rust/common_plugin_runtime/src/model.rs deleted file mode 100644 index 5d7c14f..0000000 --- a/rust/common_plugin_runtime/src/model.rs +++ /dev/null @@ -1,33 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize)] -pub struct AdditionalSearchItem { - pub entrypoint_name: String, - pub generator_entrypoint_id: String, - pub entrypoint_id: String, - pub entrypoint_uuid: String, - pub entrypoint_icon: Option>, - pub entrypoint_actions: Vec, -} - -#[derive(Debug, Deserialize)] -pub struct AdditionalSearchItemAction { - pub id: Option, - pub label: String, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub enum PreferenceUserData { - Number(f64), - String(String), - Bool(bool), - ListOfStrings(Vec), - ListOfNumbers(Vec), -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ClipboardData { - pub text_data: Option, - pub png_data: Option> -} \ No newline at end of file diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml new file mode 100644 index 0000000..c1b7429 --- /dev/null +++ b/rust/plugin_runtime/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "plugin_runtime" +version = "0.1.0" +edition = "2021" + +[dependencies] +deno_core = { version = "0.321.0" } # deno 2.1.1 +deno_runtime = { version = "0.188.0" } +tokio = "1.28.1" +anyhow = { version = "1", features = ["backtrace"] } +regex = "1.9.3" +once_cell = "1.18.0" +serde = { version = "1.0", features = ["derive"] } +tracing = "0.1" +common = { path = "../common" } +typed-path = "0.9" +indexmap = { version = "2.1.0", features = ["serde"] } +component_model = { path = "../component_model" } +bytes = "1.6.0" +image = "0.25" +resvg = { version = "0.41", default-features = false} +walkdir = "2.4.0" +numbat = "1.14.0" +interprocess = { version = "2.2.2", features = ["tokio"] } +tokio-util = "0.7.11" +bincode = "2.0.0-rc.3" +utils = { path = "../utils" } +futures = "0.3.31" + +[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] +libc = "0.2.153" + +[target.'cfg(target_os = "linux")'.dependencies] +freedesktop_entry_parser = "1.3" +freedesktop-icons = "0.2" + +[target.'cfg(target_os = "macos")'.dependencies] +cacao = "0.3.2" +plist = "1.6.1" +icns = "0.3.1" +objc2-app-kit = { version = "0.2.2", features = ["NSWorkspace", "NSImage", "NSImageRep", "NSBitmapImageRep", "NSGraphics", "NSGraphicsContext"] } +objc2-foundation = { version = "0.2.2", features = ["NSString"] } +objc2 = "0.5.2" + +[features] +scenario_runner = [] diff --git a/rust/plugin_runtime/src/api.rs b/rust/plugin_runtime/src/api.rs new file mode 100644 index 0000000..235bd95 --- /dev/null +++ b/rust/plugin_runtime/src/api.rs @@ -0,0 +1,287 @@ +use crate::model::{JsAdditionalSearchItem, JsClipboardData, JsPreferenceUserData}; +use crate::{JsRequest, JsResponse, JsUiRenderLocation}; +use common::model::{EntrypointId, RootWidget, UiRenderLocation}; +use std::collections::HashMap; +use anyhow::anyhow; +use utils::channel::RequestSender; + +#[allow(async_fn_in_trait)] +pub trait BackendForPluginRuntimeApi { + async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> ; + async fn get_asset_data(&self, path: &str) -> anyhow::Result>; + async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result>; + async fn get_plugin_preferences(&self) -> anyhow::Result>; + async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result>; + async fn plugin_preferences_required(&self) -> anyhow::Result; + async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result; + async fn clipboard_read(&self) -> anyhow::Result; + async fn clipboard_read_text(&self) -> anyhow::Result>; + async fn clipboard_write(&self, data: JsClipboardData) -> anyhow::Result<()>; + async fn clipboard_write_text(&self, data: String) -> anyhow::Result<()>; + async fn clipboard_clear(&self) -> anyhow::Result<()>; + async fn ui_update_loading_bar(&self, entrypoint_id: EntrypointId, show: bool) -> anyhow::Result<()>; + async fn ui_show_hud(&self, display: String) -> anyhow::Result<()>; + async fn ui_get_action_id_for_shortcut( + &self, + entrypoint_id: EntrypointId, + key: String, + modifier_shift: bool, + modifier_control: bool, + modifier_alt: bool, + modifier_meta: bool + ) -> anyhow::Result>; + async fn ui_render( + &self, + entrypoint_id: EntrypointId, + render_location: UiRenderLocation, + top_level_view: bool, + container: RootWidget, + ) -> anyhow::Result<()>; + async fn ui_show_plugin_error_view( + &self, + entrypoint_id: EntrypointId, + render_location: UiRenderLocation + ) -> anyhow::Result<()>; + async fn ui_show_preferences_required_view( + &self, + entrypoint_id: EntrypointId, + plugin_preferences_required: bool, + entrypoint_preferences_required: bool + ) -> anyhow::Result<()>; + async fn ui_clear_inline_view(&self) -> anyhow::Result<()>; +} + +#[derive(Clone)] +pub struct BackendForPluginRuntimeApiProxy { + request_sender: RequestSender> +} + +impl BackendForPluginRuntimeApiProxy { + pub fn new(request_sender: RequestSender>) -> Self { + Self { + request_sender + } + } +} + +impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { + async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { + let request = JsRequest::ReloadSearchIndex { + generated_commands, + refresh_search_list, + }; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::Nothing => Ok(()), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn get_asset_data(&self, path: &str) -> anyhow::Result> { + let request = JsRequest::GetAssetData { + path: path.to_string(), + }; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::AssetData { data } => Ok(data), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result> { + let request = JsRequest::GetCommandGeneratorEntrypointIds; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::CommandGeneratorEntrypointIds { data } => Ok(data), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn get_plugin_preferences(&self) -> anyhow::Result> { + let request = JsRequest::GetPluginPreferences; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::PluginPreferences { data } => Ok(data), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result> { + let request = JsRequest::GetEntrypointPreferences { + entrypoint_id, + }; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::EntrypointPreferences { data } => Ok(data), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn plugin_preferences_required(&self) -> anyhow::Result { + let request = JsRequest::PluginPreferencesRequired; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::PluginPreferencesRequired { data } => Ok(data), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result { + let request = JsRequest::EntrypointPreferencesRequired { + entrypoint_id, + }; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::EntrypointPreferencesRequired { data } => Ok(data), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn clipboard_read(&self) -> anyhow::Result { + let request = JsRequest::ClipboardRead; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::ClipboardRead { data } => Ok(data), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn clipboard_read_text(&self) -> anyhow::Result> { + let request = JsRequest::ClipboardReadText; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::ClipboardReadText { data } => Ok(data), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn clipboard_write(&self, data: JsClipboardData) -> anyhow::Result<()> { + let request = JsRequest::ClipboardWrite { + data + }; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::Nothing => Ok(()), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn clipboard_write_text(&self, data: String) -> anyhow::Result<()> { + let request = JsRequest::ClipboardWriteText { + data, + }; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::Nothing => Ok(()), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn clipboard_clear(&self) -> anyhow::Result<()> { + let request = JsRequest::ClipboardClear; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::Nothing => Ok(()), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn ui_update_loading_bar(&self, entrypoint_id: EntrypointId, show: bool) -> anyhow::Result<()> { + let request = JsRequest::UpdateLoadingBar { + entrypoint_id, + show, + }; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::Nothing => Ok(()), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn ui_show_hud(&self, display: String) -> anyhow::Result<()> { + let request = JsRequest::ShowHud { + display, + }; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::Nothing => Ok(()), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn ui_get_action_id_for_shortcut(&self, entrypoint_id: EntrypointId, key: String, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> anyhow::Result> { + let request = JsRequest::GetActionIdForShortcut { + entrypoint_id, + key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + }; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::ActionIdForShortcut { data } => Ok(data), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn ui_render( + &self, + entrypoint_id: EntrypointId, + render_location: UiRenderLocation, + top_level_view: bool, + container: RootWidget, + ) -> anyhow::Result<()> { + let request = JsRequest::Render { + entrypoint_id, + render_location: match render_location { + UiRenderLocation::InlineView => JsUiRenderLocation::InlineView, + UiRenderLocation::View => JsUiRenderLocation::View + }, + top_level_view, + container, + }; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::Nothing => Ok(()), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn ui_show_plugin_error_view(&self, entrypoint_id: EntrypointId, render_location: UiRenderLocation) -> anyhow::Result<()> { + let request = JsRequest::ShowPluginErrorView { + entrypoint_id, + render_location: match render_location { + UiRenderLocation::InlineView => JsUiRenderLocation::InlineView, + UiRenderLocation::View => JsUiRenderLocation::View + }, + }; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::Nothing => Ok(()), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn ui_show_preferences_required_view(&self, entrypoint_id: EntrypointId, plugin_preferences_required: bool, entrypoint_preferences_required: bool) -> anyhow::Result<()> { + let request = JsRequest::ShowPreferenceRequiredView { + entrypoint_id, + plugin_preferences_required, + entrypoint_preferences_required, + }; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::Nothing => Ok(()), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + + async fn ui_clear_inline_view(&self) -> anyhow::Result<()> { + let request = JsRequest::ClearInlineView; + + match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + JsResponse::Nothing => Ok(()), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } +} \ No newline at end of file diff --git a/rust/server/src/plugins/js/assets.rs b/rust/plugin_runtime/src/assets.rs similarity index 68% rename from rust/server/src/plugins/js/assets.rs rename to rust/plugin_runtime/src/assets.rs index f3b770d..b2aac4e 100644 --- a/rust/server/src/plugins/js/assets.rs +++ b/rust/plugin_runtime/src/assets.rs @@ -1,10 +1,9 @@ -use crate::plugins::data_db_repository::DataDbRepository; -use crate::plugins::js::{BackendForPluginRuntimeApiImpl, PluginData}; use deno_core::futures::executor::block_on; use deno_core::{op2, OpState}; use std::cell::RefCell; use std::rc::Rc; -use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; +use tokio::runtime::Handle; +use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; #[op2(async)] #[buffer] @@ -13,7 +12,7 @@ pub async fn asset_data(state: Rc>, #[string] path: String) -> let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api @@ -27,19 +26,23 @@ pub async fn asset_data(state: Rc>, #[string] path: String) -> #[op2] #[buffer] pub fn asset_data_blocking(state: Rc>, #[string] path: String) -> anyhow::Result> { - let api = { + let (api, outer_handle) = { let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); - api + let outer_handle = state + .borrow::() + .clone(); + + (api, outer_handle) }; tracing::trace!(target = "renderer_rs", "Fetching asset data blocking {:?}", path); - block_on(async { + outer_handle.block_on(async { let data = api.get_asset_data(&path).await?; Ok(data) diff --git a/rust/server/src/plugins/js/clipboard.rs b/rust/plugin_runtime/src/clipboard.rs similarity index 78% rename from rust/server/src/plugins/js/clipboard.rs rename to rust/plugin_runtime/src/clipboard.rs index 8075a78..574236e 100644 --- a/rust/server/src/plugins/js/clipboard.rs +++ b/rust/plugin_runtime/src/clipboard.rs @@ -1,10 +1,9 @@ -use crate::plugins::js::BackendForPluginRuntimeApiImpl; -use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; -use common_plugin_runtime::model::ClipboardData; use deno_core::{op2, OpState}; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::rc::Rc; +use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; +use crate::model::JsClipboardData; #[derive(Debug, Serialize, Deserialize)] struct JSClipboardData { @@ -19,7 +18,7 @@ pub async fn clipboard_read(state: Rc>) -> anyhow::Result() + .borrow::() .clone(); api @@ -41,7 +40,7 @@ pub async fn clipboard_read_text(state: Rc>) -> anyhow::Result< let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api @@ -56,13 +55,13 @@ pub async fn clipboard_write(state: Rc>, #[serde] data: JSClipb let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api }; - let clipboard_data = ClipboardData { + let clipboard_data = JsClipboardData { text_data: data.text_data, png_data: data.png_data, }; @@ -76,7 +75,7 @@ pub async fn clipboard_write_text(state: Rc>, #[string] data: S let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api @@ -91,7 +90,7 @@ pub async fn clipboard_clear(state: Rc>) -> anyhow::Result<()> let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api diff --git a/rust/server/src/plugins/js/command_generators.rs b/rust/plugin_runtime/src/command_generators.rs similarity index 65% rename from rust/server/src/plugins/js/command_generators.rs rename to rust/plugin_runtime/src/command_generators.rs index c5b91a1..366e1d6 100644 --- a/rust/server/src/plugins/js/command_generators.rs +++ b/rust/plugin_runtime/src/command_generators.rs @@ -1,8 +1,7 @@ -use crate::plugins::js::BackendForPluginRuntimeApiImpl; -use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; use deno_core::{op2, OpState}; use std::cell::RefCell; use std::rc::Rc; +use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; #[op2(async)] #[serde] @@ -11,7 +10,7 @@ pub async fn get_command_generator_entrypoint_ids(state: Rc>) - let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api diff --git a/rust/server/src/plugins/js/component_model.rs b/rust/plugin_runtime/src/component_model.rs similarity index 100% rename from rust/server/src/plugins/js/component_model.rs rename to rust/plugin_runtime/src/component_model.rs diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs new file mode 100644 index 0000000..0206909 --- /dev/null +++ b/rust/plugin_runtime/src/deno.rs @@ -0,0 +1,451 @@ +use std::collections::HashMap; +use std::fs::File; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::Arc; +use anyhow::{anyhow, Context}; +use deno_core::{FastString, ModuleLoadResponse, ModuleLoader, ModuleSource, ModuleSourceCode, ModuleSpecifier, ModuleType, RequestedModuleType, ResolutionKind, StaticModuleLoader}; +use deno_core::futures::Stream; +use deno_core::url::Url; +use deno_runtime::BootstrapOptions; +use deno_runtime::deno_fs::{FileSystem, RealFs}; +use deno_runtime::deno_io::{Stdio, StdioPipe}; +use deno_runtime::worker::{MainWorker, WorkerOptions, WorkerServiceOptions}; +use once_cell::sync::Lazy; +use regex::Regex; +use tokio::runtime::Handle; +use tokio::sync::mpsc::Receiver; +use common::model::PluginId; +use crate::api::BackendForPluginRuntimeApiProxy; +use crate::assets::{asset_data, asset_data_blocking}; +use crate::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; +use crate::command_generators::get_command_generator_entrypoint_ids; +use crate::component_model::ComponentModel; +use crate::environment::{environment_gauntlet_version, environment_is_development, environment_plugin_cache_dir, environment_plugin_data_dir}; +use crate::events::{op_plugin_get_pending_event, EventReceiver, JsEvent}; +use crate::JsPluginCode; +use crate::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; +use crate::model::JsInit; +use crate::permissions::{permissions_to_deno}; +use crate::plugin_data::PluginData; +use crate::plugins::applications::current_os; +use crate::plugins::numbat::{run_numbat, NumbatContext}; +use crate::plugins::settings::open_settings; +use crate::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; +use crate::search::reload_search_index; +use crate::ui::{clear_inline_view, fetch_action_id_for_shortcut, op_component_model, op_inline_view_endpoint_id, op_react_replace_view, show_hud, show_plugin_error_view, show_preferences_required_view, update_loading_bar}; + + + +pub struct CustomModuleLoader { + code: JsPluginCode, + static_loader: StaticModuleLoader, + dev_plugin: bool, +} + +impl CustomModuleLoader { + fn new(code: JsPluginCode, dev_plugin: bool) -> Self { + let module_map: HashMap<_, _> = MODULES.iter() + .map(|(key, value)| (key.parse().expect("provided key is not valid url"), FastString::from_static(value))) + .collect(); + Self { + code, + static_loader: StaticModuleLoader::new(module_map), + dev_plugin + } + } +} + +const MODULES: [(&str, &str); 10] = [ + ("gauntlet:init", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/core/dist/init.js"))), + ("gauntlet:bridge/components", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-components.js"))), + ("gauntlet:bridge/hooks", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-hooks.js"))), + ("gauntlet:bridge/helpers", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-helpers.js"))), + ("gauntlet:bridge/core", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-core.js"))), + ("gauntlet:bridge/react", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-react.js"))), + ("gauntlet:bridge/react-jsx-runtime", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-react-jsx-runtime.js"))), + ("gauntlet:bridge/internal-all", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-all.js"))), + ("gauntlet:bridge/internal-linux", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-linux.js"))), + ("gauntlet:bridge/internal-macos", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-macos.js"))), +]; + +impl ModuleLoader for CustomModuleLoader { + fn resolve( + &self, + specifier: &str, + referrer: &str, + _kind: ResolutionKind, + ) -> Result { + static PLUGIN_ENTRYPOINT_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^gauntlet:entrypoint\?(?[a-zA-Z0-9_-]+)$").expect("invalid regex")); + static PLUGIN_MODULE_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^gauntlet:module\?(?[a-zA-Z0-9_-]+)$").expect("invalid regex")); + static PATH_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^\./(?[a-zA-Z0-9_-]+)\.js$").expect("invalid regex")); + + if PLUGIN_ENTRYPOINT_PATTERN.is_match(specifier) { + return Ok(specifier.parse()?); + } + + if PLUGIN_ENTRYPOINT_PATTERN.is_match(referrer) || PLUGIN_MODULE_PATTERN.is_match(referrer) { + if let Some(captures) = PATH_PATTERN.captures(specifier) { + return Ok(format!("gauntlet:module?{}", &captures["js_module"]).parse()?); + } + } + + let specifier = match (specifier, referrer) { + ("gauntlet:init", _) => "gauntlet:init", + ("gauntlet:core", _) => "gauntlet:bridge/core", + ("gauntlet:bridge/internal-all", _) => "gauntlet:bridge/internal-all", + ("gauntlet:bridge/internal-linux", _) => "gauntlet:bridge/internal-linux", + ("gauntlet:bridge/internal-macos", _) => "gauntlet:bridge/internal-macos", + ("react", _) => "gauntlet:bridge/react", + ("react/jsx-runtime", _) => "gauntlet:bridge/react-jsx-runtime", + ("@project-gauntlet/api/components", _) => "gauntlet:bridge/components", + ("@project-gauntlet/api/hooks", _) => "gauntlet:bridge/hooks", + ("@project-gauntlet/api/helpers", _) => "gauntlet:bridge/helpers", + _ => { + return Err(anyhow!("Illegal import with specifier '{}' and referrer '{}'", specifier, referrer)) + } + }; + + Ok(Url::parse(specifier)?) + } + + fn load( + &self, + module_specifier: &ModuleSpecifier, + maybe_referrer: Option<&ModuleSpecifier>, + is_dyn_import: bool, + requested_module_type: RequestedModuleType, + ) -> ModuleLoadResponse { + + let mut specifier = module_specifier.clone(); + specifier.set_query(None); + + match specifier.as_str() { + "gauntlet:init" => { + self.static_loader.load(module_specifier, maybe_referrer, is_dyn_import, requested_module_type) + } + "gauntlet:entrypoint" | "gauntlet:module" => { + match module_specifier.query() { + None => { + ModuleLoadResponse::Sync(Err(anyhow!("Module specifier doesn't have query part"))) + }, + Some(entrypoint_id) => { + let result = self.code.js + .get(entrypoint_id) + .ok_or(anyhow!("Cannot find JS code path: {:?}", entrypoint_id)) + .map(|js| ModuleSourceCode::String(js.clone().into())) + .map(|js| ModuleSource::new(ModuleType::JavaScript, js, module_specifier, None)); + + ModuleLoadResponse::Sync(result) + } + } + } + _ => { + if specifier.as_str().starts_with("gauntlet:bridge/"){ + self.static_loader.load(module_specifier, maybe_referrer, is_dyn_import, requested_module_type) + } else { + ModuleLoadResponse::Sync(Err(anyhow!("Module not found: specifier '{}' and referrer '{:?}'", specifier, maybe_referrer.map(|url| url.as_str())))) + } + } + } + } +} + +deno_core::extension!( + gauntlet, + ops = [ + // core + op_plugin_get_pending_event, + + // logs + op_log_trace, + op_log_debug, + op_log_info, + op_log_warn, + op_log_error, + + // command generators + get_command_generator_entrypoint_ids, + + // assets + asset_data, + asset_data_blocking, + + // ui + op_react_replace_view, + op_inline_view_endpoint_id, + show_plugin_error_view, + clear_inline_view, + show_preferences_required_view, + op_component_model, + fetch_action_id_for_shortcut, + show_hud, + update_loading_bar, + + // preferences + get_plugin_preferences, + get_entrypoint_preferences, + plugin_preferences_required, + entrypoint_preferences_required, + + // search + reload_search_index, + + // clipboard + clipboard_read_text, + clipboard_read, + clipboard_write, + clipboard_write_text, + clipboard_clear, + + // plugin environment + environment_gauntlet_version, + environment_is_development, + environment_plugin_data_dir, + environment_plugin_cache_dir, + ], + options = { + event_receiver: EventReceiver, + plugin_data: PluginData, + component_model: ComponentModel, + backend_api: BackendForPluginRuntimeApiProxy, + outer_handle: Handle + }, + state = |state, options| { + state.put(options.event_receiver); + state.put(options.plugin_data); + state.put(options.component_model); + state.put(options.backend_api); + state.put(options.outer_handle); + }, +); + +mod prod { + deno_core::extension!( + gauntlet_esm, + esm_entry_point = "ext:gauntlet/bootstrap.js", + esm = [ + "ext:gauntlet/bootstrap.js" = "../../js/bridge_build/dist/bridge-bootstrap.js", + "ext:gauntlet/core.js" = "../../js/core/dist/core.js", + "ext:gauntlet/api/components.js" = "../../js/api/dist/gen/components.js", + "ext:gauntlet/api/hooks.js" = "../../js/api/dist/hooks.js", + "ext:gauntlet/api/helpers.js" = "../../js/api/dist/helpers.js", + "ext:gauntlet/renderer.js" = "../../js/react_renderer/dist/prod/renderer.js", + "ext:gauntlet/react.js" = "../../js/react/dist/prod/react.production.min.js", + "ext:gauntlet/react-jsx-runtime.js" = "../../js/react/dist/prod/react-jsx-runtime.production.min.js", + ], + ); +} + +#[allow(long_running_const_eval)] // dev renderer is 22K line file which triggers rust lint +mod dev { + deno_core::extension!( + gauntlet_esm, + esm_entry_point = "ext:gauntlet/bootstrap.js", + esm = [ + "ext:gauntlet/bootstrap.js" = "../../js/bridge_build/dist/bridge-bootstrap.js", + "ext:gauntlet/core.js" = "../../js/core/dist/core.js", + "ext:gauntlet/api/components.js" = "../../js/api/dist/gen/components.js", + "ext:gauntlet/api/hooks.js" = "../../js/api/dist/hooks.js", + "ext:gauntlet/api/helpers.js" = "../../js/api/dist/helpers.js", + "ext:gauntlet/renderer.js" = "../../js/react_renderer/dist/dev/renderer.js", + "ext:gauntlet/react.js" = "../../js/react/dist/dev/react.development.js", + "ext:gauntlet/react-jsx-runtime.js" = "../../js/react/dist/dev/react-jsx-runtime.development.js", + ], + ); +} + +deno_core::extension!( + gauntlet_internal_all, + ops = [ + // plugins numbat + run_numbat, + + // plugins applications + current_os, + + // plugins settings + open_settings, + ], + esm_entry_point = "ext:gauntlet/internal-all/bootstrap.js", + esm = [ + "ext:gauntlet/internal-all/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-all-bootstrap.js", + "ext:gauntlet/internal-all.js" = "../../js/core/dist/internal-all.js", + ], + options = { + numbat_context: NumbatContext, + }, + state = |state, options| { + state.put(options.numbat_context); + }, +); + +#[cfg(target_os = "linux")] +deno_core::extension!( + gauntlet_internal_linux, + ops = [ + // plugins applications linux + crate::plugins::applications::linux_app_from_path, + crate::plugins::applications::linux_application_dirs, + crate::plugins::applications::linux_open_application, + ], + esm_entry_point = "ext:gauntlet/internal-linux/bootstrap.js", + esm = [ + "ext:gauntlet/internal-linux/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-linux-bootstrap.js", + "ext:gauntlet/internal-linux.js" = "../../js/core/dist/internal-linux.js", + ] +); + +#[cfg(target_os = "macos")] +deno_core::extension!( + gauntlet_internal_macos, + ops = [ + // plugins applications macos + crate::plugins::applications::macos_major_version, + crate::plugins::applications::macos_settings_pre_13, + crate::plugins::applications::macos_settings_13_and_post, + crate::plugins::applications::macos_open_setting_13_and_post, + crate::plugins::applications::macos_open_setting_pre_13, + crate::plugins::applications::macos_system_applications, + crate::plugins::applications::macos_application_dirs, + crate::plugins::applications::macos_app_from_arbitrary_path, + crate::plugins::applications::macos_app_from_path, + crate::plugins::applications::macos_open_application, + ], + esm_entry_point = "ext:gauntlet/internal-macos/bootstrap.js", + esm = [ + "ext:gauntlet/internal-macos/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-macos-bootstrap.js", + "ext:gauntlet/internal-macos.js" = "../../js/core/dist/internal-macos.js", + ] +); + + +pub async fn start_js_runtime( + outer_handle: Handle, + init: JsInit, + event_stream: Receiver, + api: BackendForPluginRuntimeApiProxy, +) -> anyhow::Result<()> { + + let stdout = if let Some(stdout_file) = init.stdout_file { + let stdout_file = PathBuf::from(stdout_file); + + std::fs::create_dir_all(stdout_file.parent().unwrap())?; + + let out_log_file = File::create(stdout_file)?; + + StdioPipe::file(out_log_file) + } else { + StdioPipe::inherit() + }; + + let stderr = if let Some(stderr_file) = init.stderr_file { + let stderr_file = PathBuf::from(stderr_file); + + std::fs::create_dir_all(stderr_file.parent().unwrap())?; + + let err_log_file = File::create(stderr_file)?; + + StdioPipe::file(err_log_file) + } else { + StdioPipe::inherit() + }; + + std::fs::create_dir_all(&init.plugin_cache_dir) + .context("Unable to create plugin cache directory")?; + + std::fs::create_dir_all(&init.plugin_data_dir) + .context("Unable to create plugin data directory")?; + + let init_url: ModuleSpecifier = "gauntlet:init".parse().expect("should be valid"); + + let fs: Arc = Arc::new(RealFs); + + let home_dir = PathBuf::from(init.home_dir); + + let permissions_container = permissions_to_deno( + fs.clone(), + &init.permissions, + &home_dir, + Path::new(&init.plugin_data_dir), + Path::new(&init.plugin_cache_dir), + )?; + + let gauntlet_esm = if cfg!(feature = "release") && !init.dev_plugin { + prod::gauntlet_esm::init_ops_and_esm() + } else { + dev::gauntlet_esm::init_ops_and_esm() + }; + + let mut extensions = vec![ + gauntlet::init_ops( + EventReceiver::new(event_stream), + PluginData::new( + init.plugin_id.clone(), + init.plugin_uuid.clone(), + init.plugin_cache_dir, + init.plugin_data_dir, + init.inline_view_entrypoint_id, + home_dir + ), + ComponentModel::new(), + api, + outer_handle + ), + gauntlet_esm, + ]; + + if init.plugin_id.to_string() == "bundled://gauntlet" { + extensions.push(gauntlet_internal_all::init_ops_and_esm(NumbatContext::new())); + + #[cfg(target_os = "macos")] + extensions.push(gauntlet_internal_macos::init_ops_and_esm()); + + #[cfg(target_os = "linux")] + extensions.push(gauntlet_internal_linux::init_ops_and_esm()); + } + + let mut worker = MainWorker::bootstrap_from_options( + init_url.clone(), + WorkerServiceOptions { + blob_store: Arc::new(Default::default()), + broadcast_channel: Default::default(), + feature_checker: Arc::new(Default::default()), + fs, + module_loader: Rc::new(CustomModuleLoader::new(init.code, init.dev_plugin)), + node_services: None, + npm_process_state_provider: None, + permissions: permissions_container, + root_cert_store_provider: None, + fetch_dns_resolver: Default::default(), + shared_array_buffer_store: None, + compiled_wasm_module_store: None, + v8_code_cache: None, + }, + WorkerOptions { + bootstrap: BootstrapOptions { + is_stderr_tty: false, + is_stdout_tty: false, + ..Default::default() + }, + extensions, + maybe_inspector_server: None, + should_wait_for_inspector_session: false, + should_break_on_first_statement: false, + origin_storage_dir: Some(PathBuf::from(init.local_storage_dir)), + stdio: Stdio { + stdin: StdioPipe::inherit(), + stdout, + stderr, + }, + ..Default::default() + }, + ); + + worker.execute_main_module(&init_url).await?; + worker.run_event_loop(false).await?; + + Ok(()) +} + diff --git a/rust/server/src/plugins/js/environment.rs b/rust/plugin_runtime/src/environment.rs similarity index 95% rename from rust/server/src/plugins/js/environment.rs rename to rust/plugin_runtime/src/environment.rs index 0700094..a1349a3 100644 --- a/rust/server/src/plugins/js/environment.rs +++ b/rust/plugin_runtime/src/environment.rs @@ -1,5 +1,5 @@ -use crate::plugins::js::PluginData; use deno_core::{op2, OpState}; +use crate::plugin_data::PluginData; #[op2(fast)] pub fn environment_gauntlet_version() -> u16 { diff --git a/rust/plugin_runtime/src/events.rs b/rust/plugin_runtime/src/events.rs new file mode 100644 index 0000000..8324208 --- /dev/null +++ b/rust/plugin_runtime/src/events.rs @@ -0,0 +1,113 @@ +use std::cell::RefCell; +use std::pin::Pin; +use std::rc::Rc; +use anyhow::anyhow; +use bincode::{Decode, Encode}; +use deno_core::{op2, OpState}; +use deno_core::futures::{Stream, StreamExt}; +use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc::Receiver; +use common::model::UiWidgetId; + +#[derive(Debug, Deserialize, Serialize, Encode, Decode)] +#[serde(tag = "type")] +pub enum JsEvent { + OpenView { + #[serde(rename = "entrypointId")] + entrypoint_id: String + }, + CloseView, + RunCommand { + #[serde(rename = "entrypointId")] + entrypoint_id: String + }, + RunGeneratedCommand { + #[serde(rename = "entrypointId")] + entrypoint_id: String, + #[serde(rename = "actionIndex")] + action_index: Option + }, + ViewEvent { + #[serde(rename = "widgetId")] + widget_id: UiWidgetId, + #[serde(rename = "eventName")] + event_name: String, + #[serde(rename = "eventArguments")] + event_arguments: Vec, + }, + KeyboardEvent { + #[serde(rename = "entrypointId")] + entrypoint_id: String, + origin: JsKeyboardEventOrigin, + key: String, + #[serde(rename = "modifierShift")] + modifier_shift: bool, + #[serde(rename = "modifierControl")] + modifier_control: bool, + #[serde(rename = "modifierAlt")] + modifier_alt: bool, + #[serde(rename = "modifierMeta")] + modifier_meta: bool + }, + OpenInlineView { + #[serde(rename = "text")] + text: String, + }, + ReloadSearchIndex, + RefreshSearchIndex, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Encode, Decode)] +pub enum JsKeyboardEventOrigin { + MainView, + PluginView, +} + +// FIXME this could have been serde_v8::AnyValue but it doesn't support undefined, make a pr? +#[derive(Debug, Deserialize, Serialize, Encode, Decode)] +#[serde(tag = "type")] +pub enum JsUiPropertyValue { + String { + value: String + }, + Number { + value: f64 + }, + Bool { + value: bool + }, + Undefined, +} + +pub struct EventReceiver { + event_stream: Rc>>, +} + +impl EventReceiver { + pub fn new(event_stream: Receiver) -> EventReceiver { + Self { + event_stream: Rc::new(RefCell::new(event_stream)), + } + } +} + +#[op2(async)] +#[serde] +pub async fn op_plugin_get_pending_event(state: Rc>) -> anyhow::Result { + let event_stream = { + state.borrow() + .borrow::() + .event_stream + .clone() + }; + + let mut event_stream = event_stream.borrow_mut(); + let event = event_stream.recv() + .await + .ok_or_else(|| anyhow!("event stream was suddenly closed"))?; + + tracing::trace!("Received plugin event {:?}", event); + + Ok(event) +} + diff --git a/rust/plugin_runtime/src/lib.rs b/rust/plugin_runtime/src/lib.rs new file mode 100644 index 0000000..b251321 --- /dev/null +++ b/rust/plugin_runtime/src/lib.rs @@ -0,0 +1,295 @@ +mod api; +mod assets; +mod clipboard; +mod command_generators; +mod component_model; +mod deno; +mod environment; +mod events; +mod logs; +mod model; +mod permissions; +mod plugin_data; +mod plugins; +mod preferences; +mod search; +mod ui; + +use crate::api::BackendForPluginRuntimeApiProxy; +use crate::deno::start_js_runtime; +use anyhow::{anyhow, Context}; +use bincode::{Decode, Encode}; +use deno_core::futures::SinkExt; +use interprocess::local_socket::tokio::prelude::*; +use interprocess::local_socket::tokio::{RecvHalf, SendHalf, Stream}; +use interprocess::local_socket::{GenericFilePath, NameType, ToNsName}; +use once_cell::sync::Lazy; +use regex::Regex; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::cell::{RefCell, RefMut}; +use std::convert; +use std::fmt::Debug; +use std::ops::{Deref, DerefMut}; +use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; +use std::sync::Arc; +use tokio::io::AsyncWriteExt; +use tokio::io::{AsyncBufReadExt, AsyncReadExt}; +use tokio::runtime::Handle; +use tokio::sync::mpsc::{channel, Receiver, Sender}; +use tokio::sync::{oneshot, Mutex, MutexGuard}; +use tokio_util::sync::CancellationToken; +use utils::channel::{Payload, RequestReceiver}; + +pub use api::BackendForPluginRuntimeApi; +pub use events::JsEvent; +pub use events::JsKeyboardEventOrigin; +pub use events::JsUiPropertyValue; +pub use model::*; +pub use permissions::PERMISSIONS_VARIABLE_PATTERN; + +pub fn run_plugin_runtime(socket_name: String) { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("unable to start tokio runtime for plugin") + .block_on(run_outer(socket_name)) + .expect("plugin runtime crashed"); +} + +async fn run_outer(socket_name: String) -> anyhow::Result<()> { + tracing::info!("Starting plugin runtime at socket: {}", &socket_name); + + let stop_token = CancellationToken::new(); + + #[cfg(target_os = "windows")] + let name = socket_name.to_ns_name::()?; + + #[cfg(unix)] + let name = socket_name + .to_fs_name::()?; + + let conn = Stream::connect(name).await?; + + let (mut recver, mut sender) = conn.split(); + + let (request_sender, mut request_receiver) = utils::channel::channel::>(); + let (event_sender, event_receiver) = channel::(10); + let response_oneshot = Mutex::new(None); + + let init = recv_message::(JsMessageSide::PluginRuntime, &mut recver).await?; + + let plugin_id = init.plugin_id.clone(); + + let api = BackendForPluginRuntimeApiProxy::new(request_sender); + + let handle = Handle::current(); + + tokio::select! { + _ = stop_token.cancelled() => { + tracing::info!("Plugin runtime outer loop has been stopped {:?}", plugin_id) + } + result @ _ = { + tokio::task::unconstrained(async { + loop { + if let Err(err) = message_loop(&mut recver, &event_sender, &response_oneshot, stop_token.clone()).await { + tracing::error!("Message loop has returned an error: {:?}", err); + break; + } + } + }) + } => { + tracing::error!("Message loop has unexpectedly stopped {:?}", plugin_id) + } + result @ _ = { + tokio::task::unconstrained(async { + loop { + if let Err(err) = request_loop(&mut sender, &mut request_receiver, &response_oneshot).await { + tracing::error!("Request loop has returned an error: {:?}", err); + break; + } + } + }) + } => { + tracing::error!("Request loop has unexpectedly stopped {:?}", plugin_id) + } + result @ _ = { + run_new_tokio(handle, stop_token.clone(), init, event_receiver, api) + } => { + tracing::error!("Request loop has unexpectedly stopped {:?}", plugin_id) + } + } + + drop((recver, sender)); + + Ok(()) +} + +async fn run_new_tokio(outer_handle: Handle, stop_token: CancellationToken, init: JsInit, event_receiver: Receiver, api: BackendForPluginRuntimeApiProxy) -> anyhow::Result<()> { + tokio::task::spawn_blocking(|| { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("unable to start tokio runtime for plugin") + .block_on(run(outer_handle, stop_token, init, event_receiver, api)) + }).await??; + + Ok(()) +} + +async fn run(outer_handle: Handle, stop_token: CancellationToken, init: JsInit, event_receiver: Receiver, api: BackendForPluginRuntimeApiProxy) -> anyhow::Result<()> { + let plugin_id = init.plugin_id.clone(); + + tokio::select! { + _ = stop_token.cancelled() => { + tracing::info!("Plugin runtime inner loop has been stopped {:?}", plugin_id) + } + result @ _ = { + tokio::task::unconstrained(async { + start_js_runtime(outer_handle, init, event_receiver, api).await + }) + } => { + if let Err(err) = result { + tracing::error!("Plugin runtime inner loop has failed {:?} - {:?}", plugin_id, err) + } else { + tracing::error!("Plugin runtime inner loop has stopped unexpectedly {:?}", plugin_id) + } + } + } + + Ok(()) +} + +async fn request_loop( + send: &mut SendHalf, + request_receiver: &mut RequestReceiver>, + response_oneshot: &Mutex>>> +) -> anyhow::Result<()> { + let (request, responder) = request_receiver.recv().await; + + tracing::trace!("Received request {:?}", &request); + + let rx = { + let mut response_oneshot = response_oneshot.lock().await; + + let None = response_oneshot.deref() else { + return Err(anyhow!("Trying to set response one shot while previous is not fulfilled")) + }; + + let (tx, rx) = oneshot::channel::>(); + + *response_oneshot = Some(tx); + + rx + }; + + send_message(JsMessageSide::PluginRuntime, send, request).await?; + + tracing::trace!("Waiting for oneshot response..."); + + let response = rx.await?; + + tracing::trace!("Sending response request {:?}", &response); + + responder.respond(response); + + Ok(()) +} + +async fn message_loop( + recv: &mut RecvHalf, + event_sender: &Sender, + response_oneshot: &Mutex>>>, + stop_token: CancellationToken +) -> anyhow::Result<()> { + match recv_message::(JsMessageSide::PluginRuntime, recv).await { + Err(e) => { + tracing::error!("Unable to handle message: {:?}", e); + Err(e) + } + Ok(msg) => match msg { + JsMessage::Event(event) => { + tracing::trace!("Received plugin event from backend {:?}", event); + + event_sender + .send(event) + .await?; + + Ok(()) + } + JsMessage::Response(response) => { + let mut response_oneshot = response_oneshot.lock().await; + + match response_oneshot.take() { + Some(mut oneshot) => { + match oneshot.send(response) { + Err(_) => { + tracing::error!("Dropped oneshot receiving side"); + } + Ok(_) => { + tracing::trace!("Sending oneshot response..."); + } + } + } + None => { + tracing::error!("Received response without corresponding request: {:?}", response); + } + } + + Ok(()) + } + JsMessage::Stop => { + stop_token.cancel(); + + Ok(()) + } + }, + } +} + +#[derive(Debug)] +pub enum JsMessageSide { + PluginRuntime, + Backend +} + +static MESSAGE_ID: AtomicU32 = AtomicU32::new(0); + +pub async fn send_message(side: JsMessageSide, send: &mut SendHalf, value: T) -> anyhow::Result<()> { + let encoded: Vec = bincode::encode_to_vec(&value, bincode::config::standard())?; + + let message_id = MESSAGE_ID.fetch_add(1, Ordering::SeqCst); + + tracing::trace!(side = debug(&side), "Sending message with id {} and size of {} bytes: {:?}", message_id, encoded.len(), &value); + + send.write_u32(message_id).await?; + + send.write_u32(encoded.len() as u32).await?; + + send.write_all(&encoded[..]).await?; + + tracing::trace!(side = debug(&side), "Message with id {} and size of {} bytes has been sent", message_id, encoded.len()); + + Ok(()) +} + +pub async fn recv_message(side: JsMessageSide, recv: &mut RecvHalf) -> anyhow::Result { + tracing::trace!(side = debug(&side), "Waiting for next message..."); + + let message_id = recv.read_u32().await?; + + tracing::trace!(side = debug(&side), "Reading message with id: {}", message_id); + + let buf_size = recv.read_u32().await?; + + let mut buffer = vec![0; buf_size as usize]; + + recv.read_exact(&mut buffer).await?; + + let (decoded, _) = bincode::decode_from_slice(&buffer[..], bincode::config::standard()) + .context(format!("Unable to deserialize message with id: {}", message_id))?; + + tracing::trace!(side = debug(&side), "Received message with id {}: {:?}", message_id, &decoded); + + Ok(decoded) +} diff --git a/rust/server/src/plugins/js/logs.rs b/rust/plugin_runtime/src/logs.rs similarity index 97% rename from rust/server/src/plugins/js/logs.rs rename to rust/plugin_runtime/src/logs.rs index 471d998..8a7cab3 100644 --- a/rust/server/src/plugins/js/logs.rs +++ b/rust/plugin_runtime/src/logs.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::rc::Rc; use deno_core::{op2, OpState}; -use crate::plugins::js::PluginData; +use crate::plugin_data::PluginData; #[op2(fast)] pub fn op_log_trace(state: Rc>, #[string] target: String, #[string] message: String) -> anyhow::Result<()> { diff --git a/rust/plugin_runtime/src/model.rs b/rust/plugin_runtime/src/model.rs new file mode 100644 index 0000000..c8cfa34 --- /dev/null +++ b/rust/plugin_runtime/src/model.rs @@ -0,0 +1,204 @@ +use crate::JsEvent; +use common::model::{EntrypointId, PluginId, RootWidget}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt; +use bincode::{Decode, Encode}; + +#[derive(Debug, Encode, Decode)] +pub enum JsMessage { + Event(JsEvent), + Response(Result), + Stop, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, Encode, Decode)] +pub enum JsUiRenderLocation { + InlineView, + View +} + +#[derive(Debug, Encode, Decode)] +pub struct JsPluginCode { + pub js: HashMap, +} + +#[derive(Debug, Encode, Decode)] +pub struct JsInit { + pub plugin_id: PluginId, + pub plugin_uuid: String, + pub code: JsPluginCode, + pub permissions: JsPluginPermissions, + pub inline_view_entrypoint_id: Option, + pub dev_plugin: bool, + pub home_dir: String, + pub local_storage_dir: String, + pub plugin_cache_dir: String, + pub plugin_data_dir: String, + pub stdout_file: Option, + pub stderr_file: Option, +} + +#[derive(Debug, Encode, Decode)] +pub struct JsPluginPermissions { + pub environment: Vec, + pub network: Vec, + pub filesystem: JsPluginPermissionsFileSystem, + pub exec: JsPluginPermissionsExec, + pub system: Vec, + pub main_search_bar: Vec, +} + +#[derive(Debug, Encode, Decode)] +pub struct JsPluginPermissionsFileSystem { + pub read: Vec, + pub write: Vec, +} + +#[derive(Debug, Encode, Decode)] +pub struct JsPluginPermissionsExec { + pub command: Vec, + pub executable: Vec, +} + +#[derive(Clone, Debug, Encode, Decode)] +pub enum JsPluginPermissionsMainSearchBar { + Read, +} + +#[derive(Debug, Encode, Decode)] +pub enum JsResponse { + Nothing, + AssetData { + data: Vec + }, + CommandGeneratorEntrypointIds { + data: Vec + }, + PluginPreferences { + data: HashMap + }, + EntrypointPreferences { + data: HashMap + }, + PluginPreferencesRequired { + data: bool + }, + EntrypointPreferencesRequired { + data: bool + }, + ClipboardRead { + data: JsClipboardData + }, + ClipboardReadText { + data: Option + }, + ActionIdForShortcut { + data: Option + }, +} + +#[derive(Debug, Encode, Decode)] +pub enum JsRequest { + Render { + entrypoint_id: EntrypointId, + render_location: JsUiRenderLocation, + top_level_view: bool, + container: RootWidget, + }, + ClearInlineView, + ShowPluginErrorView { + entrypoint_id: EntrypointId, + render_location: JsUiRenderLocation, + }, + ShowPreferenceRequiredView { + entrypoint_id: EntrypointId, + plugin_preferences_required: bool, + entrypoint_preferences_required: bool + }, + ShowHud { + display: String + }, + UpdateLoadingBar { + entrypoint_id: EntrypointId, + show: bool + }, + ReloadSearchIndex { + generated_commands: Vec, + refresh_search_list: bool + }, + GetAssetData { + path: String, + }, + GetCommandGeneratorEntrypointIds, + GetPluginPreferences, + GetEntrypointPreferences { + entrypoint_id: EntrypointId, + }, + PluginPreferencesRequired, + EntrypointPreferencesRequired { + entrypoint_id: EntrypointId, + }, + ClipboardRead, + ClipboardReadText, + ClipboardWrite { + data: JsClipboardData + }, + ClipboardWriteText { + data: String + }, + ClipboardClear, + GetActionIdForShortcut { + entrypoint_id: EntrypointId, + key: String, + modifier_shift: bool, + modifier_control: bool, + modifier_alt: bool, + modifier_meta: bool + }, +} + +#[derive(Deserialize, Serialize, Encode, Decode)] +pub struct JsAdditionalSearchItem { + pub entrypoint_name: String, + pub generator_entrypoint_id: String, + pub entrypoint_id: String, + pub entrypoint_uuid: String, + pub entrypoint_icon: Option>, + pub entrypoint_actions: Vec, +} + +impl fmt::Debug for JsAdditionalSearchItem { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + // exclude entrypoint_icon + fmt.debug_struct("JsAdditionalSearchItem") + .field("entrypoint_name", &self.entrypoint_name) + .field("generator_entrypoint_id", &self.generator_entrypoint_id) + .field("entrypoint_id", &self.entrypoint_id) + .field("entrypoint_uuid", &self.entrypoint_uuid) + .field("entrypoint_actions", &self.entrypoint_actions) + .finish() + } +} + +#[derive(Debug, Deserialize, Serialize, Encode, Decode)] +pub struct JsAdditionalSearchItemAction { + pub id: Option, + pub label: String, +} + +#[derive(Debug, Deserialize, Serialize, Encode, Decode)] +#[serde(untagged)] +pub enum JsPreferenceUserData { + Number(f64), + String(String), + Bool(bool), + ListOfStrings(Vec), + ListOfNumbers(Vec), +} + +#[derive(Debug, Serialize, Deserialize, Encode, Decode)] +pub struct JsClipboardData { + pub text_data: Option, + pub png_data: Option> +} \ No newline at end of file diff --git a/rust/server/src/plugins/js/permissions.rs b/rust/plugin_runtime/src/permissions.rs similarity index 86% rename from rust/server/src/plugins/js/permissions.rs rename to rust/plugin_runtime/src/permissions.rs index 4f9c1f8..3da8f0a 100644 --- a/rust/server/src/plugins/js/permissions.rs +++ b/rust/plugin_runtime/src/permissions.rs @@ -7,46 +7,17 @@ use anyhow::anyhow; use deno_runtime::deno_fs::{FileSystemRc, RealFs}; use deno_runtime::deno_permissions::{AllowRunDescriptor, EnvDescriptor, EnvQueryDescriptor, NetDescriptor, Permissions, PermissionsContainer, QueryDescriptor, ReadDescriptor, RunQueryDescriptor, SysDescriptor, SysDescriptorParseError, UnaryPermission, WriteDescriptor}; use deno_runtime::permissions::RuntimePermissionDescriptorParser; -use tonic::codegen::Body; +use once_cell::sync::Lazy; +use regex::Regex; use typed_path::Utf8TypedPath; use common::dirs::Dirs; -use crate::plugins::loader::VARIABLE_PATTERN; +use crate::{JsPluginPermissions, JsPluginPermissionsExec}; -pub struct PluginPermissions { - pub environment: Vec, - pub network: Vec, - pub filesystem: PluginPermissionsFileSystem, - pub exec: PluginPermissionsExec, - pub system: Vec, - pub clipboard: Vec, - pub main_search_bar: Vec, -} - -pub struct PluginPermissionsFileSystem { - pub read: Vec, - pub write: Vec, -} - -pub struct PluginPermissionsExec { - pub command: Vec, - pub executable: Vec, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum PluginPermissionsClipboard { - Read, - Write, - Clear -} - -#[derive(Clone, Debug)] -pub enum PluginPermissionsMainSearchBar { - Read, -} +pub static PERMISSIONS_VARIABLE_PATTERN: Lazy = Lazy::new(|| Regex::new(r"\{(?.+?):(?.+?)}").expect("invalid regex")); pub fn permissions_to_deno( fs: FileSystemRc, - permissions: &PluginPermissions, + permissions: &JsPluginPermissions, home_dir: &Path, plugin_data_dir: &Path, plugin_cache_dir: &Path, @@ -142,7 +113,7 @@ fn sys_permission(system: &[String]) -> anyhow::Result anyhow::Result> { - if let Some(matches) = VARIABLE_PATTERN.captures(path) { + if let Some(matches) = PERMISSIONS_VARIABLE_PATTERN.captures(path) { let namespace = &matches["namespace"]; let name = &matches["name"]; @@ -216,7 +187,7 @@ fn augment_path(path: &String, home_dir: &Path, plugin_data_dir: &Path, plugin_c let replacement = replacement.to_str() .expect("non-utf8 file paths are not supported"); - Ok(Some(PathBuf::from(VARIABLE_PATTERN.replace(path, replacement).to_string()))) + Ok(Some(PathBuf::from(PERMISSIONS_VARIABLE_PATTERN.replace(path, replacement).to_string()))) } } } else { diff --git a/rust/plugin_runtime/src/plugin_data.rs b/rust/plugin_runtime/src/plugin_data.rs new file mode 100644 index 0000000..b06c8b7 --- /dev/null +++ b/rust/plugin_runtime/src/plugin_data.rs @@ -0,0 +1,55 @@ +use std::path::PathBuf; +use common::model::PluginId; + +pub struct PluginData { + plugin_id: PluginId, + plugin_uuid: String, + plugin_cache_dir: String, + plugin_data_dir: String, + inline_view_entrypoint_id: Option, + home_dir: PathBuf, +} + +impl PluginData { + pub fn new( + plugin_id: PluginId, + plugin_uuid: String, + plugin_cache_dir: String, + plugin_data_dir: String, + inline_view_entrypoint_id: Option, + home_dir: PathBuf, + ) -> Self { + Self { + plugin_id, + plugin_uuid, + plugin_cache_dir, + plugin_data_dir, + inline_view_entrypoint_id, + home_dir + } + } + + pub fn plugin_id(&self) -> PluginId { + self.plugin_id.clone() + } + + pub fn plugin_uuid(&self) -> &str { + &self.plugin_uuid + } + + pub fn plugin_cache_dir(&self) -> &str { + &self.plugin_cache_dir + } + + pub fn plugin_data_dir(&self) -> &str { + &self.plugin_data_dir + } + + pub fn inline_view_entrypoint_id(&self) -> Option { + self.inline_view_entrypoint_id.clone() + } + + pub fn home_dir(&self) -> PathBuf { + self.home_dir.clone() + } +} \ No newline at end of file diff --git a/rust/server/src/plugins/js/plugins/applications.rs b/rust/plugin_runtime/src/plugins/applications.rs similarity index 97% rename from rust/server/src/plugins/js/plugins/applications.rs rename to rust/plugin_runtime/src/plugins/applications.rs index ea57a69..2271efa 100644 --- a/rust/server/src/plugins/js/plugins/applications.rs +++ b/rust/plugin_runtime/src/plugins/applications.rs @@ -6,7 +6,7 @@ use image::ImageFormat; use image::imageops::FilterType; use serde::Serialize; use tokio::task::spawn_blocking; -use crate::plugins::js::PluginData; +use crate::plugin_data::PluginData; #[cfg(target_os = "linux")] mod linux; @@ -253,7 +253,7 @@ where } } -pub(in crate::plugins::js::plugins::applications) fn resize_icon(data: Vec) -> anyhow::Result> { +pub(in crate::plugins::applications) fn resize_icon(data: Vec) -> anyhow::Result> { let data = image::load_from_memory_with_format(&data, ImageFormat::Png)?; let data = image::imageops::resize(&data, 48, 48, FilterType::Lanczos3); diff --git a/rust/server/src/plugins/js/plugins/applications/linux.rs b/rust/plugin_runtime/src/plugins/applications/linux.rs similarity index 98% rename from rust/server/src/plugins/js/plugins/applications/linux.rs rename to rust/plugin_runtime/src/plugins/applications/linux.rs index 19577ca..2bf5cfe 100644 --- a/rust/server/src/plugins/js/plugins/applications/linux.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux.rs @@ -3,7 +3,7 @@ use std::fs::Metadata; use std::path::{Path, PathBuf}; use std::{env, fs}; -use crate::plugins::js::plugins::applications::{resize_icon, DesktopApplication, DesktopPathAction}; +use crate::plugins::applications::{resize_icon, DesktopApplication, DesktopPathAction}; use freedesktop_entry_parser::parse_entry; use freedesktop_icons::lookup; use image::imageops::FilterType; diff --git a/rust/server/src/plugins/js/plugins/applications/macos.rs b/rust/plugin_runtime/src/plugins/applications/macos.rs similarity index 98% rename from rust/server/src/plugins/js/plugins/applications/macos.rs rename to rust/plugin_runtime/src/plugins/applications/macos.rs index ad465fb..0a23e64 100644 --- a/rust/server/src/plugins/js/plugins/applications/macos.rs +++ b/rust/plugin_runtime/src/plugins/applications/macos.rs @@ -12,7 +12,7 @@ use objc2_foundation::{CGFloat, CGPoint, CGRect, NSDictionary, NSInteger, NSPoin use plist::Dictionary; use regex::Regex; use serde::Deserialize; -use crate::plugins::js::plugins::applications::{DesktopApplication, DesktopPathAction, DesktopSettings13AndPostData, DesktopSettingsPre13Data}; +use crate::plugins::applications::{DesktopApplication, DesktopPathAction, DesktopSettings13AndPostData, DesktopSettingsPre13Data}; pub fn macos_major_version() -> u8 { diff --git a/rust/server/src/plugins/js/plugins/mod.rs b/rust/plugin_runtime/src/plugins/mod.rs similarity index 100% rename from rust/server/src/plugins/js/plugins/mod.rs rename to rust/plugin_runtime/src/plugins/mod.rs diff --git a/rust/server/src/plugins/js/plugins/numbat.rs b/rust/plugin_runtime/src/plugins/numbat.rs similarity index 100% rename from rust/server/src/plugins/js/plugins/numbat.rs rename to rust/plugin_runtime/src/plugins/numbat.rs diff --git a/rust/server/src/plugins/js/plugins/settings.rs b/rust/plugin_runtime/src/plugins/settings.rs similarity index 100% rename from rust/server/src/plugins/js/plugins/settings.rs rename to rust/plugin_runtime/src/plugins/settings.rs diff --git a/rust/server/src/plugins/js/preferences.rs b/rust/plugin_runtime/src/preferences.rs similarity index 74% rename from rust/server/src/plugins/js/preferences.rs rename to rust/plugin_runtime/src/preferences.rs index 3ba37d3..30d8514 100644 --- a/rust/server/src/plugins/js/preferences.rs +++ b/rust/plugin_runtime/src/preferences.rs @@ -1,22 +1,20 @@ -use crate::plugins::js::BackendForPluginRuntimeApiImpl; use common::model::EntrypointId; -use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; -use common_plugin_runtime::model::PreferenceUserData; use deno_core::futures::executor::block_on; use deno_core::{op2, OpState}; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; - +use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; +use crate::model::JsPreferenceUserData; #[op2] #[serde] -pub fn get_plugin_preferences(state: Rc>) -> anyhow::Result> { +pub fn get_plugin_preferences(state: Rc>) -> anyhow::Result> { let api = { let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api @@ -29,12 +27,12 @@ pub fn get_plugin_preferences(state: Rc>) -> anyhow::Result>, #[string] entrypoint_id: &str) -> anyhow::Result> { +pub fn get_entrypoint_preferences(state: Rc>, #[string] entrypoint_id: &str) -> anyhow::Result> { let api = { let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api @@ -52,7 +50,7 @@ pub async fn plugin_preferences_required(state: Rc>) -> anyhow: let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api @@ -67,7 +65,7 @@ pub async fn entrypoint_preferences_required(state: Rc>, #[stri let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api diff --git a/rust/server/src/plugins/js/search.rs b/rust/plugin_runtime/src/search.rs similarity index 50% rename from rust/server/src/plugins/js/search.rs rename to rust/plugin_runtime/src/search.rs index de1ce15..0a2ce30 100644 --- a/rust/server/src/plugins/js/search.rs +++ b/rust/plugin_runtime/src/search.rs @@ -1,17 +1,16 @@ -use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; -use common_plugin_runtime::model::AdditionalSearchItem; use deno_core::{op2, OpState}; use std::cell::RefCell; use std::rc::Rc; -use crate::plugins::js::BackendForPluginRuntimeApiImpl; +use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; +use crate::model::JsAdditionalSearchItem; #[op2(async)] -pub async fn reload_search_index(state: Rc>, #[serde] generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { +pub async fn reload_search_index(state: Rc>, #[serde] generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { let api = { let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api diff --git a/rust/server/src/plugins/js/ui.rs b/rust/plugin_runtime/src/ui.rs similarity index 65% rename from rust/server/src/plugins/js/ui.rs rename to rust/plugin_runtime/src/ui.rs index 2c1b826..7bbb57b 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/plugin_runtime/src/ui.rs @@ -4,18 +4,18 @@ use std::io::Read; use std::rc::Rc; use anyhow::{anyhow, Context}; use deno_core::{op2, OpState, serde_v8, v8}; -use deno_core::futures::executor::block_on; -use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; +use futures::executor::block_on; use indexmap::IndexMap; -use serde::{de, Deserialize, Deserializer}; +use serde::{de, Deserialize, Deserializer, Serialize}; use serde::de::Error; +use tokio::runtime::Handle; use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, EntrypointId, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Image, ImageSource, ImageSourceAsset, ImageSourceUrl, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectItemWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiPropertyValue, UiRenderLocation, UiWidgetId, WidgetVisitor}; -use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; use component_model::{Component, Property, PropertyType, SharedType}; use component_model::Component::Root; +use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; +use crate::component_model::ComponentModel; use crate::model::JsUiRenderLocation; -use crate::plugins::data_db_repository::DataDbRepository; -use crate::plugins::js::{ComponentModel, PluginData, BackendForPluginRuntimeApiImpl}; +use crate::plugin_data::PluginData; #[op2] pub fn show_plugin_error_view(state: Rc>, #[string] entrypoint_id: String, #[serde] render_location: JsUiRenderLocation) -> anyhow::Result<()> { @@ -23,7 +23,7 @@ pub fn show_plugin_error_view(state: Rc>, #[string] entrypoint_ let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api @@ -34,12 +34,14 @@ pub fn show_plugin_error_view(state: Rc>, #[string] entrypoint_ JsUiRenderLocation::View => UiRenderLocation::View, }; - block_on(async { + tokio::spawn(async move { api.ui_show_plugin_error_view( EntrypointId::from_string(entrypoint_id), render_location, ).await - }) + }); + + Ok(()) } #[op2(fast)] @@ -48,19 +50,21 @@ pub fn show_preferences_required_view(state: Rc>, #[string] ent let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api }; - block_on(async { + tokio::spawn(async move { api.ui_show_preferences_required_view( EntrypointId::from_string(entrypoint_id), plugin_preferences_required, entrypoint_preferences_required ).await - }) + }); + + Ok(()) } #[op2(fast)] @@ -69,13 +73,15 @@ pub fn clear_inline_view(state: Rc>) -> anyhow::Result<()> { let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api }; - block_on(async { api.ui_clear_inline_view().await }) + tokio::spawn(async move { api.ui_clear_inline_view().await }); + + Ok(()) } #[op2] @@ -100,20 +106,22 @@ pub fn op_react_replace_view<'a>( let mut deserializer = serde_v8::Deserializer::new(scope, container.v8_value, None); - #[cfg(feature = "scenario_runner")] - let container_value = serde_value::Value::deserialize(&mut deserializer)?; let container = RootWidget::deserialize(&mut deserializer)?; - let images = ImageGatherer::run_gatherer(state.clone(), &container)?; + let entrypoint_id = EntrypointId::from_string(entrypoint_id); - let api = { + let (api, outer_handle) = { let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); - api + let outer_handle = state + .borrow::() + .clone(); + + (api, outer_handle) }; let render_location = match render_location { @@ -121,17 +129,18 @@ pub fn op_react_replace_view<'a>( JsUiRenderLocation::View => UiRenderLocation::View, }; - block_on(async { - api.ui_render( - EntrypointId::from_string(entrypoint_id), - render_location, - top_level_view, - container, - #[cfg(feature = "scenario_runner")] - container_value, - images - ).await - }) + block_on(async move { + outer_handle.spawn(async move { + api.ui_render( + entrypoint_id, + render_location, + top_level_view, + container, + ).await + }).await + })??; + + Ok(()) } #[op2] @@ -158,7 +167,7 @@ pub async fn fetch_action_id_for_shortcut( let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api @@ -182,13 +191,13 @@ pub async fn show_hud(state: Rc>, #[string] display: String) -> let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api }; - block_on(async { api.ui_show_hud(display).await }) + api.ui_show_hud(display).await } #[op2(async)] @@ -197,75 +206,13 @@ pub async fn update_loading_bar(state: Rc>, #[string] entrypoin let state = state.borrow(); let api = state - .borrow::() + .borrow::() .clone(); api }; - block_on(async { api.ui_update_loading_bar(EntrypointId::from_string(entrypoint_id), show).await }) -} - -struct ImageGatherer { - state: Rc>, - image_sources: HashMap> -} - -impl WidgetVisitor for ImageGatherer { - fn image(&mut self, widget_id: UiWidgetId, widget: &Image) { - if let Image::ImageSource(image_source) = &widget { - self.image_sources.insert(widget_id, get_image_date(self.state.clone(), image_source)); - } - } -} - -impl ImageGatherer { - fn run_gatherer(state: Rc>, root_widget: &RootWidget) -> anyhow::Result> { - let mut gatherer = Self { - state, - image_sources: HashMap::new() - }; - - gatherer.root_widget(root_widget); - - gatherer.image_sources - .into_iter() - .map(|(widget_id, image)| image.map(|image| (widget_id, image))) - .collect::>() - } -} - -fn get_image_date(state: Rc>, source: &ImageSource) -> anyhow::Result { - match source { - ImageSource::ImageSourceAsset(ImageSourceAsset { asset }) => { - let bytes = { - let state = state.borrow(); - - let api = state - .borrow::() - .clone(); - - block_on(async { - api.get_asset_data(&asset).await - })? - }; - - Ok(bytes::Bytes::from(bytes)) - } - ImageSource::ImageSourceUrl(ImageSourceUrl { url }) => { - // FIXME implement error handling so it doesn't error whole view - // TODO implement caching - - let bytes: bytes::Bytes = ureq::get(&url) - .call()? - .into_reader() - .bytes() - .collect::>>()? - .into(); - - Ok(bytes) - } - } + api.ui_update_loading_bar(EntrypointId::from_string(entrypoint_id), show).await } #[allow(unused)] diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 24687fe..6eb6414 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -4,9 +4,7 @@ edition = "2021" [dependencies] serde = { version = "1.0", features = ["derive"] } -serde-value = "0.7.0" -deno_core = { version = "0.321.0" } # deno 2.1.1 -deno_runtime = { version = "0.188.0" } + tokio = "1.28.1" tokio-util = "0.7.11" toml = "0.8.10" @@ -22,46 +20,31 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } sqlx = { version = "0.8.2", features = [ "runtime-tokio", "json", "sqlite" ] } common = { path = "../common" } -common_plugin_runtime = { path = "../common_plugin_runtime" } utils = { path = "../utils" } -component_model = { path = "../component_model" } indexmap = { version = "2.1.0", features = ["serde"] } tonic = "0.11.0" client = { path = "../client" } walkdir = "2.4.0" include_dir = "0.7.3" open = "5" -numbat = "1.14.0" uuid = "1.8" -resvg = { version = "0.41", default-features = false} -image = "0.25" arboard = { version = "=3.2.1", features = ["wayland-data-control"] } # TODO update when dependency hell is solved -ureq = "2.10.0" bytes = "1.6.0" typed-path = "0.9" +plugin_runtime = { path = "../plugin_runtime" } +futures = "0.3.31" +url = "2.5.4" +image = "0.25" +interprocess = { version = "2.2.2", features = ["tokio"] } +ureq = "2.10.0" scenario_runner = { path = "../scenario_runner", optional = true } itertools = "0.10.5" vergen-pretty = "0.3.5" -[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] -libc = "0.2.153" - -[target.'cfg(target_os = "linux")'.dependencies] -freedesktop_entry_parser = "1.3" -freedesktop-icons = "0.2" - -[target.'cfg(target_os = "macos")'.dependencies] -cacao = "0.3.2" -plist = "1.6.1" -icns = "0.3.1" -objc2-app-kit = { version = "0.2.2", features = ["NSWorkspace", "NSImage", "NSImageRep", "NSBitmapImageRep", "NSGraphics", "NSGraphicsContext"] } -objc2-foundation = { version = "0.2.2", features = ["NSString"] } -objc2 = "0.5.2" - [features] release = ["common/release"] -scenario_runner = ["dep:scenario_runner", "common/scenario_runner"] +scenario_runner = ["dep:scenario_runner", "common/scenario_runner", "plugin_runtime/scenario_runner"] [build-dependencies] vergen-gitcl = { version = "1.0.1", features = ["build", "cargo"] } diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index ca77828..3e921e5 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -5,7 +5,8 @@ use client::{open_window, start_client}; use common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; use common::rpc::backend_api::BackendApi; use common::rpc::backend_server::start_backend_server; -use common::{settings_env_data_to_string, SettingsEnvData}; +use common::{settings_env_data_from_string, settings_env_data_to_string, SettingsEnvData}; +use plugin_runtime::run_plugin_runtime; use utils::channel::{channel, RequestReceiver, RequestSender}; use crate::plugins::ApplicationManager; use crate::rpc::BackendServerImpl; @@ -17,8 +18,15 @@ pub(in crate) mod plugins; pub(in crate) mod model; const SETTINGS_ENV: &'static str = "GAUNTLET_INTERNAL_SETTINGS"; +const PLUGIN_RUNTIME_ENV: &'static str = "GAUNTLET_INTERNAL_PLUGIN_RUNTIME"; pub fn start(minimized: bool) { + if let Ok(socket_name) = std::env::var(PLUGIN_RUNTIME_ENV) { + run_plugin_runtime(socket_name); + + return; + } + tracing::info!("Gauntlet Build Information:"); for (name, value) in vergen_pretty_env!() { if let Some(value) = value { diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index b27ad47..db52beb 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -1,81 +1,5 @@ use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, UiPropertyValue, UiWidgetId}; -use serde::{Deserialize, Serialize}; -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] -pub enum JsUiRenderLocation { - InlineView, - View -} - -#[derive(Deserialize, Serialize)] -#[serde(tag = "type")] -pub enum JsUiEvent { - OpenView { - #[serde(rename = "entrypointId")] - entrypoint_id: String - }, - CloseView, - RunCommand { - #[serde(rename = "entrypointId")] - entrypoint_id: String - }, - RunGeneratedCommand { - #[serde(rename = "entrypointId")] - entrypoint_id: String, - #[serde(rename = "actionIndex")] - action_index: Option - }, - ViewEvent { - #[serde(rename = "widgetId")] - widget_id: UiWidgetId, - #[serde(rename = "eventName")] - event_name: String, - #[serde(rename = "eventArguments")] - event_arguments: Vec, - }, - KeyboardEvent { - #[serde(rename = "entrypointId")] - entrypoint_id: String, - origin: JsKeyboardEventOrigin, - key: String, - #[serde(rename = "modifierShift")] - modifier_shift: bool, - #[serde(rename = "modifierControl")] - modifier_control: bool, - #[serde(rename = "modifierAlt")] - modifier_alt: bool, - #[serde(rename = "modifierMeta")] - modifier_meta: bool - }, - OpenInlineView { - #[serde(rename = "text")] - text: String, - }, - ReloadSearchIndex, - RefreshSearchIndex, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub enum JsKeyboardEventOrigin { - MainView, - PluginView, -} - -// FIXME this could have been serde_v8::AnyValue but it doesn't support undefined, make a pr? -#[derive(Debug, Deserialize, Serialize)] -#[serde(tag = "type")] -pub enum JsUiPropertyValue { - String { - value: String - }, - Number { - value: f64 - }, - Bool { - value: bool - }, - Undefined, -} #[derive(Debug)] pub enum IntermediateUiEvent { diff --git a/rust/server/src/plugins/clipboard.rs b/rust/server/src/plugins/clipboard.rs index 9191afa..a350889 100644 --- a/rust/server/src/plugins/clipboard.rs +++ b/rust/server/src/plugins/clipboard.rs @@ -3,7 +3,7 @@ use arboard::ImageData; use image::RgbaImage; use std::io::Cursor; use std::sync::{Arc, RwLock}; -use common_plugin_runtime::model::ClipboardData; +use plugin_runtime::JsClipboardData; #[derive(Clone)] pub struct Clipboard { @@ -20,7 +20,7 @@ impl Clipboard { }) } - pub fn read(&self) -> anyhow::Result { + pub fn read(&self) -> anyhow::Result { let mut clipboard = self.clipboard.write().expect("lock is poisoned"); let png_data = match clipboard.get_image() { @@ -57,7 +57,7 @@ impl Clipboard { } }; - Ok(ClipboardData { + Ok(JsClipboardData { text_data, png_data, }) @@ -81,7 +81,7 @@ impl Clipboard { Ok(data) } - pub fn write(&self, data: ClipboardData) -> anyhow::Result<()> { + pub fn write(&self, data: JsClipboardData) -> anyhow::Result<()> { let mut clipboard = self.clipboard.write().expect("lock is poisoned"); if let Some(png_data) = data.png_data { diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 873e9ef..de61cc3 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -2,10 +2,8 @@ use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use anyhow::{anyhow, Context}; -use deno_core::error::AnyError; -use deno_core::futures; -use deno_core::futures::{StreamExt, TryStreamExt}; -use deno_core::futures::future::join_all; +use futures::{StreamExt, TryStreamExt}; +use futures::future::join_all; use serde::{Deserialize, Serialize}; use sqlx::{Error, Executor, Pool, Row, Sqlite, SqlitePool}; use sqlx::migrate::Migrator; @@ -404,7 +402,7 @@ impl DataDbRepository { .then(|plugin| async move { let entrypoints = self.get_entrypoints_by_plugin_id(&plugin.id).await?; - Ok::<(DbReadPlugin, Vec), AnyError>((plugin, entrypoints)) + Ok::<(DbReadPlugin, Vec), anyhow::Error>((plugin, entrypoints)) }) .try_collect::)>>() .await?; diff --git a/rust/server/src/plugins/image_gatherer.rs b/rust/server/src/plugins/image_gatherer.rs new file mode 100644 index 0000000..f3747b3 --- /dev/null +++ b/rust/server/src/plugins/image_gatherer.rs @@ -0,0 +1,58 @@ +use std::collections::HashMap; +use common::model::{Image, ImageSource, ImageSourceAsset, ImageSourceUrl, RootWidget, UiWidgetId, WidgetVisitor}; +use plugin_runtime::BackendForPluginRuntimeApi; +use crate::plugins::js::BackendForPluginRuntimeApiImpl; +use futures::StreamExt; +use std::io::Read; + +pub struct ImageGatherer<'a> { + api: &'a BackendForPluginRuntimeApiImpl, + image_sources: HashMap>> +} + +impl<'a> WidgetVisitor for ImageGatherer<'a> { + async fn image(&mut self, widget_id: UiWidgetId, widget: &Image) { + if let Image::ImageSource(image_source) = &widget { + self.image_sources.insert(widget_id, get_image_date(&self.api, image_source).await); + } + } +} + +impl<'a> ImageGatherer<'a> { + pub async fn run_gatherer(api: &'a BackendForPluginRuntimeApiImpl, root_widget: &RootWidget) -> anyhow::Result>> { + let mut gatherer = Self { + api, + image_sources: HashMap::new() + }; + + gatherer.root_widget(root_widget).await; + + gatherer.image_sources + .into_iter() + .map(|(widget_id, image)| image.map(|image| (widget_id, image))) + .collect::>() + } +} + +async fn get_image_date(api: &BackendForPluginRuntimeApiImpl, source: &ImageSource) -> anyhow::Result> { + match source { + ImageSource::ImageSourceAsset(ImageSourceAsset { asset }) => { + let bytes = api.get_asset_data(&asset).await?; + + Ok(bytes) + } + ImageSource::ImageSourceUrl(ImageSourceUrl { url }) => { + // FIXME implement error handling so it doesn't error whole view + // TODO implement caching + + let bytes = ureq::get(&url) + .call()? + .into_reader() + .bytes() + .collect::>>()? + .into(); + + Ok(bytes) + } + } +} diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs new file mode 100644 index 0000000..30e4c07 --- /dev/null +++ b/rust/server/src/plugins/js.rs @@ -0,0 +1,1094 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::fs::File; +use std::hash::Hash; +use std::io; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::rc::Rc; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{anyhow, Context}; +use bytes::Bytes; +use futures::AsyncBufReadExt; +use indexmap::IndexMap; +use interprocess::local_socket::{ListenerOptions, ToFsName, ToNsName}; +use interprocess::local_socket::tokio::{RecvHalf, SendHalf}; +use interprocess::local_socket::traits::tokio::{Listener, Stream}; +use interprocess::TryClone; +use once_cell::sync::Lazy; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncRead, AsyncReadExt}; +use tokio::net::TcpStream; +use tokio::sync::Mutex; +use tokio::task::spawn_blocking; +use tokio_util::sync::CancellationToken; +use common::dirs::Dirs; +use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; +use common::rpc::frontend_api::FrontendApi; +use common::settings_env_data_to_string; +use plugin_runtime::{recv_message, send_message, BackendForPluginRuntimeApi, JsAdditionalSearchItem, JsClipboardData, JsInit, JsKeyboardEventOrigin, JsPluginCode, JsPluginPermissions, JsPreferenceUserData, JsEvent, JsUiPropertyValue, JsRequest, JsUiRenderLocation, JsResponse, JsMessage, JsPluginPermissionsFileSystem, JsPluginPermissionsExec, JsPluginPermissionsMainSearchBar, JsMessageSide}; +use crate::model::{IntermediateUiEvent}; +use crate::plugins::clipboard::Clipboard; +use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; +use crate::plugins::icon_cache::IconCache; +use crate::plugins::run_status::RunStatusGuard; +use crate::search::{SearchIndex, SearchIndexItem, SearchIndexItemAction}; +use crate::{PLUGIN_RUNTIME_ENV, SETTINGS_ENV}; +use crate::plugins::image_gatherer::ImageGatherer; + +pub struct PluginRuntimeData { + pub id: PluginId, + pub uuid: String, + pub name: String, + pub entrypoint_names: HashMap, + pub code: JsPluginCode, + pub inline_view_entrypoint_id: Option, + pub permissions: PluginPermissions, + pub command_receiver: tokio::sync::broadcast::Receiver, + pub db_repository: DataDbRepository, + pub search_index: SearchIndex, + pub icon_cache: IconCache, + pub frontend_api: FrontendApi, + pub dirs: Dirs, + pub clipboard: Clipboard, +} + +pub struct PluginPermissions { + pub environment: Vec, + pub network: Vec, + pub filesystem: JsPluginPermissionsFileSystem, + pub exec: JsPluginPermissionsExec, + pub system: Vec, + pub clipboard: Vec, + pub main_search_bar: Vec, +} + +#[derive(Clone, Debug)] +pub struct PluginRuntimePermissions { + pub clipboard: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub enum PluginPermissionsClipboard { + Read, + Write, + Clear +} + +#[derive(Clone, Debug)] +pub enum PluginCommand { + One { + id: PluginId, + data: OnePluginCommandData, + }, + All { + data: AllPluginCommandData, + } +} + +#[derive(Clone, Debug)] +pub enum OnePluginCommandData { + RenderView { + entrypoint_id: EntrypointId, + }, + CloseView, + RunCommand { + entrypoint_id: String, + }, + RunGeneratedCommand { + entrypoint_id: String, + action_index: Option + }, + HandleViewEvent { + widget_id: UiWidgetId, + event_name: String, + event_arguments: Vec, + }, + HandleKeyboardEvent { + entrypoint_id: EntrypointId, + origin: KeyboardEventOrigin, + key: PhysicalKey, + modifier_shift: bool, + modifier_control: bool, + modifier_alt: bool, + modifier_meta: bool, + }, + ReloadSearchIndex, + RefreshSearchIndex, +} + +#[derive(Clone, Debug)] +pub enum AllPluginCommandData { + OpenInlineView { + text: String + } +} + +pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: RunStatusGuard) -> anyhow::Result<()> { + + let runtime_permissions = PluginRuntimePermissions { + clipboard: data.permissions.clipboard, + }; + + let api = BackendForPluginRuntimeApiImpl::new( + data.icon_cache.clone(), + data.db_repository, + data.search_index, + data.clipboard, + data.frontend_api, + data.uuid.clone(), + data.id.clone(), + data.name, + data.entrypoint_names, + runtime_permissions, + ); + + let mut command_receiver = data.command_receiver; + let cache = data.icon_cache; + let plugin_uuid = data.uuid.clone(); + let plugin_id = data.id.clone(); + + let plugin_id_str = plugin_id.to_string(); + let dev_plugin = plugin_id_str.starts_with("file://"); + + let (stdout_file, stderr_file) = if dev_plugin { + let (stdout_file, stderr_file) = data.dirs.plugin_log_files(&plugin_uuid); + + let stdout_file = stdout_file + .to_str() + .context("non-uft8 paths are not supported")? + .to_string(); + + let stderr_file = stderr_file.to_str() + .context("non-uft8 paths are not supported")? + .to_string(); + + (Some(stdout_file), Some(stderr_file)) + } else { + (None, None) + }; + + let home_dir = data.dirs.home_dir(); + let local_storage_dir = data.dirs.plugin_local_storage(&plugin_uuid); + let uds_socket_file = data.dirs.plugin_uds_socket(&plugin_uuid); + let plugin_cache_dir = data.dirs.plugin_cache(&plugin_uuid)?; + let plugin_data_dir = data.dirs.plugin_data(&plugin_uuid)?; + + #[cfg(target_os = "windows")] + let name_str = format!("project-gauntlet-{}", plugin_uuid); + + #[cfg(unix)] + let name_str = uds_socket_file.clone(); + + // namespaced, removed when both client and server disconnect + #[cfg(target_os = "windows")] + let name = name_str.clone().to_ns_name::()?; + + // not namespaced, needs to be cleaned up manually, + // by using close-behind semantics and additionally removing it before creating a new runtime + #[cfg(unix)] + let name = { + let uds_socket_file = uds_socket_file.clone(); + + // manually remove in case of unexpected situation where removing after connection did not work properly + let _ = std::fs::remove_file(&uds_socket_file); + + std::fs::create_dir_all(&uds_socket_file.parent().unwrap())?; + + uds_socket_file.to_fs_name::()? + }; + + let opts = ListenerOptions::new().name(name); + + let listener = opts.create_tokio()?; + + let home_dir = home_dir + .to_str() + .context("non-uft8 paths are not supported")? + .to_string(); + + let local_storage_dir = local_storage_dir + .to_str() + .context("non-uft8 paths are not supported")? + .to_string(); + + let uds_socket_file = uds_socket_file + .to_str() + .context("non-uft8 paths are not supported")? + .to_string(); + + let plugin_cache_dir = plugin_cache_dir + .to_str() + .context("non-uft8 paths are not supported")? + .to_string(); + + let plugin_data_dir = plugin_data_dir + .to_str() + .context("non-uft8 paths are not supported")? + .to_string(); + + let permissions = JsPluginPermissions { + environment: data.permissions.environment, + network: data.permissions.network, + filesystem: data.permissions.filesystem, + exec: data.permissions.exec, + system: data.permissions.system, + main_search_bar: data.permissions.main_search_bar, + }; + + let init = JsInit { + plugin_id: plugin_id.clone(), + plugin_uuid: plugin_uuid.clone(), + code: data.code, + permissions, + inline_view_entrypoint_id: data.inline_view_entrypoint_id, + dev_plugin, + home_dir, + local_storage_dir, + plugin_cache_dir, + plugin_data_dir, + stdout_file, + stderr_file, + }; + + let current_exe = std::env::current_exe() + .context("unable to get current_exe")?; + + std::process::Command::new(current_exe) + .env(PLUGIN_RUNTIME_ENV, name_str) + .spawn() + .context("start plugin runtime process")?; + + // use only for debugging, only works if only one plugin is enabled + // std::thread::spawn(move || { + // plugin_runtime::run_plugin_runtime(name_str.to_str().unwrap().to_string()) + // }); + + let conn = listener.accept().await?; + + #[cfg(unix)] + let _ = std::fs::remove_file(&uds_socket_file); + + let (mut recver, mut sender) = conn.split(); + + send_message(JsMessageSide::Backend, &mut sender, init).await?; + + let sender = Mutex::new(sender); + + tokio::select! { + _ = run_status_guard.stopped() => { + let mut sender = sender.lock().await; + + tracing::info!("Requesting plugin runtime to stop..."); + + send_message(JsMessageSide::Backend, &mut sender, JsMessage::Stop).await?; + + tokio::time::sleep(Duration::from_secs(1)).await; + } + result @ _ = { + tokio::task::unconstrained(async { + loop { + + if let Err(err) = event_loop(&mut command_receiver, &sender, plugin_id.clone()).await { + tracing::error!("Event loop faced an error {:?}", err); + break; + } + } + }) + } => { + tracing::error!("Event loop has unexpectedly stopped {:?}", plugin_id) + } + result @ _ = { + tokio::task::unconstrained(async { + loop { + if let Err(err) = request_loop(&mut recver, &sender, &api).await { + tracing::error!("Request loop faced an error {:?}", err); + break; + } + } + }) + } => { + tracing::error!("Request loop has unexpectedly stopped {:?}", plugin_id) + } + } + + drop((recver, sender)); + + drop(run_status_guard); + + if let Err(err) = cache.clear_plugin_icon_cache_dir(&plugin_uuid) { + tracing::error!(target = "plugin", "plugin {:?} unable to cleanup icon cache {:?}", plugin_id, err) + } + + Ok(()) +} + +async fn event_loop(command_receiver: &mut tokio::sync::broadcast::Receiver, send: &Mutex, plugin_id: PluginId) -> anyhow::Result<()> { + let command = command_receiver.recv().await?; + + let event = match command { + PluginCommand::One { id, data } => { + if id != plugin_id { + None + } else { + match data { + OnePluginCommandData::RenderView { entrypoint_id } => { + Some(IntermediateUiEvent::OpenView { + entrypoint_id, + }) + } + OnePluginCommandData::CloseView => { + Some(IntermediateUiEvent::CloseView) + } + OnePluginCommandData::RunCommand { entrypoint_id } => { + Some(IntermediateUiEvent::RunCommand { + entrypoint_id, + }) + } + OnePluginCommandData::RunGeneratedCommand { entrypoint_id, action_index } => { + Some(IntermediateUiEvent::RunGeneratedCommand { + entrypoint_id, + action_index + }) + } + OnePluginCommandData::HandleViewEvent { widget_id, event_name, event_arguments } => { + Some(IntermediateUiEvent::HandleViewEvent { + widget_id, + event_name, + event_arguments, + }) + } + OnePluginCommandData::HandleKeyboardEvent { entrypoint_id, origin, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { + Some(IntermediateUiEvent::HandleKeyboardEvent { + entrypoint_id, + origin, + key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + }) + } + OnePluginCommandData::ReloadSearchIndex => { + Some(IntermediateUiEvent::ReloadSearchIndex) + } + OnePluginCommandData::RefreshSearchIndex => { + Some(IntermediateUiEvent::RefreshSearchIndex) + } + } + } + } + PluginCommand::All { data } => { + match data { + AllPluginCommandData::OpenInlineView { text } => { + Some(IntermediateUiEvent::OpenInlineView { text }) + } + } + } + }; + + + if let Some(event) = event { + let mut send = send.lock().await; + + send_message(JsMessageSide::Backend, &mut send, JsMessage::Event(from_intermediate_to_js_event(event))).await?; + } + + Ok(()) +} + + +async fn request_loop(recv: &mut RecvHalf, send: &Mutex, api: &BackendForPluginRuntimeApiImpl) -> anyhow::Result<()> { + match recv_message::(JsMessageSide::Backend, recv).await { + Err(e) => { + Err(anyhow!("Unable to handle message: {:?}", e)) + } + Ok(message) => { + tracing::trace!("Handling request message: {:?}", message); + + match handle_message(message, api).await { + Ok(response) => { + let mut send = send.lock().await; + + send_message(JsMessageSide::Backend, &mut send, JsMessage::Response(Ok(response))).await?; + + Ok(()) + } + Err(err) => { + let mut send = send.lock().await; + + let err = format!("{:?}", err); + + send_message(JsMessageSide::Backend, &mut send, JsMessage::Response(Err(err))).await?; + + Ok(()) + } + } + } + } +} + +async fn handle_message(message: JsRequest, api: &BackendForPluginRuntimeApiImpl) -> anyhow::Result { + match message { + JsRequest::Render { entrypoint_id, render_location, top_level_view, container } => { + let render_location = match render_location { + JsUiRenderLocation::InlineView => UiRenderLocation::InlineView, + JsUiRenderLocation::View => UiRenderLocation::View + }; + + api.ui_render(entrypoint_id, render_location, top_level_view, container).await?; + + Ok(JsResponse::Nothing) + } + JsRequest::ClearInlineView => { + api.ui_clear_inline_view().await?; + + Ok(JsResponse::Nothing) + } + JsRequest::ShowPluginErrorView { entrypoint_id, render_location } => { + let render_location = match render_location { + JsUiRenderLocation::InlineView => UiRenderLocation::InlineView, + JsUiRenderLocation::View => UiRenderLocation::View + }; + + api.ui_show_plugin_error_view(entrypoint_id, render_location).await?; + + Ok(JsResponse::Nothing) + } + JsRequest::ShowPreferenceRequiredView { entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { + api.ui_show_preferences_required_view(entrypoint_id, plugin_preferences_required, entrypoint_preferences_required).await?; + + Ok(JsResponse::Nothing) + } + JsRequest::ShowHud { display } => { + api.ui_show_hud(display).await?; + + Ok(JsResponse::Nothing) + } + JsRequest::UpdateLoadingBar { entrypoint_id, show } => { + api.ui_update_loading_bar(entrypoint_id, show).await?; + + Ok(JsResponse::Nothing) + } + JsRequest::ReloadSearchIndex { generated_commands, refresh_search_list } => { + api.reload_search_index(generated_commands, refresh_search_list).await?; + + Ok(JsResponse::Nothing) + } + JsRequest::GetAssetData { path } => { + let data = api.get_asset_data(&path).await?; + + Ok(JsResponse::AssetData { + data + }) + } + JsRequest::GetCommandGeneratorEntrypointIds => { + let data = api.get_command_generator_entrypoint_ids().await?; + + Ok(JsResponse::CommandGeneratorEntrypointIds { + data + }) + } + JsRequest::GetPluginPreferences => { + let data = api.get_plugin_preferences().await?; + + Ok(JsResponse::PluginPreferences { + data + }) + } + JsRequest::GetEntrypointPreferences { entrypoint_id } => { + let data = api.get_entrypoint_preferences(entrypoint_id).await?; + + Ok(JsResponse::EntrypointPreferences { + data + }) + } + JsRequest::PluginPreferencesRequired => { + let data = api.plugin_preferences_required().await?; + + Ok(JsResponse::PluginPreferencesRequired { + data + }) + } + JsRequest::EntrypointPreferencesRequired { entrypoint_id } => { + let data = api.entrypoint_preferences_required(entrypoint_id).await?; + + Ok(JsResponse::EntrypointPreferencesRequired { + data + }) + } + JsRequest::ClipboardRead => { + let data = api.clipboard_read().await?; + + Ok(JsResponse::ClipboardRead { + data + }) + } + JsRequest::ClipboardReadText => { + let data = api.clipboard_read_text().await?; + + Ok(JsResponse::ClipboardReadText { + data + }) + } + JsRequest::ClipboardWrite { data } => { + api.clipboard_write(data).await?; + + Ok(JsResponse::Nothing) + } + JsRequest::ClipboardWriteText { data } => { + api.clipboard_write_text(data).await?; + + Ok(JsResponse::Nothing) + } + JsRequest::ClipboardClear => { + api.clipboard_clear().await?; + + Ok(JsResponse::Nothing) + } + JsRequest::GetActionIdForShortcut { entrypoint_id, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { + let data = api.ui_get_action_id_for_shortcut( + entrypoint_id, + key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + ).await?; + + Ok(JsResponse::ActionIdForShortcut { + data + }) + } + } +} + +fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsEvent { + match event { + IntermediateUiEvent::OpenView { entrypoint_id } => JsEvent::OpenView { + entrypoint_id: entrypoint_id.to_string(), + }, + IntermediateUiEvent::CloseView => JsEvent::CloseView, + IntermediateUiEvent::RunCommand { entrypoint_id } => JsEvent::RunCommand { + entrypoint_id + }, + IntermediateUiEvent::RunGeneratedCommand { entrypoint_id, action_index } => JsEvent::RunGeneratedCommand { + entrypoint_id, + action_index, + }, + IntermediateUiEvent::HandleViewEvent { widget_id, event_name, event_arguments } => { + let event_arguments = event_arguments.into_iter() + .map(|arg| match arg { + UiPropertyValue::String(value) => JsUiPropertyValue::String { value }, + UiPropertyValue::Number(value) => JsUiPropertyValue::Number { value }, + UiPropertyValue::Bool(value) => JsUiPropertyValue::Bool { value }, + UiPropertyValue::Undefined => JsUiPropertyValue::Undefined, + UiPropertyValue::Array(_) | UiPropertyValue::Bytes(_) | UiPropertyValue::Object(_) => { + todo!() + } + }) + .collect(); + + JsEvent::ViewEvent { + widget_id, + event_name, + event_arguments, + } + } + IntermediateUiEvent::HandleKeyboardEvent { entrypoint_id, origin, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { + JsEvent::KeyboardEvent { + entrypoint_id: entrypoint_id.to_string(), + origin: match origin { + KeyboardEventOrigin::MainView => JsKeyboardEventOrigin::MainView, + KeyboardEventOrigin::PluginView => JsKeyboardEventOrigin::PluginView, + }, + key: key.to_value(), + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + } + } + IntermediateUiEvent::OpenInlineView { text } => JsEvent::OpenInlineView { text }, + IntermediateUiEvent::ReloadSearchIndex => JsEvent::ReloadSearchIndex, + IntermediateUiEvent::RefreshSearchIndex => JsEvent::RefreshSearchIndex, + } +} + +#[derive(Clone)] +pub struct BackendForPluginRuntimeApiImpl { + icon_cache: IconCache, + repository: DataDbRepository, + search_index: SearchIndex, + clipboard: Clipboard, + frontend_api: FrontendApi, + plugin_uuid: String, + plugin_id: PluginId, + plugin_name: String, + entrypoint_names: HashMap, + permissions: PluginRuntimePermissions +} + +impl BackendForPluginRuntimeApiImpl { + fn new( + icon_cache: IconCache, + repository: DataDbRepository, + search_index: SearchIndex, + clipboard: Clipboard, + frontend_api: FrontendApi, + plugin_uuid: String, + plugin_id: PluginId, + plugin_name: String, + entrypoint_names: HashMap, + permissions: PluginRuntimePermissions + ) -> Self { + Self { + icon_cache, + repository, + search_index, + clipboard, + frontend_api, + plugin_uuid, + plugin_id, + plugin_name, + entrypoint_names, + permissions + } + } +} + +impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { + async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { + self.icon_cache.clear_plugin_icon_cache_dir(&self.plugin_uuid) + .context("error when clearing up icon cache before recreating it")?; + + let DbReadPlugin { name, .. } = self.repository.get_plugin_by_id(&self.plugin_id.to_string()) + .await + .context("error when getting plugin by id")?; + + let entrypoints = self.repository.get_entrypoints_by_plugin_id(&self.plugin_id.to_string()) + .await + .context("error when getting entrypoints by plugin id")?; + + let frecency_map = self.repository.get_frecency_for_plugin(&self.plugin_id.to_string()) + .await + .context("error when getting frecency for plugin")?; + + let mut shortcuts = HashMap::new(); + + for DbReadPluginEntrypoint { id, .. } in &entrypoints { + let entrypoint_shortcuts = self.repository.action_shortcuts(&self.plugin_id.to_string(), id).await?; + shortcuts.insert(id.clone(), entrypoint_shortcuts); + } + + let mut plugins_search_items = generated_commands.into_iter() + .map(|item| { + let entrypoint_icon_path = match item.entrypoint_icon { + None => None, + Some(data) => Some(self.icon_cache.save_entrypoint_icon_to_cache(&self.plugin_uuid, &item.entrypoint_uuid, &data)?), + }; + + let entrypoint_frecency = frecency_map.get(&item.entrypoint_id).cloned().unwrap_or(0.0); + + let shortcuts = shortcuts + .get(&item.generator_entrypoint_id); + + let entrypoint_actions = item.entrypoint_actions.iter() + .map(|action| { + let shortcut = match (shortcuts, &action.id) { + (Some(shortcuts), Some(id)) => { + shortcuts.get(id).cloned() + } + _ => None + }; + + SearchIndexItemAction { + label: action.label.clone(), + shortcut, + } + }) + .collect(); + + Ok(SearchIndexItem { + entrypoint_type: SearchResultEntrypointType::GeneratedCommand, + entrypoint_id: EntrypointId::from_string(item.entrypoint_id), + entrypoint_name: item.entrypoint_name, + entrypoint_icon_path, + entrypoint_frecency, + entrypoint_actions, + }) + }) + .collect::>>()?; + + let mut icon_asset_data = HashMap::new(); + + for entrypoint in &entrypoints { + if let Some(path_to_asset) = &entrypoint.icon_path { + let result = self.repository.get_asset_data(&self.plugin_id.to_string(), path_to_asset) + .await; + + if let Ok(data) = result { + icon_asset_data.insert((entrypoint.id.clone(), path_to_asset.clone()), data); + } + } + } + + let mut builtin_search_items = entrypoints.into_iter() + .filter(|entrypoint| entrypoint.enabled) + .map(|entrypoint| { + let entrypoint_type = db_entrypoint_from_str(&entrypoint.entrypoint_type); + let entrypoint_id = entrypoint.id.to_string(); + + let entrypoint_frecency = frecency_map.get(&entrypoint_id).cloned().unwrap_or(0.0); + + let entrypoint_icon_path = match entrypoint.icon_path { + None => None, + Some(path_to_asset) => { + match icon_asset_data.get(&(entrypoint.id, path_to_asset)) { + None => None, + Some(data) => Some(self.icon_cache.save_entrypoint_icon_to_cache(&self.plugin_uuid, &entrypoint.uuid, data)?) + } + }, + }; + + let entrypoint_id = EntrypointId::from_string(entrypoint_id); + + match &entrypoint_type { + DbPluginEntrypointType::Command => { + Ok(Some(SearchIndexItem { + entrypoint_type: SearchResultEntrypointType::Command, + entrypoint_name: entrypoint.name, + entrypoint_id, + entrypoint_icon_path, + entrypoint_frecency, + entrypoint_actions: vec![], + })) + }, + DbPluginEntrypointType::View => { + Ok(Some(SearchIndexItem { + entrypoint_type: SearchResultEntrypointType::View, + entrypoint_name: entrypoint.name, + entrypoint_id, + entrypoint_icon_path, + entrypoint_frecency, + entrypoint_actions: vec![], + })) + }, + DbPluginEntrypointType::CommandGenerator | DbPluginEntrypointType::InlineView => { + Ok(None) + } + } + }) + .collect::>>()? + .into_iter() + .flat_map(|item| item) + .collect::>(); + + plugins_search_items.append(&mut builtin_search_items); + + self.search_index.save_for_plugin(self.plugin_id.clone(), name, plugins_search_items, refresh_search_list) + .context("error when updating search index")?; + + Ok(()) + } + + async fn get_asset_data(&self, path: &str) -> anyhow::Result> { + let data = self.repository.get_asset_data(&self.plugin_id.to_string(), &path) + .await?; + + Ok(data) + } + + async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result> { + let result = self.repository.get_entrypoints_by_plugin_id(&self.plugin_id.to_string()).await? + .into_iter() + .filter(|entrypoint| entrypoint.enabled) + .filter(|entrypoint| matches!(db_entrypoint_from_str(&entrypoint.entrypoint_type), DbPluginEntrypointType::CommandGenerator)) + .map(|entrypoint| entrypoint.id) + .collect::>(); + + Ok(result) + } + + async fn get_plugin_preferences(&self) -> anyhow::Result> { + let DbReadPlugin { preferences, preferences_user_data, .. } = self.repository + .get_plugin_by_id(&self.plugin_id.to_string()) + .await?; + + Ok(preferences_to_js(preferences, preferences_user_data)) + } + + async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result> { + let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = self.repository + .get_entrypoint_by_id(&self.plugin_id.to_string(), &entrypoint_id.to_string()) + .await?; + + Ok(preferences_to_js(preferences, preferences_user_data)) + } + + async fn plugin_preferences_required(&self) -> anyhow::Result { + let DbReadPlugin { preferences, preferences_user_data, .. } = self.repository + .get_plugin_by_id(&self.plugin_id.to_string()).await?; + + Ok(any_preferences_missing_value(preferences, preferences_user_data)) + } + + async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result { + let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = self.repository + .get_entrypoint_by_id(&self.plugin_id.to_string(), &entrypoint_id.to_string()).await?; + + Ok(any_preferences_missing_value(preferences, preferences_user_data)) + } + + async fn clipboard_read(&self) -> anyhow::Result { + let allow = self + .permissions + .clipboard + .contains(&PluginPermissionsClipboard::Read); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); + } + + tracing::debug!("Reading from clipboard, plugin id: {:?}", self.plugin_id); + + self.clipboard.read() + } + + async fn clipboard_read_text(&self) -> anyhow::Result> { + let allow = self + .permissions + .clipboard + .contains(&PluginPermissionsClipboard::Read); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); + } + + tracing::debug!("Reading text from clipboard, plugin id: {:?}", self.plugin_id); + + self.clipboard.read_text() + } + + async fn clipboard_write(&self, data: JsClipboardData) -> anyhow::Result<()> { + let allow = self + .permissions + .clipboard + .contains(&PluginPermissionsClipboard::Write); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); + } + + tracing::debug!("Writing to clipboard, plugin id: {:?}", self.plugin_id); + + self.clipboard.write(data) + } + + async fn clipboard_write_text(&self, data: String) -> anyhow::Result<()> { + let allow = self + .permissions + .clipboard + .contains(&PluginPermissionsClipboard::Write); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); + } + + tracing::debug!("Writing text to clipboard, plugin id: {:?}", self.plugin_id); + + self.clipboard.write_text(data) + } + + async fn clipboard_clear(&self) -> anyhow::Result<()> { + let allow = self + .permissions + .clipboard + .contains(&PluginPermissionsClipboard::Clear); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'clear' permission for clipboard")); + } + + tracing::debug!("Clearing clipboard, plugin id: {:?}", self.plugin_id); + + self.clipboard.clear() + } + + async fn ui_update_loading_bar(&self, entrypoint_id: EntrypointId, show: bool) -> anyhow::Result<()> { + self.frontend_api.update_loading_bar(self.plugin_id.clone(), entrypoint_id, show).await?; + + Ok(()) + } + + async fn ui_show_hud(&self, display: String) -> anyhow::Result<()> { + self.frontend_api.show_hud(display).await?; + + Ok(()) + } + + async fn ui_get_action_id_for_shortcut( + &self, + entrypoint_id: EntrypointId, + key: String, + modifier_shift: bool, + modifier_control: bool, + modifier_alt: bool, + modifier_meta: bool + ) -> anyhow::Result> { + let result = self.repository.get_action_id_for_shortcut( + &self.plugin_id.to_string(), + &entrypoint_id.to_string(), + PhysicalKey::from_value(key), + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta + ).await?; + + Ok(result) + } + + async fn ui_render( + &self, + entrypoint_id: EntrypointId, + render_location: UiRenderLocation, + top_level_view: bool, + container: RootWidget, + ) -> anyhow::Result<()> { + + let entrypoint_name = self.entrypoint_names + .get(&entrypoint_id) + .expect("entrypoint name for id should always exist") + .to_string(); + + let images = ImageGatherer::run_gatherer(&self, &container).await?; + + self.frontend_api.replace_view( + self.plugin_id.clone(), + self.plugin_name.clone(), + entrypoint_id, + entrypoint_name, + render_location, + top_level_view, + container, + images + ).await?; + + Ok(()) + } + + async fn ui_show_plugin_error_view( + &self, + entrypoint_id: EntrypointId, + render_location: UiRenderLocation + ) -> anyhow::Result<()> { + self.frontend_api.show_plugin_error_view( + self.plugin_id.clone(), + entrypoint_id, + render_location + ).await?; + + Ok(()) + } + + async fn ui_show_preferences_required_view( + &self, + entrypoint_id: EntrypointId, + plugin_preferences_required: bool, + entrypoint_preferences_required: bool + ) -> anyhow::Result<()> { + + self.frontend_api.show_preference_required_view( + self.plugin_id.clone(), + entrypoint_id, + plugin_preferences_required, + entrypoint_preferences_required + ).await?; + + Ok(()) + } + + async fn ui_clear_inline_view(&self) -> anyhow::Result<()> { + self.frontend_api.clear_inline_view(self.plugin_id.clone()).await?; + + Ok(()) + } +} + + +fn preferences_to_js( + preferences: HashMap, + mut preferences_user_data: HashMap +) -> HashMap { + preferences.into_iter() + .map(|(name, preference)| { + let user_data = match preferences_user_data.remove(&name) { + None => match preference { + DbPluginPreference::Number { default, .. } => JsPreferenceUserData::Number(default.expect("at this point preference should always have value")), + DbPluginPreference::String { default, .. } => JsPreferenceUserData::String(default.expect("at this point preference should always have value")), + DbPluginPreference::Enum { default, .. } => JsPreferenceUserData::String(default.expect("at this point preference should always have value")), + DbPluginPreference::Bool { default, .. } => JsPreferenceUserData::Bool(default.expect("at this point preference should always have value")), + DbPluginPreference::ListOfStrings { default, .. } => JsPreferenceUserData::ListOfStrings(default.expect("at this point preference should always have value")), + DbPluginPreference::ListOfNumbers { default, .. } => JsPreferenceUserData::ListOfNumbers(default.expect("at this point preference should always have value")), + DbPluginPreference::ListOfEnums { default, .. } => JsPreferenceUserData::ListOfStrings(default.expect("at this point preference should always have value")), + } + Some(user_data) => match user_data { + DbPluginPreferenceUserData::Number { value } => JsPreferenceUserData::Number(value.expect("at this point preference should always have value")), + DbPluginPreferenceUserData::String { value } => JsPreferenceUserData::String(value.expect("at this point preference should always have value")), + DbPluginPreferenceUserData::Enum { value } => JsPreferenceUserData::String(value.expect("at this point preference should always have value")), + DbPluginPreferenceUserData::Bool { value } => JsPreferenceUserData::Bool(value.expect("at this point preference should always have value")), + DbPluginPreferenceUserData::ListOfStrings { value } => JsPreferenceUserData::ListOfStrings(value.expect("at this point preference should always have value")), + DbPluginPreferenceUserData::ListOfNumbers { value } => JsPreferenceUserData::ListOfNumbers(value.expect("at this point preference should always have value")), + DbPluginPreferenceUserData::ListOfEnums { value } => JsPreferenceUserData::ListOfStrings(value.expect("at this point preference should always have value")), + } + }; + + (name, user_data) + }) + .collect() +} + +fn any_preferences_missing_value(preferences: HashMap, preferences_user_data: HashMap) -> bool { + for (name, preference) in preferences { + match preferences_user_data.get(&name) { + None => { + let no_default = match preference { + DbPluginPreference::Number { default, .. } => default.is_none(), + DbPluginPreference::String { default, .. } => default.is_none(), + DbPluginPreference::Enum { default, .. } => default.is_none(), + DbPluginPreference::Bool { default, .. } => default.is_none(), + DbPluginPreference::ListOfStrings { default, .. } => default.is_none(), + DbPluginPreference::ListOfNumbers { default, .. } => default.is_none(), + DbPluginPreference::ListOfEnums { default, .. } => default.is_none(), + }; + + if no_default { + return true + } + } + Some(preference) => { + let no_value = match preference { + DbPluginPreferenceUserData::Number { value } => value.is_none(), + DbPluginPreferenceUserData::String { value } => value.is_none(), + DbPluginPreferenceUserData::Enum { value } => value.is_none(), + DbPluginPreferenceUserData::Bool { value } => value.is_none(), + DbPluginPreferenceUserData::ListOfStrings { value } => value.is_none(), + DbPluginPreferenceUserData::ListOfNumbers { value } => value.is_none(), + DbPluginPreferenceUserData::ListOfEnums { value } => value.is_none(), + }; + + if no_value { + return true + } + } + } + } + + false +} \ No newline at end of file diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs deleted file mode 100644 index 9dad5a9..0000000 --- a/rust/server/src/plugins/js/mod.rs +++ /dev/null @@ -1,1331 +0,0 @@ -use std::cell::RefCell; -use std::collections::HashMap; -use std::fs::File; -use std::hash::Hash; -use std::net::SocketAddr; -use std::path::{Path, PathBuf}; -use std::pin::Pin; -use std::rc::Rc; -use std::str::FromStr; -use std::sync::Arc; -use std::time::Duration; - -use anyhow::{anyhow, Context}; -use bytes::Bytes; -use deno_core::futures::executor::block_on; -use deno_core::futures::{FutureExt, Stream, StreamExt}; -use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; -use deno_core::{futures, op2, serde_v8, v8, FastString, ModuleLoadResponse, ModuleLoader, ModuleSource, ModuleSourceCode, ModuleSourceFuture, ModuleType, OpState, RequestedModuleType, ResolutionKind, StaticModuleLoader}; -use deno_core::url::Url; -use deno_runtime::deno_core::ModuleSpecifier; -use deno_runtime::deno_io::{Stdio, StdioPipe}; -use deno_runtime::worker::{MainWorker, WorkerServiceOptions}; -use deno_runtime::worker::WorkerOptions; -use deno_runtime::BootstrapOptions; -use deno_runtime::deno_fs::{FileSystem, FileSystemRc, RealFs}; -use indexmap::IndexMap; -use once_cell::sync::Lazy; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use serde_value::Value; -use tokio::net::TcpStream; -use tokio::task::spawn_blocking; -use tokio_util::sync::CancellationToken; - -use common::dirs::Dirs; -use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; -use common::rpc::frontend_api::FrontendApi; -use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi; -use common_plugin_runtime::model::{AdditionalSearchItem, ClipboardData, PreferenceUserData}; - -use crate::model::{IntermediateUiEvent, JsKeyboardEventOrigin, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation}; -use crate::plugins::clipboard::Clipboard; -use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; -use crate::plugins::icon_cache::IconCache; -use crate::plugins::js::assets::{asset_data, asset_data_blocking}; -use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; -use crate::plugins::js::command_generators::{get_command_generator_entrypoint_ids}; -use crate::plugins::js::component_model::ComponentModel; -use crate::plugins::js::environment::{environment_gauntlet_version, environment_is_development, environment_plugin_cache_dir, environment_plugin_data_dir}; -use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; -use crate::plugins::js::permissions::{permissions_to_deno, PluginPermissions, PluginPermissionsClipboard}; -use crate::plugins::js::plugins::applications::current_os; -use crate::plugins::js::plugins::numbat::{run_numbat, NumbatContext}; -use crate::plugins::js::plugins::settings::open_settings; -use crate::plugins::js::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; -use crate::plugins::js::search::reload_search_index; -use crate::plugins::js::ui::{clear_inline_view, fetch_action_id_for_shortcut, op_component_model, op_inline_view_endpoint_id, op_react_replace_view, show_hud, show_plugin_error_view, show_preferences_required_view, update_loading_bar}; -use crate::plugins::run_status::RunStatusGuard; -use crate::search::{SearchIndex, SearchIndexItem, SearchIndexItemAction}; - -mod ui; -mod plugins; -mod logs; -mod assets; -mod preferences; -mod search; -mod command_generators; -pub mod clipboard; -pub mod permissions; -mod environment; -mod component_model; - -pub struct PluginRuntimeData { - pub id: PluginId, - pub uuid: String, - pub name: String, - pub entrypoint_names: HashMap, - pub code: PluginCode, - pub inline_view_entrypoint_id: Option, - pub permissions: PluginPermissions, - pub command_receiver: tokio::sync::broadcast::Receiver, - pub db_repository: DataDbRepository, - pub search_index: SearchIndex, - pub icon_cache: IconCache, - pub frontend_api: FrontendApi, - pub dirs: Dirs, - pub clipboard: Clipboard, -} - -pub struct PluginCode { - pub js: HashMap, -} - -#[derive(Clone, Debug)] -pub struct PluginRuntimePermissions { - pub clipboard: Vec, -} - -#[derive(Clone, Debug)] -pub enum PluginCommand { - One { - id: PluginId, - data: OnePluginCommandData, - }, - All { - data: AllPluginCommandData, - } -} - -#[derive(Clone, Debug)] -pub enum OnePluginCommandData { - RenderView { - entrypoint_id: EntrypointId, - }, - CloseView, - RunCommand { - entrypoint_id: String, - }, - RunGeneratedCommand { - entrypoint_id: String, - action_index: Option - }, - HandleViewEvent { - widget_id: UiWidgetId, - event_name: String, - event_arguments: Vec, - }, - HandleKeyboardEvent { - entrypoint_id: EntrypointId, - origin: KeyboardEventOrigin, - key: PhysicalKey, - modifier_shift: bool, - modifier_control: bool, - modifier_alt: bool, - modifier_meta: bool, - }, - ReloadSearchIndex, - RefreshSearchIndex, -} - -#[derive(Clone, Debug)] -pub enum AllPluginCommandData { - OpenInlineView { - text: String - } -} - -pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: RunStatusGuard) -> anyhow::Result<()> { - let mut command_receiver = data.command_receiver; - let command_stream = async_stream::stream! { - loop { - yield command_receiver.recv().await.unwrap(); - } - }; - - let plugin_id = data.id.clone(); - let event_stream = command_stream - .filter_map(move |command: PluginCommand| { - let plugin_id = plugin_id.clone(); - - let event = match command { - PluginCommand::One { id, data } => { - if id != plugin_id { - None - } else { - match data { - OnePluginCommandData::RenderView { entrypoint_id } => { - Some(IntermediateUiEvent::OpenView { - entrypoint_id, - }) - } - OnePluginCommandData::CloseView => { - Some(IntermediateUiEvent::CloseView) - } - OnePluginCommandData::RunCommand { entrypoint_id } => { - Some(IntermediateUiEvent::RunCommand { - entrypoint_id, - }) - } - OnePluginCommandData::RunGeneratedCommand { entrypoint_id, action_index } => { - Some(IntermediateUiEvent::RunGeneratedCommand { - entrypoint_id, - action_index - }) - } - OnePluginCommandData::HandleViewEvent { widget_id, event_name, event_arguments } => { - Some(IntermediateUiEvent::HandleViewEvent { - widget_id, - event_name, - event_arguments, - }) - } - OnePluginCommandData::HandleKeyboardEvent { entrypoint_id, origin, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { - Some(IntermediateUiEvent::HandleKeyboardEvent { - entrypoint_id, - origin, - key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta - }) - } - OnePluginCommandData::ReloadSearchIndex => { - Some(IntermediateUiEvent::ReloadSearchIndex) - } - OnePluginCommandData::RefreshSearchIndex => { - Some(IntermediateUiEvent::RefreshSearchIndex) - } - } - } - } - PluginCommand::All { data } => { - match data { - AllPluginCommandData::OpenInlineView { text } => { - Some(IntermediateUiEvent::OpenInlineView { text }) - } - } - } - }; - - async move { - event - } - }); - - let event_stream = Box::pin(event_stream); - - let cache = data.icon_cache.clone(); - let plugin_uuid = data.uuid.clone(); - let plugin_id = data.id.clone(); - - let thread_fn = move || { - let plugin_id = data.id.clone(); - - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("unable to start tokio runtime for plugin") - .block_on({ - let plugin_id = data.id.clone(); - - async move { - tokio::select! { - _ = run_status_guard.stopped() => { - tracing::info!(target = "plugin", "Plugin runtime has been stopped {:?}", plugin_id) - } - result @ _ = { - tokio::task::unconstrained(async move { - start_js_runtime( - data.id.clone(), - data.uuid, - data.name, - data.entrypoint_names, - data.code, - data.permissions, - data.inline_view_entrypoint_id, - event_stream, - data.frontend_api, - data.db_repository, - data.search_index, - data.icon_cache, - data.dirs, - data.clipboard - ).await - }) - } => { - if let Err(err) = result { - tracing::error!(target = "plugin", "Plugin runtime has failed {:?} - {:?}", plugin_id, err) - } else { - tracing::error!(target = "plugin", "Plugin runtime has stopped unexpectedly {:?}", plugin_id) - } - } - } - } - }); - - if let Err(err) = cache.clear_plugin_icon_cache_dir(&plugin_uuid) { - tracing::error!(target = "plugin", "plugin {:?} unable to cleanup icon cache {:?}", plugin_id, err) - } - }; - - std::thread::Builder::new() - .name("plugin-js-thread".into()) - .spawn(thread_fn) - .expect("failed to spawn plugin js thread"); - - Ok(()) -} - -async fn start_js_runtime( - plugin_id: PluginId, - plugin_uuid: String, - plugin_name: String, - entrypoint_names: HashMap, - code: PluginCode, - permissions: PluginPermissions, - inline_view_entrypoint_id: Option, - event_stream: Pin>>, - frontend_api: FrontendApi, - repository: DataDbRepository, - search_index: SearchIndex, - icon_cache: IconCache, - dirs: Dirs, - clipboard: Clipboard, -) -> anyhow::Result<()> { - let plugin_id_str = plugin_id.to_string(); - let dev_plugin = plugin_id_str.starts_with("file://"); - - let (stdout, stderr) = if dev_plugin { - let (out_log_file, err_log_file) = dirs.plugin_log_files(&plugin_uuid); - - std::fs::create_dir_all(out_log_file.parent().unwrap())?; - std::fs::create_dir_all(err_log_file.parent().unwrap())?; - - let out_log_file = File::create(out_log_file)?; - let err_log_file = File::create(err_log_file)?; - - (StdioPipe::file(out_log_file), StdioPipe::file(err_log_file)) - } else { - (StdioPipe::inherit(), StdioPipe::inherit()) - }; - - let home_dir = dirs.home_dir(); - let local_storage_dir = dirs.plugin_local_storage(&plugin_uuid); - let plugin_cache_dir = dirs.plugin_cache(&plugin_uuid)?.to_str().expect("non-uft8 paths are not supported").to_string(); - let plugin_data_dir = dirs.plugin_data(&plugin_uuid)?.to_str().expect("non-uft8 paths are not supported").to_string(); - - std::fs::create_dir_all(&plugin_cache_dir) - .context("Unable to create plugin cache directory")?; - - std::fs::create_dir_all(&plugin_data_dir) - .context("Unable to create plugin data directory")?; - - let init_url: ModuleSpecifier = "gauntlet:init".parse().expect("should be valid"); - - let fs: Arc = Arc::new(RealFs); - - let permissions_container = permissions_to_deno( - fs.clone(), - &permissions, - &home_dir, - &PathBuf::from(&plugin_data_dir), - &PathBuf::from(&plugin_cache_dir), - )?; - - let runtime_permissions = PluginRuntimePermissions { - clipboard: permissions.clipboard, - }; - - let gauntlet_esm = if cfg!(feature = "release") && !dev_plugin { - prod::gauntlet_esm::init_ops_and_esm() - } else { - dev::gauntlet_esm::init_ops_and_esm() - }; - - let mut extensions = vec![ - gauntlet::init_ops( - EventReceiver::new(event_stream), - PluginData::new( - plugin_id.clone(), - plugin_uuid.clone(), - plugin_cache_dir, - plugin_data_dir, - inline_view_entrypoint_id, - home_dir - ), - ComponentModel::new(), - BackendForPluginRuntimeApiImpl::new( - icon_cache, - repository, - search_index, - clipboard, - frontend_api, - plugin_uuid, - plugin_id, - plugin_name, - entrypoint_names, - runtime_permissions, - ), - ), - gauntlet_esm, - ]; - - if plugin_id_str == "bundled://gauntlet" { - extensions.push(gauntlet_internal_all::init_ops_and_esm(NumbatContext::new())); - - #[cfg(target_os = "macos")] - extensions.push(gauntlet_internal_macos::init_ops_and_esm()); - - #[cfg(target_os = "linux")] - extensions.push(gauntlet_internal_linux::init_ops_and_esm()); - } - - let mut worker = MainWorker::bootstrap_from_options( - init_url.clone(), - WorkerServiceOptions { - blob_store: Arc::new(Default::default()), - broadcast_channel: Default::default(), - feature_checker: Arc::new(Default::default()), - fs, - module_loader: Rc::new(CustomModuleLoader::new(code, dev_plugin)), - node_services: None, - npm_process_state_provider: None, - permissions: permissions_container, - root_cert_store_provider: None, - fetch_dns_resolver: Default::default(), - shared_array_buffer_store: None, - compiled_wasm_module_store: None, - v8_code_cache: None, - }, - WorkerOptions { - bootstrap: BootstrapOptions { - is_stderr_tty: false, - is_stdout_tty: false, - ..Default::default() - }, - extensions, - maybe_inspector_server: None, - should_wait_for_inspector_session: false, - should_break_on_first_statement: false, - origin_storage_dir: Some(local_storage_dir), - stdio: Stdio { - stdin: StdioPipe::inherit(), - stdout, - stderr, - }, - ..Default::default() - }, - ); - - worker.execute_main_module(&init_url).await?; - worker.run_event_loop(false).await?; - - Ok(()) -} - -pub struct CustomModuleLoader { - code: PluginCode, - static_loader: StaticModuleLoader, - dev_plugin: bool, -} - -impl CustomModuleLoader { - fn new(code: PluginCode, dev_plugin: bool) -> Self { - let module_map: HashMap<_, _> = MODULES.iter() - .map(|(key, value)| (key.parse().expect("provided key is not valid url"), FastString::from_static(value))) - .collect(); - Self { - code, - static_loader: StaticModuleLoader::new(module_map), - dev_plugin - } - } -} - -const MODULES: [(&str, &str); 10] = [ - ("gauntlet:init", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/core/dist/init.js"))), - ("gauntlet:bridge/components", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-components.js"))), - ("gauntlet:bridge/hooks", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-hooks.js"))), - ("gauntlet:bridge/helpers", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-helpers.js"))), - ("gauntlet:bridge/core", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-core.js"))), - ("gauntlet:bridge/react", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-react.js"))), - ("gauntlet:bridge/react-jsx-runtime", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-react-jsx-runtime.js"))), - ("gauntlet:bridge/internal-all", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-all.js"))), - ("gauntlet:bridge/internal-linux", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-linux.js"))), - ("gauntlet:bridge/internal-macos", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-macos.js"))), -]; - -impl ModuleLoader for CustomModuleLoader { - fn resolve( - &self, - specifier: &str, - referrer: &str, - _kind: ResolutionKind, - ) -> Result { - static PLUGIN_ENTRYPOINT_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^gauntlet:entrypoint\?(?[a-zA-Z0-9_-]+)$").expect("invalid regex")); - static PLUGIN_MODULE_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^gauntlet:module\?(?[a-zA-Z0-9_-]+)$").expect("invalid regex")); - static PATH_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^\./(?[a-zA-Z0-9_-]+)\.js$").expect("invalid regex")); - - if PLUGIN_ENTRYPOINT_PATTERN.is_match(specifier) { - return Ok(specifier.parse()?); - } - - if PLUGIN_ENTRYPOINT_PATTERN.is_match(referrer) || PLUGIN_MODULE_PATTERN.is_match(referrer) { - if let Some(captures) = PATH_PATTERN.captures(specifier) { - return Ok(format!("gauntlet:module?{}", &captures["js_module"]).parse()?); - } - } - - let specifier = match (specifier, referrer) { - ("gauntlet:init", _) => "gauntlet:init", - ("gauntlet:core", _) => "gauntlet:bridge/core", - ("gauntlet:bridge/internal-all", _) => "gauntlet:bridge/internal-all", - ("gauntlet:bridge/internal-linux", _) => "gauntlet:bridge/internal-linux", - ("gauntlet:bridge/internal-macos", _) => "gauntlet:bridge/internal-macos", - ("react", _) => "gauntlet:bridge/react", - ("react/jsx-runtime", _) => "gauntlet:bridge/react-jsx-runtime", - ("@project-gauntlet/api/components", _) => "gauntlet:bridge/components", - ("@project-gauntlet/api/hooks", _) => "gauntlet:bridge/hooks", - ("@project-gauntlet/api/helpers", _) => "gauntlet:bridge/helpers", - _ => { - return Err(anyhow!("Illegal import with specifier '{}' and referrer '{}'", specifier, referrer)) - } - }; - - Ok(Url::parse(specifier)?) - } - - fn load( - &self, - module_specifier: &ModuleSpecifier, - maybe_referrer: Option<&ModuleSpecifier>, - is_dyn_import: bool, - requested_module_type: RequestedModuleType, - ) -> ModuleLoadResponse { - - let mut specifier = module_specifier.clone(); - specifier.set_query(None); - - match specifier.as_str() { - "gauntlet:init" => { - self.static_loader.load(module_specifier, maybe_referrer, is_dyn_import, requested_module_type) - } - "gauntlet:entrypoint" | "gauntlet:module" => { - match module_specifier.query() { - None => { - ModuleLoadResponse::Sync(Err(anyhow!("Module specifier doesn't have query part"))) - }, - Some(entrypoint_id) => { - let result = self.code.js - .get(entrypoint_id) - .ok_or(anyhow!("Cannot find JS code path: {:?}", entrypoint_id)) - .map(|js| ModuleSourceCode::String(js.clone().into())) - .map(|js| ModuleSource::new(ModuleType::JavaScript, js, module_specifier, None)); - - ModuleLoadResponse::Sync(result) - } - } - } - _ => { - if specifier.as_str().starts_with("gauntlet:bridge/"){ - self.static_loader.load(module_specifier, maybe_referrer, is_dyn_import, requested_module_type) - } else { - ModuleLoadResponse::Sync(Err(anyhow!("Module not found: specifier '{}' and referrer '{:?}'", specifier, maybe_referrer.map(|url| url.as_str())))) - } - } - } - } -} - -deno_core::extension!( - gauntlet, - ops = [ - // core - op_plugin_get_pending_event, - - // logs - op_log_trace, - op_log_debug, - op_log_info, - op_log_warn, - op_log_error, - - // command generators - get_command_generator_entrypoint_ids, - - // assets - asset_data, - asset_data_blocking, - - // ui - op_react_replace_view, - op_inline_view_endpoint_id, - show_plugin_error_view, - clear_inline_view, - show_preferences_required_view, - op_component_model, - fetch_action_id_for_shortcut, - show_hud, - update_loading_bar, - - // preferences - get_plugin_preferences, - get_entrypoint_preferences, - plugin_preferences_required, - entrypoint_preferences_required, - - // search - reload_search_index, - - // clipboard - clipboard_read_text, - clipboard_read, - clipboard_write, - clipboard_write_text, - clipboard_clear, - - // plugin environment - environment_gauntlet_version, - environment_is_development, - environment_plugin_data_dir, - environment_plugin_cache_dir, - ], - options = { - event_receiver: EventReceiver, - plugin_data: PluginData, - component_model: ComponentModel, - backend_api: BackendForPluginRuntimeApiImpl, - }, - state = |state, options| { - state.put(options.event_receiver); - state.put(options.plugin_data); - state.put(options.component_model); - state.put(options.backend_api); - }, -); - -mod prod { - deno_core::extension!( - gauntlet_esm, - esm_entry_point = "ext:gauntlet/bootstrap.js", - esm = [ - "ext:gauntlet/bootstrap.js" = "../../js/bridge_build/dist/bridge-bootstrap.js", - "ext:gauntlet/core.js" = "../../js/core/dist/core.js", - "ext:gauntlet/api/components.js" = "../../js/api/dist/gen/components.js", - "ext:gauntlet/api/hooks.js" = "../../js/api/dist/hooks.js", - "ext:gauntlet/api/helpers.js" = "../../js/api/dist/helpers.js", - "ext:gauntlet/renderer.js" = "../../js/react_renderer/dist/prod/renderer.js", - "ext:gauntlet/react.js" = "../../js/react/dist/prod/react.production.min.js", - "ext:gauntlet/react-jsx-runtime.js" = "../../js/react/dist/prod/react-jsx-runtime.production.min.js", - ], - ); -} - -#[allow(long_running_const_eval)] // dev renderer is 22K line file which triggers rust lint -mod dev { - deno_core::extension!( - gauntlet_esm, - esm_entry_point = "ext:gauntlet/bootstrap.js", - esm = [ - "ext:gauntlet/bootstrap.js" = "../../js/bridge_build/dist/bridge-bootstrap.js", - "ext:gauntlet/core.js" = "../../js/core/dist/core.js", - "ext:gauntlet/api/components.js" = "../../js/api/dist/gen/components.js", - "ext:gauntlet/api/hooks.js" = "../../js/api/dist/hooks.js", - "ext:gauntlet/api/helpers.js" = "../../js/api/dist/helpers.js", - "ext:gauntlet/renderer.js" = "../../js/react_renderer/dist/dev/renderer.js", - "ext:gauntlet/react.js" = "../../js/react/dist/dev/react.development.js", - "ext:gauntlet/react-jsx-runtime.js" = "../../js/react/dist/dev/react-jsx-runtime.development.js", - ], - ); -} - -deno_core::extension!( - gauntlet_internal_all, - ops = [ - // plugins numbat - run_numbat, - - // plugins applications - current_os, - - // plugins settings - open_settings, - ], - esm_entry_point = "ext:gauntlet/internal-all/bootstrap.js", - esm = [ - "ext:gauntlet/internal-all/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-all-bootstrap.js", - "ext:gauntlet/internal-all.js" = "../../js/core/dist/internal-all.js", - ], - options = { - numbat_context: NumbatContext, - }, - state = |state, options| { - state.put(options.numbat_context); - }, -); - -#[cfg(target_os = "linux")] -deno_core::extension!( - gauntlet_internal_linux, - ops = [ - // plugins applications linux - crate::plugins::js::plugins::applications::linux_app_from_path, - crate::plugins::js::plugins::applications::linux_application_dirs, - crate::plugins::js::plugins::applications::linux_open_application, - ], - esm_entry_point = "ext:gauntlet/internal-linux/bootstrap.js", - esm = [ - "ext:gauntlet/internal-linux/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-linux-bootstrap.js", - "ext:gauntlet/internal-linux.js" = "../../js/core/dist/internal-linux.js", - ] -); - -#[cfg(target_os = "macos")] -deno_core::extension!( - gauntlet_internal_macos, - ops = [ - // plugins applications macos - crate::plugins::js::plugins::applications::macos_major_version, - crate::plugins::js::plugins::applications::macos_settings_pre_13, - crate::plugins::js::plugins::applications::macos_settings_13_and_post, - crate::plugins::js::plugins::applications::macos_open_setting_13_and_post, - crate::plugins::js::plugins::applications::macos_open_setting_pre_13, - crate::plugins::js::plugins::applications::macos_system_applications, - crate::plugins::js::plugins::applications::macos_application_dirs, - crate::plugins::js::plugins::applications::macos_app_from_arbitrary_path, - crate::plugins::js::plugins::applications::macos_app_from_path, - crate::plugins::js::plugins::applications::macos_open_application, - ], - esm_entry_point = "ext:gauntlet/internal-macos/bootstrap.js", - esm = [ - "ext:gauntlet/internal-macos/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-macos-bootstrap.js", - "ext:gauntlet/internal-macos.js" = "../../js/core/dist/internal-macos.js", - ] -); - -#[op2(async)] -#[serde] -pub async fn op_plugin_get_pending_event(state: Rc>) -> anyhow::Result { - let event_stream = { - state.borrow() - .borrow::() - .event_stream - .clone() - }; - - let mut event_stream = event_stream.borrow_mut(); - let event = event_stream.next() - .await - .ok_or_else(|| anyhow!("event stream was suddenly closed"))?; - - tracing::trace!(target = "renderer_rs", "Received plugin event {:?}", event); - - Ok(from_intermediate_to_js_event(event)) -} - -fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { - match event { - IntermediateUiEvent::OpenView { entrypoint_id } => JsUiEvent::OpenView { - entrypoint_id: entrypoint_id.to_string(), - }, - IntermediateUiEvent::CloseView => JsUiEvent::CloseView, - IntermediateUiEvent::RunCommand { entrypoint_id } => JsUiEvent::RunCommand { - entrypoint_id - }, - IntermediateUiEvent::RunGeneratedCommand { entrypoint_id, action_index } => JsUiEvent::RunGeneratedCommand { - entrypoint_id, - action_index, - }, - IntermediateUiEvent::HandleViewEvent { widget_id, event_name, event_arguments } => { - let event_arguments = event_arguments.into_iter() - .map(|arg| match arg { - UiPropertyValue::String(value) => JsUiPropertyValue::String { value }, - UiPropertyValue::Number(value) => JsUiPropertyValue::Number { value }, - UiPropertyValue::Bool(value) => JsUiPropertyValue::Bool { value }, - UiPropertyValue::Undefined => JsUiPropertyValue::Undefined, - UiPropertyValue::Array(_) | UiPropertyValue::Bytes(_) | UiPropertyValue::Object(_) => { - todo!() - } - }) - .collect(); - - JsUiEvent::ViewEvent { - widget_id, - event_name, - event_arguments, - } - } - IntermediateUiEvent::HandleKeyboardEvent { entrypoint_id, origin, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { - JsUiEvent::KeyboardEvent { - entrypoint_id: entrypoint_id.to_string(), - origin: match origin { - KeyboardEventOrigin::MainView => JsKeyboardEventOrigin::MainView, - KeyboardEventOrigin::PluginView => JsKeyboardEventOrigin::PluginView, - }, - key: key.to_value(), - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta - } - } - IntermediateUiEvent::OpenInlineView { text } => JsUiEvent::OpenInlineView { text }, - IntermediateUiEvent::ReloadSearchIndex => JsUiEvent::ReloadSearchIndex, - IntermediateUiEvent::RefreshSearchIndex => JsUiEvent::RefreshSearchIndex, - } -} - -pub struct PluginData { - plugin_id: PluginId, - plugin_uuid: String, - plugin_cache_dir: String, - plugin_data_dir: String, - inline_view_entrypoint_id: Option, - home_dir: PathBuf, -} - -impl PluginData { - fn new( - plugin_id: PluginId, - plugin_uuid: String, - plugin_cache_dir: String, - plugin_data_dir: String, - inline_view_entrypoint_id: Option, - home_dir: PathBuf, - ) -> Self { - Self { - plugin_id, - plugin_uuid, - plugin_cache_dir, - plugin_data_dir, - inline_view_entrypoint_id, - home_dir - } - } - - fn plugin_id(&self) -> PluginId { - self.plugin_id.clone() - } - - fn plugin_uuid(&self) -> &str { - &self.plugin_uuid - } - - fn plugin_cache_dir(&self) -> &str { - &self.plugin_cache_dir - } - - fn plugin_data_dir(&self) -> &str { - &self.plugin_data_dir - } - - fn inline_view_entrypoint_id(&self) -> Option { - self.inline_view_entrypoint_id.clone() - } - - fn home_dir(&self) -> PathBuf { - self.home_dir.clone() - } -} - - - -pub struct EventReceiver { - event_stream: Rc>>>>, -} - -impl EventReceiver { - fn new(event_stream: Pin>>) -> EventReceiver { - Self { - event_stream: Rc::new(RefCell::new(event_stream)), - } - } -} - -#[derive(Clone)] -pub struct BackendForPluginRuntimeApiImpl { - icon_cache: IconCache, - repository: DataDbRepository, - search_index: SearchIndex, - clipboard: Clipboard, - frontend_api: FrontendApi, - plugin_uuid: String, - plugin_id: PluginId, - plugin_name: String, - entrypoint_names: HashMap, - permissions: PluginRuntimePermissions -} - -impl BackendForPluginRuntimeApiImpl { - fn new( - icon_cache: IconCache, - repository: DataDbRepository, - search_index: SearchIndex, - clipboard: Clipboard, - frontend_api: FrontendApi, - plugin_uuid: String, - plugin_id: PluginId, - plugin_name: String, - entrypoint_names: HashMap, - permissions: PluginRuntimePermissions - ) -> Self { - Self { - icon_cache, - repository, - search_index, - clipboard, - frontend_api, - plugin_uuid, - plugin_id, - plugin_name, - entrypoint_names, - permissions - } - } -} - -impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { - async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { - self.icon_cache.clear_plugin_icon_cache_dir(&self.plugin_uuid) - .context("error when clearing up icon cache before recreating it")?; - - let DbReadPlugin { name, .. } = self.repository.get_plugin_by_id(&self.plugin_id.to_string()) - .await - .context("error when getting plugin by id")?; - - let entrypoints = self.repository.get_entrypoints_by_plugin_id(&self.plugin_id.to_string()) - .await - .context("error when getting entrypoints by plugin id")?; - - let frecency_map = self.repository.get_frecency_for_plugin(&self.plugin_id.to_string()) - .await - .context("error when getting frecency for plugin")?; - - let mut shortcuts = HashMap::new(); - - for DbReadPluginEntrypoint { id, .. } in &entrypoints { - let entrypoint_shortcuts = self.repository.action_shortcuts(&self.plugin_id.to_string(), id).await?; - shortcuts.insert(id.clone(), entrypoint_shortcuts); - } - - let mut plugins_search_items = generated_commands.into_iter() - .map(|item| { - let entrypoint_icon_path = match item.entrypoint_icon { - None => None, - Some(data) => Some(self.icon_cache.save_entrypoint_icon_to_cache(&self.plugin_uuid, &item.entrypoint_uuid, &data)?), - }; - - let entrypoint_frecency = frecency_map.get(&item.entrypoint_id).cloned().unwrap_or(0.0); - - let shortcuts = shortcuts - .get(&item.generator_entrypoint_id); - - let entrypoint_actions = item.entrypoint_actions.iter() - .map(|action| { - let shortcut = match (shortcuts, &action.id) { - (Some(shortcuts), Some(id)) => { - shortcuts.get(id).cloned() - } - _ => None - }; - - SearchIndexItemAction { - label: action.label.clone(), - shortcut, - } - }) - .collect(); - - Ok(SearchIndexItem { - entrypoint_type: SearchResultEntrypointType::GeneratedCommand, - entrypoint_id: EntrypointId::from_string(item.entrypoint_id), - entrypoint_name: item.entrypoint_name, - entrypoint_icon_path, - entrypoint_frecency, - entrypoint_actions, - }) - }) - .collect::>>()?; - - let mut icon_asset_data = HashMap::new(); - - for entrypoint in &entrypoints { - if let Some(path_to_asset) = &entrypoint.icon_path { - let result = self.repository.get_asset_data(&self.plugin_id.to_string(), path_to_asset) - .await; - - if let Ok(data) = result { - icon_asset_data.insert((entrypoint.id.clone(), path_to_asset.clone()), data); - } - } - } - - let mut builtin_search_items = entrypoints.into_iter() - .filter(|entrypoint| entrypoint.enabled) - .map(|entrypoint| { - let entrypoint_type = db_entrypoint_from_str(&entrypoint.entrypoint_type); - let entrypoint_id = entrypoint.id.to_string(); - - let entrypoint_frecency = frecency_map.get(&entrypoint_id).cloned().unwrap_or(0.0); - - let entrypoint_icon_path = match entrypoint.icon_path { - None => None, - Some(path_to_asset) => { - match icon_asset_data.get(&(entrypoint.id, path_to_asset)) { - None => None, - Some(data) => Some(self.icon_cache.save_entrypoint_icon_to_cache(&self.plugin_uuid, &entrypoint.uuid, data)?) - } - }, - }; - - let entrypoint_id = EntrypointId::from_string(entrypoint_id); - - match &entrypoint_type { - DbPluginEntrypointType::Command => { - Ok(Some(SearchIndexItem { - entrypoint_type: SearchResultEntrypointType::Command, - entrypoint_name: entrypoint.name, - entrypoint_id, - entrypoint_icon_path, - entrypoint_frecency, - entrypoint_actions: vec![], - })) - }, - DbPluginEntrypointType::View => { - Ok(Some(SearchIndexItem { - entrypoint_type: SearchResultEntrypointType::View, - entrypoint_name: entrypoint.name, - entrypoint_id, - entrypoint_icon_path, - entrypoint_frecency, - entrypoint_actions: vec![], - })) - }, - DbPluginEntrypointType::CommandGenerator | DbPluginEntrypointType::InlineView => { - Ok(None) - } - } - }) - .collect::>>()? - .into_iter() - .flat_map(|item| item) - .collect::>(); - - plugins_search_items.append(&mut builtin_search_items); - - self.search_index.save_for_plugin(self.plugin_id.clone(), name, plugins_search_items, refresh_search_list) - .context("error when updating search index")?; - - Ok(()) - } - - async fn get_asset_data(&self, path: &str) -> anyhow::Result> { - let data = self.repository.get_asset_data(&self.plugin_id.to_string(), &path) - .await?; - - Ok(data) - } - - async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result> { - let result = self.repository.get_entrypoints_by_plugin_id(&self.plugin_id.to_string()).await? - .into_iter() - .filter(|entrypoint| entrypoint.enabled) - .filter(|entrypoint| matches!(db_entrypoint_from_str(&entrypoint.entrypoint_type), DbPluginEntrypointType::CommandGenerator)) - .map(|entrypoint| entrypoint.id) - .collect::>(); - - Ok(result) - } - - async fn get_plugin_preferences(&self) -> anyhow::Result> { - let DbReadPlugin { preferences, preferences_user_data, .. } = self.repository - .get_plugin_by_id(&self.plugin_id.to_string()) - .await?; - - Ok(preferences_to_js(preferences, preferences_user_data)) - } - - async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result> { - let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = self.repository - .get_entrypoint_by_id(&self.plugin_id.to_string(), &entrypoint_id.to_string()) - .await?; - - Ok(preferences_to_js(preferences, preferences_user_data)) - } - - async fn plugin_preferences_required(&self) -> anyhow::Result { - let DbReadPlugin { preferences, preferences_user_data, .. } = self.repository - .get_plugin_by_id(&self.plugin_id.to_string()).await?; - - Ok(any_preferences_missing_value(preferences, preferences_user_data)) - } - - async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result { - let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = self.repository - .get_entrypoint_by_id(&self.plugin_id.to_string(), &entrypoint_id.to_string()).await?; - - Ok(any_preferences_missing_value(preferences, preferences_user_data)) - } - - async fn clipboard_read(&self) -> anyhow::Result { - let allow = self - .permissions - .clipboard - .contains(&PluginPermissionsClipboard::Read); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); - } - - tracing::debug!("Reading from clipboard, plugin id: {:?}", self.plugin_id); - - self.clipboard.read() - } - - async fn clipboard_read_text(&self) -> anyhow::Result> { - let allow = self - .permissions - .clipboard - .contains(&PluginPermissionsClipboard::Read); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); - } - - tracing::debug!("Reading text from clipboard, plugin id: {:?}", self.plugin_id); - - self.clipboard.read_text() - } - - async fn clipboard_write(&self, data: ClipboardData) -> anyhow::Result<()> { - let allow = self - .permissions - .clipboard - .contains(&PluginPermissionsClipboard::Write); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); - } - - tracing::debug!("Writing to clipboard, plugin id: {:?}", self.plugin_id); - - self.clipboard.write(data) - } - - async fn clipboard_write_text(&self, data: String) -> anyhow::Result<()> { - let allow = self - .permissions - .clipboard - .contains(&PluginPermissionsClipboard::Write); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); - } - - tracing::debug!("Writing text to clipboard, plugin id: {:?}", self.plugin_id); - - self.clipboard.write_text(data) - } - - async fn clipboard_clear(&self) -> anyhow::Result<()> { - let allow = self - .permissions - .clipboard - .contains(&PluginPermissionsClipboard::Clear); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'clear' permission for clipboard")); - } - - tracing::debug!("Clearing clipboard, plugin id: {:?}", self.plugin_id); - - self.clipboard.clear() - } - - async fn ui_update_loading_bar(&self, entrypoint_id: EntrypointId, show: bool) -> anyhow::Result<()> { - self.frontend_api.update_loading_bar(self.plugin_id.clone(), entrypoint_id, show).await?; - - Ok(()) - } - - async fn ui_show_hud(&self, display: String) -> anyhow::Result<()> { - self.frontend_api.show_hud(display).await?; - - Ok(()) - } - - async fn ui_get_action_id_for_shortcut( - &self, - entrypoint_id: EntrypointId, - key: String, - modifier_shift: bool, - modifier_control: bool, - modifier_alt: bool, - modifier_meta: bool - ) -> anyhow::Result> { - let result = self.repository.get_action_id_for_shortcut( - &self.plugin_id.to_string(), - &entrypoint_id.to_string(), - PhysicalKey::from_value(key), - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta - ).await?; - - Ok(result) - } - - async fn ui_render( - &self, - entrypoint_id: EntrypointId, - render_location: UiRenderLocation, - top_level_view: bool, - container: RootWidget, - #[cfg(feature = "scenario_runner")] - container_value: serde_value::Value, - images: HashMap - ) -> anyhow::Result<()> { - - let entrypoint_name = self.entrypoint_names - .get(&entrypoint_id) - .expect("entrypoint name for id should always exist") - .to_string(); - - self.frontend_api.replace_view( - self.plugin_id.clone(), - self.plugin_name.clone(), - entrypoint_id, - entrypoint_name, - render_location, - top_level_view, - container, - #[cfg(feature = "scenario_runner")] - container_value, - images - ).await?; - - Ok(()) - } - - async fn ui_show_plugin_error_view( - &self, - entrypoint_id: EntrypointId, - render_location: UiRenderLocation - ) -> anyhow::Result<()> { - self.frontend_api.show_plugin_error_view( - self.plugin_id.clone(), - entrypoint_id, - render_location - ).await?; - - Ok(()) - } - - async fn ui_show_preferences_required_view( - &self, - entrypoint_id: EntrypointId, - plugin_preferences_required: bool, - entrypoint_preferences_required: bool - ) -> anyhow::Result<()> { - - self.frontend_api.show_preference_required_view( - self.plugin_id.clone(), - entrypoint_id, - plugin_preferences_required, - entrypoint_preferences_required - ).await?; - - Ok(()) - } - - async fn ui_clear_inline_view(&self) -> anyhow::Result<()> { - self.frontend_api.clear_inline_view(self.plugin_id.clone()).await?; - - Ok(()) - } -} - - -fn preferences_to_js( - preferences: HashMap, - mut preferences_user_data: HashMap -) -> HashMap { - preferences.into_iter() - .map(|(name, preference)| { - let user_data = match preferences_user_data.remove(&name) { - None => match preference { - DbPluginPreference::Number { default, .. } => PreferenceUserData::Number(default.expect("at this point preference should always have value")), - DbPluginPreference::String { default, .. } => PreferenceUserData::String(default.expect("at this point preference should always have value")), - DbPluginPreference::Enum { default, .. } => PreferenceUserData::String(default.expect("at this point preference should always have value")), - DbPluginPreference::Bool { default, .. } => PreferenceUserData::Bool(default.expect("at this point preference should always have value")), - DbPluginPreference::ListOfStrings { default, .. } => PreferenceUserData::ListOfStrings(default.expect("at this point preference should always have value")), - DbPluginPreference::ListOfNumbers { default, .. } => PreferenceUserData::ListOfNumbers(default.expect("at this point preference should always have value")), - DbPluginPreference::ListOfEnums { default, .. } => PreferenceUserData::ListOfStrings(default.expect("at this point preference should always have value")), - } - Some(user_data) => match user_data { - DbPluginPreferenceUserData::Number { value } => PreferenceUserData::Number(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::String { value } => PreferenceUserData::String(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::Enum { value } => PreferenceUserData::String(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::Bool { value } => PreferenceUserData::Bool(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::ListOfStrings { value } => PreferenceUserData::ListOfStrings(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::ListOfNumbers { value } => PreferenceUserData::ListOfNumbers(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::ListOfEnums { value } => PreferenceUserData::ListOfStrings(value.expect("at this point preference should always have value")), - } - }; - - (name, user_data) - }) - .collect() -} - -fn any_preferences_missing_value(preferences: HashMap, preferences_user_data: HashMap) -> bool { - for (name, preference) in preferences { - match preferences_user_data.get(&name) { - None => { - let no_default = match preference { - DbPluginPreference::Number { default, .. } => default.is_none(), - DbPluginPreference::String { default, .. } => default.is_none(), - DbPluginPreference::Enum { default, .. } => default.is_none(), - DbPluginPreference::Bool { default, .. } => default.is_none(), - DbPluginPreference::ListOfStrings { default, .. } => default.is_none(), - DbPluginPreference::ListOfNumbers { default, .. } => default.is_none(), - DbPluginPreference::ListOfEnums { default, .. } => default.is_none(), - }; - - if no_default { - return true - } - } - Some(preference) => { - let no_value = match preference { - DbPluginPreferenceUserData::Number { value } => value.is_none(), - DbPluginPreferenceUserData::String { value } => value.is_none(), - DbPluginPreferenceUserData::Enum { value } => value.is_none(), - DbPluginPreferenceUserData::Bool { value } => value.is_none(), - DbPluginPreferenceUserData::ListOfStrings { value } => value.is_none(), - DbPluginPreferenceUserData::ListOfNumbers { value } => value.is_none(), - DbPluginPreferenceUserData::ListOfEnums { value } => value.is_none(), - }; - - if no_value { - return true - } - } - } - } - - false -} diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index 8e0e0b2..d31e643 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -6,7 +6,6 @@ use std::path::{Path, PathBuf}; use std::thread; use anyhow::{anyhow, Context}; -use deno_core::url; use include_dir::Dir; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -17,17 +16,16 @@ use regex::{Match, Regex}; use tracing_subscriber::fmt::format; use typed_path::{TypedPathBuf, Utf8TypedPath, Utf8UnixComponent, Utf8WindowsComponent, Utf8WindowsPrefix, Utf8WindowsPrefixComponent}; use common::model::{DownloadStatus, PluginId}; +use plugin_runtime::PERMISSIONS_VARIABLE_PATTERN; use crate::model::ActionShortcutKey; use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_to_str, db_plugin_type_to_str, DbCode, DbPluginAction, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbPluginType, DbPreferenceEnumValue, DbWritePlugin, DbWritePluginAssetData, DbWritePluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions, DbPluginPermissionsFileSystem, DbPluginPermissionsExec}; use crate::plugins::download_status::DownloadStatusHolder; -use crate::plugins::js::permissions::{PluginPermissionsExec, PluginPermissionsFileSystem}; pub struct PluginLoader { db_repository: DataDbRepository, download_status_holder: DownloadStatusHolder } -pub static VARIABLE_PATTERN: Lazy = Lazy::new(|| Regex::new(r"\{(?.+?):(?.+?)}").expect("invalid regex")); impl PluginLoader { pub fn new(db_repository: DataDbRepository) -> Self { @@ -425,7 +423,7 @@ impl PluginLoader { // TODO custom parser for fun? for better error reporting, that will include cross-platform path parser - let matches = VARIABLE_PATTERN.captures_iter(path).collect::>(); + let matches = PERMISSIONS_VARIABLE_PATTERN.captures_iter(path).collect::>(); let augmented_path = match matches.as_slice() { [] => path.to_owned(), [variable] => { @@ -463,9 +461,9 @@ impl PluginLoader { }; if windows_like_path { - VARIABLE_PATTERN.replace(path, "C:\\dummy-root").to_string() + PERMISSIONS_VARIABLE_PATTERN.replace(path, "C:\\dummy-root").to_string() } else { - VARIABLE_PATTERN.replace(path, "/dummy-root").to_string() + PERMISSIONS_VARIABLE_PATTERN.replace(path, "/dummy-root").to_string() } } [_, ..] => { diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 6268688..f0c3b5e 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -4,7 +4,6 @@ use std::sync::Mutex; use std::thread; use std::time::Duration; use anyhow::anyhow; -use deno_core::futures::channel::mpsc::Sender; use include_dir::{include_dir, Dir}; use tokio::runtime::Handle; @@ -13,13 +12,13 @@ use common::rpc::frontend_api::FrontendApi; use common::{settings_env_data_to_string, SettingsEnvData}; use utils::channel::RequestSender; use common::dirs::Dirs; -use crate::model::{ActionShortcutKey, JsKeyboardEventOrigin}; +use plugin_runtime::{JsPluginCode, JsPluginPermissions, JsPluginPermissionsExec, JsPluginPermissionsFileSystem, JsPluginPermissionsMainSearchBar}; +use crate::model::{ActionShortcutKey}; use crate::plugins::clipboard::Clipboard; use crate::plugins::config_reader::ConfigReader; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginActionShortcutKind, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginMainSearchBarPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; -use crate::plugins::js::{start_plugin_runtime, AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginRuntimeData}; -use crate::plugins::js::permissions::{PluginPermissions, PluginPermissionsClipboard, PluginPermissionsExec, PluginPermissionsFileSystem, PluginPermissionsMainSearchBar}; +use crate::plugins::js::{start_plugin_runtime, AllPluginCommandData, OnePluginCommandData, PluginCommand, PluginPermissions, PluginPermissionsClipboard, PluginRuntimeData}; use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; use crate::search::SearchIndex; @@ -34,6 +33,8 @@ mod download_status; mod icon_cache; pub(super) mod frecency; mod clipboard; +mod runtime; +mod image_gatherer; static BUNDLED_PLUGINS: [(&str, Dir); 1] = [ ("gauntlet", include_dir!("$CARGO_MANIFEST_DIR/../../bundled_plugins/gauntlet/dist")), @@ -470,7 +471,7 @@ impl ApplicationManager { .args(["settings"]) .env(SETTINGS_ENV, settings_env_data_to_string(data)) .spawn() - .expect("failed to execute settings process"); // this can fail in dev if binary was replaced by frontend compilation + .expect("failed to execute settings process"); // this can fail in dev if binary was replaced by more recent compilation } async fn reload_plugin(&self, plugin_id: PluginId) -> anyhow::Result<()> { @@ -530,7 +531,7 @@ impl ApplicationManager { .main_search_bar .into_iter() .map(|permission| match permission { - DbPluginMainSearchBarPermissions::Read => PluginPermissionsMainSearchBar::Read, + DbPluginMainSearchBarPermissions::Read => JsPluginPermissionsMainSearchBar::Read, }) .collect(); @@ -539,16 +540,16 @@ impl ApplicationManager { uuid: plugin.uuid, name: plugin.name, entrypoint_names, - code: PluginCode { js: plugin.code.js }, + code: JsPluginCode { js: plugin.code.js }, inline_view_entrypoint_id, permissions: PluginPermissions { environment: plugin.permissions.environment, network: plugin.permissions.network, - filesystem: PluginPermissionsFileSystem { + filesystem: JsPluginPermissionsFileSystem { read: plugin.permissions.filesystem.read, write: plugin.permissions.filesystem.write, }, - exec: PluginPermissionsExec { + exec: JsPluginPermissionsExec { command: plugin.permissions.exec.command, executable: plugin.permissions.exec.executable, }, diff --git a/rust/server/src/plugins/runtime.rs b/rust/server/src/plugins/runtime.rs new file mode 100644 index 0000000..e69de29 diff --git a/rust/utils/Cargo.toml b/rust/utils/Cargo.toml index ce43eb7..0d0cbca 100644 --- a/rust/utils/Cargo.toml +++ b/rust/utils/Cargo.toml @@ -5,3 +5,4 @@ edition = "2021" [dependencies] tokio = "1.28.1" thiserror = "1.0.48" +log = "0.4.22" diff --git a/rust/utils/src/channel.rs b/rust/utils/src/channel.rs index 6f4ed4c..aeb308c 100644 --- a/rust/utils/src/channel.rs +++ b/rust/utils/src/channel.rs @@ -1,11 +1,14 @@ use std::time::Duration; - +use thiserror::Error; use tokio::sync::{mpsc, oneshot}; use tokio::time::error::Elapsed; -#[derive(Debug)] +#[derive(Error, Debug)] pub enum RequestError { + #[error("request timeout")] TimeoutError, + #[error("other side was dropped")] + OtherSideWasDropped, } impl From for RequestError { @@ -51,16 +54,16 @@ impl RequestSender { } } - pub fn send(&self, request: Req) -> ResponseReceiver { + pub fn send(&self, request: Req) -> Result, RequestError> { let (response_sender, response_receiver) = oneshot::channel::(); let responder = Responder::new(response_sender); let payload = (request, responder); - self.request_sender.send(payload).expect("the other side is closed"); - ResponseReceiver::new(response_receiver) + self.request_sender.send(payload).map_err(|err| RequestError::OtherSideWasDropped)?; + Ok(ResponseReceiver::new(response_receiver)) } pub async fn send_receive(&self, request: Req) -> Result { - let mut receiver = self.send(request); + let mut receiver = self.send(request)?; let duration = Duration::from_secs(30); From b7517591896db5a6c17ad60839d4e1abe3bfee16 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:56:17 +0100 Subject: [PATCH 214/540] Fix main window closing on macOS because hud window took focus --- Cargo.lock | 26 +++++++++++++------------- rust/client/src/ui/hud/mod.rs | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 446b5d9..1f83ada 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3259,7 +3259,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "dpi" version = "0.1.1" -source = "git+https://github.com/project-gauntlet/winit.git?rev=ddf41619ffb9e1fc9fc157ca6ea99b81e62b0e5a#ddf41619ffb9e1fc9fc157ca6ea99b81e62b0e5a" +source = "git+https://github.com/project-gauntlet/winit.git?rev=e710490e0da949f68b40a4e9de2678ac77543f3e#e710490e0da949f68b40a4e9de2678ac77543f3e" [[package]] name = "dprint-swc-ext" @@ -5101,7 +5101,7 @@ dependencies = [ [[package]] name = "iced" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" dependencies = [ "iced_core", "iced_futures", @@ -5129,7 +5129,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" dependencies = [ "bitflags 2.6.0", "bytes", @@ -5156,7 +5156,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" dependencies = [ "futures", "iced_core", @@ -5170,7 +5170,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5222,7 +5222,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -5234,7 +5234,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" dependencies = [ "bytes", "iced_core", @@ -5255,7 +5255,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" dependencies = [ "bytemuck", "cosmic-text", @@ -5270,7 +5270,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5289,7 +5289,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" dependencies = [ "iced_renderer", "iced_runtime", @@ -5304,7 +5304,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#fac387a521a32e839b51ccadbfeb8dedee924dfc" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" dependencies = [ "iced_futures", "iced_graphics", @@ -12760,7 +12760,7 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" version = "0.30.99" -source = "git+https://github.com/project-gauntlet/winit.git?rev=ddf41619ffb9e1fc9fc157ca6ea99b81e62b0e5a#ddf41619ffb9e1fc9fc157ca6ea99b81e62b0e5a" +source = "git+https://github.com/project-gauntlet/winit.git?rev=e710490e0da949f68b40a4e9de2678ac77543f3e#e710490e0da949f68b40a4e9de2678ac77543f3e" dependencies = [ "ahash 0.8.11", "android-activity", @@ -12774,7 +12774,7 @@ dependencies = [ "core-foundation 0.9.4", "core-graphics 0.23.2", "cursor-icon", - "dpi 0.1.1 (git+https://github.com/project-gauntlet/winit.git?rev=ddf41619ffb9e1fc9fc157ca6ea99b81e62b0e5a)", + "dpi 0.1.1 (git+https://github.com/project-gauntlet/winit.git?rev=e710490e0da949f68b40a4e9de2678ac77543f3e)", "js-sys", "libc", "memmap2 0.9.5", diff --git a/rust/client/src/ui/hud/mod.rs b/rust/client/src/ui/hud/mod.rs index 940d80f..39030c8 100644 --- a/rust/client/src/ui/hud/mod.rs +++ b/rust/client/src/ui/hud/mod.rs @@ -33,7 +33,7 @@ fn open_non_wayland() -> Task { level: Level::AlwaysOnTop, #[cfg(target_os = "macos")] platform_specific: window::settings::PlatformSpecific { - window_kind: window::settings::WindowKind::Popup, + window_kind: window::settings::WindowKind::Panel, ..Default::default() }, exit_on_close_request: false, From a4451acae4c2f530547d9dc9dc578af615535408 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:03:22 +0100 Subject: [PATCH 215/540] Fix message loop in plugin runtime sometimes getting deadlocked --- rust/plugin_runtime/src/lib.rs | 13 +++++++++---- rust/server/src/plugins/js.rs | 2 ++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/rust/plugin_runtime/src/lib.rs b/rust/plugin_runtime/src/lib.rs index b251321..00139e6 100644 --- a/rust/plugin_runtime/src/lib.rs +++ b/rust/plugin_runtime/src/lib.rs @@ -211,9 +211,14 @@ async fn message_loop( JsMessage::Event(event) => { tracing::trace!("Received plugin event from backend {:?}", event); - event_sender - .send(event) - .await?; + let event_sender = event_sender.clone(); + + tokio::spawn(async move { + event_sender + .send(event) + .await + .expect("event receiver was dropped"); + }); Ok(()) } @@ -227,7 +232,7 @@ async fn message_loop( tracing::error!("Dropped oneshot receiving side"); } Ok(_) => { - tracing::trace!("Sending oneshot response..."); + tracing::trace!("Oneshot response sent"); } } } diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 30e4c07..f492ffa 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -415,6 +415,8 @@ async fn request_loop(recv: &mut RecvHalf, send: &Mutex, api: &Backend Ok(response) => { let mut send = send.lock().await; + tracing::trace!("Sending request response: {:?}", response); + send_message(JsMessageSide::Backend, &mut send, JsMessage::Response(Ok(response))).await?; Ok(()) From 84a598ddab1fb441d143c80b7ac6d0f051841378 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:03:53 +0100 Subject: [PATCH 216/540] Move hud window on all non-wayland platforms slightly lower --- rust/client/src/ui/hud/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rust/client/src/ui/hud/mod.rs b/rust/client/src/ui/hud/mod.rs index 39030c8..edb27e4 100644 --- a/rust/client/src/ui/hud/mod.rs +++ b/rust/client/src/ui/hud/mod.rs @@ -1,5 +1,5 @@ use iced::window::{Level, Position, Settings}; -use iced::{window, Size, Task}; +use iced::{window, Point, Size, Task}; use std::convert; use std::time::Duration; use crate::ui::AppMsg; @@ -25,7 +25,9 @@ pub fn show_hud_window( fn open_non_wayland() -> Task { let settings = Settings { size: Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT), - position: Position::Centered, + position: Position::SpecificWith(|window, screen| { + Point::new((screen.width - window.width) / 2.0, (screen.height - window.height) / 1.25) + }), resizable: false, decorations: false, transparent: true, From c5faf7f8128d1fb07d2939760f5b20143c4a2d5f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 19 Dec 2024 18:26:09 +0100 Subject: [PATCH 217/540] Actually fix occasional deadlock when spamming keys in UI --- rust/client/src/ui/client_context.rs | 5 +- rust/client/src/ui/mod.rs | 275 ++++++++++++------------- rust/client/src/ui/state/mod.rs | 92 +++------ rust/client/src/ui/widget.rs | 6 +- rust/client/src/ui/widget_container.rs | 5 +- 5 files changed, 173 insertions(+), 210 deletions(-) diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index 89589d9..6c46b67 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -5,6 +5,7 @@ use crate::ui::AppMsg; use common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiWidgetId}; use iced::Task; use std::collections::HashMap; +use std::sync::Arc; pub struct ClientContext { inline_views: Vec<(PluginId, PluginWidgetContainer)>, // Vec to have stable ordering @@ -75,10 +76,10 @@ impl ClientContext { self.view.get_entrypoint_id() } - pub fn replace_view( + pub fn render_ui( &mut self, render_location: UiRenderLocation, - container: RootWidget, + container: Arc, images: HashMap>, plugin_id: &PluginId, plugin_name: &str, diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 318a943..238987d 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -75,7 +75,7 @@ pub struct AppModel { prompt: String, // state - client_context: Arc>, + client_context: ClientContext, global_state: GlobalState, search_results: Vec, loading_bar_state: HashMap<(PluginId, EntrypointId), ()>, @@ -116,7 +116,17 @@ pub enum AppMsg { PromptSubmit, UpdateSearchResults, SetSearchResults(Vec), - ReplaceView { + RenderPluginUI { + plugin_id: PluginId, + plugin_name: String, + entrypoint_id: EntrypointId, + entrypoint_name: String, + render_location: UiRenderLocation, + top_level_view: bool, + container: Arc, + images: HashMap>, + }, + HandleRenderPluginUI { top_level_view: bool, has_children: bool, render_location: UiRenderLocation, @@ -196,7 +206,10 @@ pub enum AppMsg { widget_id: UiWidgetId }, #[cfg(target_os = "linux")] - LayerShell(layer_shell::LayerShellAppMsg) + LayerShell(layer_shell::LayerShellAppMsg), + ClearInlineView { + plugin_id: PluginId, + }, } #[cfg(target_os = "linux")] @@ -387,7 +400,7 @@ fn new( None }; - let (client_context, global_state) = if cfg!(feature = "scenario_runner") { + let global_state = if cfg!(feature = "scenario_runner") { let gen_in = std::env::var("GAUNTLET_SCREENSHOT_GEN_IN") .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_IN"); @@ -417,28 +430,23 @@ fn new( let plugin_id = PluginId::from_string("__SCREENSHOT_GEN___"); let entrypoint_id = EntrypointId::from_string(entrypoint_id); - let mut context = ClientContext::new(); - let render_location = ui_render_location_from_scenario(render_location); - let has_children = container.content.is_some(); - // ignore commands because screenshots are non-interactive - let _ = context.replace_view( + let msg = AppMsg::RenderPluginUI { + plugin_id: plugin_id.clone(), + plugin_name: "Screenshot Plugin".to_string(), + entrypoint_id: entrypoint_id.clone(), + entrypoint_name: "Screenshot Entrypoint".to_string(), render_location, - container, - images, - &plugin_id, - "Screenshot Plugin", - &entrypoint_id, - "Screenshot Entrypoint", - ); + top_level_view, + container: Arc::new(container), + images + }; - let context = Arc::new(StdRwLock::new(context)); + tasks.push(Task::done(msg)); - tasks.push(Task::perform(async { }, move |_| AppMsg::ReplaceView { top_level_view, has_children, render_location })); - - let state= match render_location { - UiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique(), context.clone()), + match render_location { + UiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique()), UiRenderLocation::View => GlobalState::new_plugin( PluginViewData { top_level_view, @@ -448,11 +456,8 @@ fn new( entrypoint_name: gen_name, action_shortcuts: Default::default(), }, - context.clone() ) - }; - - (context, state) + } } ScenarioFrontendEvent::ShowPreferenceRequiredView { entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { let error_view = ErrorViewData::PreferenceRequired { @@ -462,7 +467,7 @@ fn new( entrypoint_preferences_required, }; - (Arc::new(StdRwLock::new(ClientContext::new())), GlobalState::new_error(error_view)) + GlobalState::new_error(error_view) } ScenarioFrontendEvent::ShowPluginErrorView { entrypoint_id, render_location: _ } => { let error_view = ErrorViewData::PluginError { @@ -470,12 +475,11 @@ fn new( entrypoint_id: EntrypointId::from_string(entrypoint_id), }; - (Arc::new(StdRwLock::new(ClientContext::new())), GlobalState::new_error(error_view)) + GlobalState::new_error(error_view) } } } else { - let context = Arc::new(StdRwLock::new(ClientContext::new())); - (context.clone(), GlobalState::new(text_input::Id::unique(), context.clone())) + GlobalState::new(text_input::Id::unique()) }; ( @@ -496,7 +500,7 @@ fn new( // state global_state, - client_context, + client_context: ClientContext::new(), search_results: vec![], loading_bar_state: HashMap::new(), hud_display: None, @@ -631,14 +635,47 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } AppMsg::PromptSubmit => { - state.global_state.primary(&state.search_results) + state.global_state.primary(&state.client_context, &state.search_results) }, AppMsg::SetSearchResults(new_search_results) => { state.search_results = new_search_results; Task::none() } - AppMsg::ReplaceView { top_level_view, render_location, has_children } => { + AppMsg::RenderPluginUI { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + render_location, + top_level_view, + container, + images + } => { + let has_children = container.content.is_some(); + + Task::batch([ + Task::done(state.client_context.render_ui( + render_location, + container, + images, + &plugin_id, + &plugin_name, + &entrypoint_id, + &entrypoint_name, + )), + Task::done(AppMsg::HandleRenderPluginUI { + top_level_view, + has_children, + render_location, + }) + ]) + } + AppMsg::HandleRenderPluginUI { + top_level_view, + has_children, + render_location + } => { match &mut state.global_state { GlobalState::MainView { pending_plugin_view_data, focused_search_result, pending_plugin_view_loading_bar, .. } => { @@ -662,7 +699,6 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { top_level_view, ..pending_plugin_view_data }, - state.client_context.clone() ) } }; @@ -697,22 +733,22 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { keyboard::Event::KeyPressed { key, modifiers, physical_key, text, .. } => { tracing::debug!("Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", key, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); match key { - Key::Named(Named::ArrowUp) => state.global_state.up(&state.search_results), - Key::Named(Named::ArrowDown) => state.global_state.down(&state.search_results), - Key::Named(Named::ArrowLeft) => state.global_state.left(&state.search_results), - Key::Named(Named::ArrowRight) => state.global_state.right(&state.search_results), - Key::Named(Named::Escape) => state.global_state.back(), - Key::Named(Named::Tab) if !modifiers.shift() => state.global_state.next(), - Key::Named(Named::Tab) if modifiers.shift() => state.global_state.previous(), + Key::Named(Named::ArrowUp) => state.global_state.up(&state.client_context, &state.search_results), + Key::Named(Named::ArrowDown) => state.global_state.down(&state.client_context, &state.search_results), + Key::Named(Named::ArrowLeft) => state.global_state.left(&state.client_context, &state.search_results), + Key::Named(Named::ArrowRight) => state.global_state.right(&state.client_context, &state.search_results), + Key::Named(Named::Escape) => state.global_state.back(&state.client_context), + Key::Named(Named::Tab) if !modifiers.shift() => state.global_state.next(&state.client_context), + Key::Named(Named::Tab) if modifiers.shift() => state.global_state.previous(&state.client_context), Key::Named(Named::Enter) => { if modifiers.logo() || modifiers.alt() || modifiers.control() { Task::none() // to avoid not wanted "enter" presses } else { if modifiers.shift() { // for main view, also fired in cases where main text field is not focused - state.global_state.secondary(&state.search_results) + state.global_state.secondary(&state.client_context, &state.search_results) } else { - state.global_state.primary(&state.search_results) + state.global_state.primary(&state.client_context, &state.search_results) } } }, @@ -729,9 +765,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { GlobalState::PluginView { sub_state, .. } => { match sub_state { PluginViewState::None => { - let mut client_context = state.client_context.write().expect("lock is poisoned"); - - client_context.backspace_text() + state.client_context.backspace_text() } PluginViewState::ActionPanel { .. } => Task::none() } @@ -852,9 +886,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { match text { None => Task::none(), Some(text) => { - let mut client_context = state.client_context.write().expect("lock is poisoned"); - - client_context.append_text(text.as_str()) + state.client_context.append_text(text.as_str()) } } } @@ -900,7 +932,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } AppMsg::IcedEvent(_, _) => Task::none(), AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::Noop, .. } => Task::none(), - AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => state.global_state.back(), + AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => state.global_state.back(&state.client_context), AppMsg::WidgetEvent { widget_event, plugin_id, render_location } => { state.handle_plugin_event(widget_event, plugin_id, render_location) } @@ -1020,8 +1052,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { MainViewState::search_result_action_panel(sub_state, keyboard); } } else { - let client_context = state.client_context.read().expect("lock is poisoned"); - if let Some(_) = client_context.get_first_inline_view_container() { + if let Some(_) = state.client_context.get_first_inline_view_container() { MainViewState::inline_result_action_panel(sub_state, keyboard); } } @@ -1036,9 +1067,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } GlobalState::ErrorView { .. } => { }, GlobalState::PluginView { sub_state, .. } => { - let client_context = state.client_context.read().expect("lock is poisoned"); - - client_context.toggle_action_panel(); + state.client_context.toggle_action_panel(); match sub_state { PluginViewState::None => { @@ -1078,9 +1107,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { ]) } AppMsg::OnAnyActionMainViewInlineViewPanelKeyboardWithFocus { widget_id } => { - let client_context = state.client_context.read().expect("lock is poisoned"); - - match client_context.get_first_inline_view_container() { + match state.client_context.get_first_inline_view_container() { Some(container) => { let plugin_id = container.get_plugin_id(); @@ -1100,13 +1127,11 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::done(AppMsg::OnAnyActionPluginViewAnyPanel { widget_id }) } AppMsg::OnAnyActionPluginViewAnyPanelKeyboardWithFocus { widget_id } => { - let client_context = state.client_context.read().expect("lock is poisoned"); - Task::batch([ Task::done(AppMsg::ToggleActionPanel { keyboard: true }), Task::done(AppMsg::RunPluginAction { render_location: UiRenderLocation::View, - plugin_id: client_context.get_view_plugin_id(), + plugin_id: state.client_context.get_view_plugin_id(), widget_id, }) ]) @@ -1127,9 +1152,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index } => { - let client_context = state.client_context.read().expect("lock is poisoned"); - - if let Some(container) = client_context.get_first_inline_view_container() { + if let Some(container) = state.client_context.get_first_inline_view_container() { let plugin_id = container.get_plugin_id(); let action_ids = container.get_action_ids(); @@ -1172,11 +1195,9 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } AppMsg::OnAnyActionPluginViewAnyPanel { widget_id } => { - let client_context = state.client_context.read().expect("lock is poisoned"); - Task::done(AppMsg::RunPluginAction { render_location: UiRenderLocation::View, - plugin_id: client_context.get_view_plugin_id(), + plugin_id: state.client_context.get_view_plugin_id(), widget_id, }) } @@ -1187,9 +1208,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { state.close_plugin_view(plugin_id) } AppMsg::InlineViewShortcuts { shortcuts } => { - let mut client_context = state.client_context.write().expect("lock is poisoned"); - - client_context.set_inline_view_shortcuts(shortcuts); + state.client_context.set_inline_view_shortcuts(shortcuts); Task::none() } @@ -1288,15 +1307,18 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } AppMsg::FocusPluginViewSearchBar { widget_id } => { - let mut client_context = state.client_context.write().expect("lock is poisoned"); - - client_context.focus_search_bar(widget_id) + state.client_context.focus_search_bar(widget_id) } #[cfg(target_os = "linux")] AppMsg::LayerShell(_) => { // handled by library Task::none() } + AppMsg::ClearInlineView { plugin_id } => { + state.client_context.clear_inline_view(&plugin_id); + + Task::none() + } } } @@ -1603,9 +1625,7 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { .into() }; - let client_context = state.client_context.read().expect("lock is poisoned"); - - let inline_view = match client_context.get_all_inline_view_containers().first() { + let inline_view = match state.client_context.get_all_inline_view_containers().first() { Some((plugin_id, container)) => { let plugin_id = plugin_id.clone(); container.render_inline_root_widget() @@ -1688,9 +1708,7 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { (Some((label, primary_action_widget_id, default_shortcut)), Some(action_panel)) } } else { - let client_context = state.client_context.read().expect("lock is poisoned"); - - match client_context.get_first_inline_view_action_panel() { + match state.client_context.get_first_inline_view_action_panel() { None => (None, None), Some(action_panel) => { match action_panel.find_first() { @@ -1781,8 +1799,7 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { GlobalState::PluginView { plugin_view_data, sub_state, .. } => { let PluginViewData { plugin_id, action_shortcuts, .. } = plugin_view_data; - let client_context = state.client_context.read().expect("lock is poisoned"); - let view_container = client_context.get_view_container(); + let view_container = state.client_context.get_view_container(); let container_element = view_container .render_root_widget(sub_state, action_shortcuts) @@ -1805,7 +1822,6 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { } fn subscription(state: &AppModel) -> Subscription { - let client_context = state.client_context.clone(); let frontend_receiver = state.frontend_receiver.clone(); struct RequestLoop; @@ -1839,7 +1855,7 @@ fn subscription(state: &AppModel) -> Subscription { stream::channel( 100, |sender| async move { - request_loop(client_context, frontend_receiver, sender).await; + request_loop(frontend_receiver, sender).await; panic!("request_rx was unexpectedly closed") }, @@ -1922,11 +1938,9 @@ impl AppModel { fn reset_window_state(&mut self) -> Task { self.prompt = "".to_string(); - let mut client_context = self.client_context.write().expect("lock is poisoned"); + self.client_context.clear_all_inline_views(); - client_context.clear_all_inline_views(); - - GlobalState::initial(&mut self.global_state, self.client_context.clone()) + GlobalState::initial(&mut self.global_state) } fn open_plugin_view(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Task { @@ -1975,14 +1989,10 @@ impl AppModel { fn handle_plugin_event(&self, widget_event: ComponentWidgetEvent, plugin_id: PluginId, render_location: UiRenderLocation) -> Task { let mut backend_client = self.backend_api.clone(); - let client_context = self.client_context.clone(); + + let event = self.client_context.handle_event(render_location, &plugin_id, widget_event.clone()); Task::perform(async move { - let event = { - let client_context = client_context.read().expect("lock is poisoned"); - client_context.handle_event(render_location, &plugin_id, widget_event.clone()) - }; - if let Some(event) = event { match event { UiViewEvent::View { widget_id, event_name, event_arguments } => { @@ -2028,8 +2038,7 @@ impl AppModel { let mut backend_client = self.backend_api.clone(); let (plugin_id, entrypoint_id) = { - let client_context = self.client_context.read().expect("lock is poisoned"); - (client_context.get_view_plugin_id(), client_context.get_view_entrypoint_id()) + (self.client_context.get_view_plugin_id(), self.client_context.get_view_entrypoint_id()) }; Task::perform( @@ -2047,8 +2056,7 @@ impl AppModel { let mut backend_client = self.backend_api.clone(); let (plugin_id, entrypoint_id) = { - let client_context = self.client_context.read().expect("lock is poisoned"); - match client_context.get_first_inline_view_container() { + match self.client_context.get_first_inline_view_container() { None => { return Task::none() }, @@ -2136,7 +2144,6 @@ fn handle_backend_error(result: Result, conver } async fn request_loop( - client_context: Arc>, frontend_receiver: Arc>>, mut sender: Sender, ) { @@ -2144,7 +2151,7 @@ async fn request_loop( loop { let (request_data, responder) = frontend_receiver.recv().await; - let app_msgs = { + let app_msg = { match request_data { UiRequestData::ReplaceView { plugin_id, @@ -2156,48 +2163,30 @@ async fn request_loop( container, images } => { - let has_children = container.content.is_some(); - - let message = { - let mut client_context = client_context.write().expect("lock is poisoned"); - - client_context.replace_view( - render_location, - container, - images, - &plugin_id, - &plugin_name, - &entrypoint_id, - &entrypoint_name - ) - }; - responder.respond(UiResponseData::Nothing); - vec![ - AppMsg::ReplaceView { - top_level_view, - has_children, - render_location, - }, - message - ] + AppMsg::RenderPluginUI { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + render_location, + top_level_view, + container: Arc::new(container), + images + } } UiRequestData::ClearInlineView { plugin_id } => { - { - let mut client_context = client_context.write().expect("lock is poisoned"); - - client_context.clear_inline_view(&plugin_id); - } - responder.respond(UiResponseData::Nothing); - vec![AppMsg::Noop] // refresh ui + AppMsg::ClearInlineView { + plugin_id + } } UiRequestData::ShowWindow => { responder.respond(UiResponseData::Nothing); - vec![AppMsg::ShowWindow] + AppMsg::ShowWindow } UiRequestData::ShowPreferenceRequiredView { plugin_id, @@ -2207,54 +2196,52 @@ async fn request_loop( } => { responder.respond(UiResponseData::Nothing); - vec![AppMsg::ShowPreferenceRequiredView { + AppMsg::ShowPreferenceRequiredView { plugin_id, entrypoint_id, plugin_preferences_required, entrypoint_preferences_required - }] + } } UiRequestData::ShowPluginErrorView { plugin_id, entrypoint_id, render_location } => { responder.respond(UiResponseData::Nothing); - vec![AppMsg::ShowPluginErrorView { + AppMsg::ShowPluginErrorView { plugin_id, entrypoint_id, render_location, - }] + } } UiRequestData::RequestSearchResultUpdate => { responder.respond(UiResponseData::Nothing); - vec![AppMsg::UpdateSearchResults] + AppMsg::UpdateSearchResults } UiRequestData::ShowHud { display } => { responder.respond(UiResponseData::Nothing); - vec![AppMsg::ShowHud { + AppMsg::ShowHud { display - }] + } } UiRequestData::SetGlobalShortcut { shortcut } => { - vec![AppMsg::SetGlobalShortcut { + AppMsg::SetGlobalShortcut { shortcut, responder: Arc::new(Mutex::new(Some(responder))) - }] + } } UiRequestData::UpdateLoadingBar { plugin_id, entrypoint_id, show } => { responder.respond(UiResponseData::Nothing); - vec![AppMsg::UpdateLoadingBar { + AppMsg::UpdateLoadingBar { plugin_id, entrypoint_id, show - }] + } } } }; - for app_msg in app_msgs { - let _ = sender.send(app_msg).await; - } + let _ = sender.send(app_msg).await; } } diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 8a69809..39e6121 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -11,7 +11,6 @@ use iced::widget::text_input; use iced::widget::text_input::focus; use iced::Task; use std::collections::HashMap; -use std::sync::{Arc, RwLock as StdRwLock}; pub enum GlobalState { MainView { @@ -22,7 +21,6 @@ pub enum GlobalState { focused_search_result: ScrollHandle, // state - client_context: Arc>, sub_state: MainViewState, pending_plugin_view_data: Option, pending_plugin_view_loading_bar: LoadingBarState, @@ -31,9 +29,6 @@ pub enum GlobalState { error_view: ErrorViewData, }, PluginView { - // logic - client_context: Arc>, - // state plugin_view_data: PluginViewData, sub_state: PluginViewState, @@ -76,13 +71,12 @@ pub enum LoadingBarState { impl GlobalState { - pub fn new(search_field_id: text_input::Id, client_context: Arc>) -> GlobalState { + pub fn new(search_field_id: text_input::Id) -> GlobalState { GlobalState::MainView { search_field_id, focused_search_result: ScrollHandle::new(true, ESTIMATED_MAIN_LIST_ITEM_HEIGHT, 7), sub_state: MainViewState::new(), pending_plugin_view_data: None, - client_context, pending_plugin_view_loading_bar: LoadingBarState::Off, } } @@ -93,22 +87,21 @@ impl GlobalState { } } - pub fn new_plugin(plugin_view_data: PluginViewData, client_context: Arc>) -> GlobalState { + pub fn new_plugin(plugin_view_data: PluginViewData) -> GlobalState { GlobalState::PluginView { - client_context, plugin_view_data, sub_state: PluginViewState::new(), } } - pub fn initial(prev_global_state: &mut GlobalState, client_context: Arc>) -> Task { + pub fn initial(prev_global_state: &mut GlobalState) -> Task { let search_field_id = text_input::Id::unique(); - *prev_global_state = GlobalState::new(search_field_id.clone(), client_context); + *prev_global_state = GlobalState::new(search_field_id.clone()); Task::batch([ focus(search_field_id), - Task::perform(async {}, |_| AppMsg::UpdateSearchResults), + Task::done(AppMsg::UpdateSearchResults), ]) } @@ -118,27 +111,27 @@ impl GlobalState { Task::none() } - pub fn plugin(prev_global_state: &mut GlobalState, plugin_view_data: PluginViewData, client_context: Arc>) -> Task { - *prev_global_state = GlobalState::new_plugin(plugin_view_data, client_context); + pub fn plugin(prev_global_state: &mut GlobalState, plugin_view_data: PluginViewData) -> Task { + *prev_global_state = GlobalState::new_plugin(plugin_view_data); Task::none() } } pub trait Focus { - fn primary(&mut self, focus_list: &[T]) -> Task; - fn secondary(&mut self, focus_list: &[T]) -> Task; - fn back(&mut self) -> Task; - fn next(&mut self) -> Task; - fn previous(&mut self) -> Task; - fn up(&mut self, focus_list: &[T]) -> Task; - fn down(&mut self, focus_list: &[T]) -> Task; - fn left(&mut self, focus_list: &[T]) -> Task; - fn right(&mut self, focus_list: &[T]) -> Task; + fn primary(&mut self, client_context: &ClientContext, focus_list: &[T]) -> Task; + fn secondary(&mut self, client_context: &ClientContext, focus_list: &[T]) -> Task; + fn back(&mut self, client_context: &ClientContext) -> Task; + fn next(&mut self, client_context: &ClientContext) -> Task; + fn previous(&mut self, client_context: &ClientContext) -> Task; + fn up(&mut self, client_context: &ClientContext, focus_list: &[T]) -> Task; + fn down(&mut self, client_context: &ClientContext, focus_list: &[T]) -> Task; + fn left(&mut self, client_context: &ClientContext, focus_list: &[T]) -> Task; + fn right(&mut self, client_context: &ClientContext, focus_list: &[T]) -> Task; } impl Focus for GlobalState { - fn primary(&mut self, focus_list: &[SearchResult]) -> Task { + fn primary(&mut self, client_context: &ClientContext, focus_list: &[SearchResult]) -> Task { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { @@ -173,9 +166,7 @@ impl Focus for GlobalState { } } } - GlobalState::PluginView { sub_state, client_context, .. } => { - let client_context = client_context.read().expect("lock is poisoned"); - + GlobalState::PluginView { sub_state, .. } => { let action_ids = client_context.get_action_ids(); match sub_state { @@ -201,7 +192,7 @@ impl Focus for GlobalState { } } - fn secondary(&mut self, focus_list: &[SearchResult]) -> Task { + fn secondary(&mut self, client_context: &ClientContext, focus_list: &[SearchResult]) -> Task { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { @@ -219,9 +210,7 @@ impl Focus for GlobalState { } } } - GlobalState::PluginView { sub_state, client_context, .. } => { - let client_context = client_context.read().expect("lock is poisoned"); - + GlobalState::PluginView { sub_state, .. } => { let action_ids = client_context.get_action_ids(); match sub_state { @@ -243,7 +232,7 @@ impl Focus for GlobalState { } } - fn back(&mut self) -> Task { + fn back(&mut self, _client_context: &ClientContext) -> Task { match self { GlobalState::MainView { sub_state, .. } => { match sub_state { @@ -268,7 +257,6 @@ impl Focus for GlobalState { .. }, sub_state, - client_context, .. } => { match sub_state { @@ -276,11 +264,9 @@ impl Focus for GlobalState { if *top_level_view { let plugin_id = plugin_id.clone(); - let client_context = client_context.clone(); - Task::batch([ Task::done(AppMsg::ClosePluginView(plugin_id)), - GlobalState::initial(self, client_context) + GlobalState::initial(self) ]) } else { let plugin_id = plugin_id.clone(); @@ -298,21 +284,21 @@ impl Focus for GlobalState { } } } - fn next(&mut self) -> Task { + fn next(&mut self, _client_context: &ClientContext) -> Task { match self { GlobalState::MainView { .. } => Task::none(), GlobalState::PluginView { .. } => Task::none(), GlobalState::ErrorView { .. } => Task::none(), } } - fn previous(&mut self) -> Task { + fn previous(&mut self, _client_context: &ClientContext) -> Task { match self { GlobalState::MainView { .. } => Task::none(), GlobalState::PluginView { .. } => Task::none(), GlobalState::ErrorView { .. } => Task::none(), } } - fn up(&mut self, _focus_list: &[SearchResult]) -> Task { + fn up(&mut self, client_context: &ClientContext, _focus_list: &[SearchResult]) -> Task { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { @@ -331,11 +317,9 @@ impl Focus for GlobalState { } } GlobalState::ErrorView { .. } => Task::none(), - GlobalState::PluginView { sub_state, client_context, .. } => { + GlobalState::PluginView { sub_state, .. } => { match sub_state { PluginViewState::None => { - let client_context = client_context.read().expect("lock is poisoned"); - client_context.focus_up() }, PluginViewState::ActionPanel { focused_action_item } => { @@ -346,9 +330,9 @@ impl Focus for GlobalState { }, } } - fn down(&mut self, focus_list: &[SearchResult]) -> Task { + fn down(&mut self, client_context: &ClientContext, focus_list: &[SearchResult]) -> Task { match self { - GlobalState::MainView { focused_search_result, sub_state, client_context, .. } => { + GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { MainViewState::None => { if focus_list.len() != 0 { @@ -371,8 +355,6 @@ impl Focus for GlobalState { } } MainViewState::InlineViewActionPanel { focused_action_item } => { - let client_context = client_context.read().expect("lock is poisoned"); - match client_context.get_first_inline_view_action_panel() { Some(action_panel) => { if action_panel.action_count() != 0 { @@ -388,16 +370,12 @@ impl Focus for GlobalState { } } GlobalState::ErrorView { .. } => Task::none(), - GlobalState::PluginView { sub_state, client_context, .. } => { + GlobalState::PluginView { sub_state, .. } => { match sub_state { PluginViewState::None => { - let client_context = client_context.read().expect("lock is poisoned"); - client_context.focus_down() }, PluginViewState::ActionPanel { focused_action_item } => { - let client_context = client_context.read().expect("lock is poisoned"); - let action_ids = client_context.get_action_ids(); if action_ids.len() != 0 { @@ -411,13 +389,11 @@ impl Focus for GlobalState { } } } - fn left(&mut self, _focus_list: &[SearchResult]) -> Task { + fn left(&mut self, client_context: &ClientContext, _focus_list: &[SearchResult]) -> Task { match self { - GlobalState::PluginView { client_context, sub_state, .. } => { + GlobalState::PluginView { sub_state, .. } => { match sub_state { PluginViewState::None => { - let client_context = client_context.read().expect("lock is poisoned"); - client_context.focus_left() } PluginViewState::ActionPanel { .. } => Task::none() @@ -427,13 +403,11 @@ impl Focus for GlobalState { GlobalState::ErrorView { .. } => Task::none(), } } - fn right(&mut self, _focus_list: &[SearchResult]) -> Task { + fn right(&mut self, client_context: &ClientContext, _focus_list: &[SearchResult]) -> Task { match self { - GlobalState::PluginView { client_context, sub_state, .. } => { + GlobalState::PluginView { sub_state, .. } => { match sub_state { PluginViewState::None => { - let client_context = client_context.read().expect("lock is poisoned"); - client_context.focus_right() } PluginViewState::ActionPanel { .. } => Task::none() diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 28045f0..56a81aa 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -32,18 +32,18 @@ use itertools::Itertools; use std::cell::Cell; use std::collections::HashMap; use std::fmt::{Debug, Display}; - +use std::sync::Arc; #[derive(Debug)] pub struct ComponentWidgets<'b> { - root_widget: &'b mut Option, + root_widget: &'b mut Option>, state: &'b mut HashMap, images: &'b HashMap> } impl<'b> ComponentWidgets<'b> { pub fn new( - root_widget: &'b mut Option, + root_widget: &'b mut Option>, state: &'b mut HashMap, images: &'b HashMap> ) -> ComponentWidgets<'b> { diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 9146fdd..1ee3b71 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -7,12 +7,13 @@ use common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiWidg use std::collections::HashMap; use std::mem; use std::ops::DerefMut; +use std::rc::Rc; use std::sync::{Arc, Mutex}; use iced::Task; use crate::ui::AppMsg; pub struct PluginWidgetContainer { - root_widget: Arc>>, + root_widget: Arc>>>, state: Arc>>, images: HashMap>, plugin_id: Option, @@ -44,7 +45,7 @@ impl PluginWidgetContainer { pub fn replace_view( &mut self, - container: RootWidget, + container: Arc, images: HashMap>, plugin_id: &PluginId, plugin_name: &str, From c41adc3c41d18c858c7844931c4b0dea5ab3a7ef Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 19 Dec 2024 21:24:49 +0100 Subject: [PATCH 218/540] Replace tools git submodule with github dependency --- .gitmodules | 3 - bundled_plugins/gauntlet/package.json | 2 +- dev_plugin/package.json | 2 +- package-lock.json | 2198 ++++++++++++----- package.json | 9 +- scenarios/plugins/docs_detail/package.json | 2 +- scenarios/plugins/docs_form/package.json | 2 +- scenarios/plugins/docs_grid/package.json | 2 +- .../docs_inline_separators/package.json | 2 +- .../docs_inline_three_sections/package.json | 2 +- .../docs_inline_two_sections/package.json | 2 +- scenarios/plugins/docs_list/package.json | 2 +- tools | 1 - 13 files changed, 1593 insertions(+), 636 deletions(-) delete mode 100644 .gitmodules delete mode 160000 tools diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 607863f..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "tools"] - path = tools - url = https://github.com/project-gauntlet/tools diff --git a/bundled_plugins/gauntlet/package.json b/bundled_plugins/gauntlet/package.json index 66a4080..3c9d5d6 100644 --- a/bundled_plugins/gauntlet/package.json +++ b/bundled_plugins/gauntlet/package.json @@ -12,7 +12,7 @@ }, "devDependencies": { "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "@types/react": "^18.2.14", "typescript": "^5.7.2" } diff --git a/dev_plugin/package.json b/dev_plugin/package.json index 617e170..7d820da 100644 --- a/dev_plugin/package.json +++ b/dev_plugin/package.json @@ -13,7 +13,7 @@ "devDependencies": { "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../js/deno", - "@project-gauntlet/tools": "file:../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "typescript": "^5.7.2" } } diff --git a/package-lock.json b/package-lock.json index 5b6cd4a..7501333 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,6 @@ "": { "name": "project-gauntlet", "workspaces": [ - "tools", "dev_plugin", "bundled_plugins/*", "scenarios/plugins/*", @@ -31,11 +30,152 @@ }, "devDependencies": { "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, + "bundled_plugins/gauntlet/node_modules/@mstssk/cleandir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", + "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", + "dev": true, + "license": "MIT", + "bin": { + "cleandir": "bin/cleandir.js" + } + }, + "bundled_plugins/gauntlet/node_modules/@project-gauntlet/tools": { + "version": "0.9.0", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "dev": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.5", + "@grpc/proto-loader": "^0.7.13", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "chalk": "^5.4.0", + "commander": "^12.1.0", + "rollup": "^4.28.1", + "rollup-plugin-cleandir": "^3.0.0", + "rollup-plugin-typescript2": "^0.36.0", + "simple-git": "^3.27.0", + "tail": "^2.2.6", + "toml": "^3.0.0", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0" + }, + "bin": { + "gauntlet": "bin/main.js" + } + }, + "bundled_plugins/gauntlet/node_modules/@rollup/plugin-commonjs": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", + "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "bundled_plugins/gauntlet/node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "bundled_plugins/gauntlet/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "bundled_plugins/gauntlet/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "bundled_plugins/gauntlet/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "bundled_plugins/gauntlet/node_modules/rollup-plugin-cleandir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", + "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mstssk/cleandir": "^2.0.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + } + }, "dev_plugin": { "name": "@project-gauntlet/dev-plugin", "dependencies": { @@ -45,11 +185,152 @@ }, "devDependencies": { "@project-gauntlet/deno": "file:../js/deno", - "@project-gauntlet/tools": "file:../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, + "dev_plugin/node_modules/@mstssk/cleandir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", + "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", + "dev": true, + "license": "MIT", + "bin": { + "cleandir": "bin/cleandir.js" + } + }, + "dev_plugin/node_modules/@project-gauntlet/tools": { + "version": "0.9.0", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "dev": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.5", + "@grpc/proto-loader": "^0.7.13", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "chalk": "^5.4.0", + "commander": "^12.1.0", + "rollup": "^4.28.1", + "rollup-plugin-cleandir": "^3.0.0", + "rollup-plugin-typescript2": "^0.36.0", + "simple-git": "^3.27.0", + "tail": "^2.2.6", + "toml": "^3.0.0", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0" + }, + "bin": { + "gauntlet": "bin/main.js" + } + }, + "dev_plugin/node_modules/@rollup/plugin-commonjs": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", + "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "dev_plugin/node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "dev_plugin/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "dev_plugin/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "dev_plugin/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "dev_plugin/node_modules/rollup-plugin-cleandir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", + "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mstssk/cleandir": "^2.0.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + } + }, "js/api": { "name": "@project-gauntlet/api", "version": "0.11.0", @@ -278,9 +559,11 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.11.1.tgz", - "integrity": "sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw==", + "version": "1.12.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.5.tgz", + "integrity": "sha512-d3iiHxdpg5+ZcJ6jnDSOT8Z0O0VMVGy34jAnYLUX8yd36b1qn8f1TwOA/Lc7TsOh03IkPJ38eGI5qD2EjNkoEA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" @@ -293,6 +576,7 @@ "version": "0.7.13", "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dev": true, "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", @@ -310,6 +594,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -326,6 +611,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, "engines": { "node": ">=12" }, @@ -337,6 +623,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "engines": { "node": ">=12" }, @@ -347,12 +634,14 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -369,6 +658,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -383,6 +673,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -398,12 +689,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true }, "node_modules/@js-sdsl/ordered-map": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/js-sdsl" @@ -427,235 +720,6 @@ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" }, - "node_modules/@mstssk/cleandir": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-1.2.1.tgz", - "integrity": "sha512-n/62eVEJOBb3CI1Y4r/IOLzvXoI0/MxGGOryT7UGF2C2WSXV3HozwrowqtHoDDbLmWIw+HJbuHZl9vledmGpoQ==", - "bin": { - "cleandir": "bin/cleandir.js" - } - }, - "node_modules/@node-kit/extra.fs": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@node-kit/extra.fs/-/extra.fs-3.2.0.tgz", - "integrity": "sha512-BtGZcB4ffMneAyKd2qy0umOURc7kMjgNOQlEQNqgkQwcwoGejqiJLQpzKVhAr2waBx7OLZx+/3sNzGgngSzx7w==", - "dev": true - }, - "node_modules/@node-kit/lerna-workspace-root": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@node-kit/lerna-workspace-root/-/lerna-workspace-root-3.2.0.tgz", - "integrity": "sha512-z9yfrFJzFuib4Pz1fU6tn6YmKhrISpopbKJsh8BfMXYmKRF4/8Td2JPfNIubAN2rlG3F3hSO4VPCDRwHNaKOhg==", - "dev": true, - "dependencies": { - "@node-kit/extra.fs": "3.2.0", - "find-up": "^5.0.0" - } - }, - "node_modules/@node-kit/lerna-workspace-root/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@node-kit/lerna-workspace-root/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@node-kit/lerna-workspace-root/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@node-kit/lerna-workspace-root/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@node-kit/pnpm-workspace-root": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@node-kit/pnpm-workspace-root/-/pnpm-workspace-root-3.2.0.tgz", - "integrity": "sha512-+6/4wT34/iipIaFc9P2dYxRNdfrcUULDERnWd+xdLjP4K2VyPRpx/E83BQ1eXhuuNtvevKI5yOxIR03+gb97ZA==", - "dev": true, - "dependencies": { - "@node-kit/extra.fs": "3.2.0", - "@pnpm/error": "^5.0.2", - "find-up": "^5.0.0" - } - }, - "node_modules/@node-kit/pnpm-workspace-root/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@node-kit/pnpm-workspace-root/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@node-kit/pnpm-workspace-root/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@node-kit/pnpm-workspace-root/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@node-kit/yarn-workspace-root": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@node-kit/yarn-workspace-root/-/yarn-workspace-root-3.2.0.tgz", - "integrity": "sha512-CJmCTaM0Goi9bRL4gfFxx1INxU9ETyqcnPGFoOMIHCyxJYRpB5C+C3x4BNWsBsBErdZ+BoyvgLsPj6jukCnFFQ==", - "dev": true, - "dependencies": { - "@node-kit/extra.fs": "3.2.0", - "find-up": "^5.0.0", - "micromatch": "^4.0.5" - } - }, - "node_modules/@node-kit/yarn-workspace-root/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@node-kit/yarn-workspace-root/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@node-kit/yarn-workspace-root/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@node-kit/yarn-workspace-root/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@octokit/app": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.1.0.tgz", @@ -1056,42 +1120,6 @@ "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.4.0.tgz", "integrity": "sha512-FE2V+QZ2UYlh+9wWd5BPLNXG+J/XUD/PPq0ovS+nCcGX4+3qVbi3jYOmCTW48hg9SBBLtInx9+o7fFt4H5iP0Q==" }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pnpm/constants": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@pnpm/constants/-/constants-7.1.1.tgz", - "integrity": "sha512-31pZqMtjwV+Vaq7MaPrT1EoDFSYwye3dp6BiHIGRJmVThCQwySRKM7hCvqqI94epNkqFAAYoWrNynWoRYosGdw==", - "dev": true, - "engines": { - "node": ">=16.14" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - }, - "node_modules/@pnpm/error": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@pnpm/error/-/error-5.0.3.tgz", - "integrity": "sha512-ONJU5cUeoeJSy50qOYsMZQHTA/9QKmGgh1ATfEpCLgtbdwqUiwD9MxHNeXUYYI/pocBCz6r1ZCFqiQvO+8SUKA==", - "dev": true, - "dependencies": { - "@pnpm/constants": "7.1.1" - }, - "engines": { - "node": ">=16.14" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - }, "node_modules/@project-gauntlet/api": { "resolved": "js/api", "link": true @@ -1164,10 +1192,6 @@ "resolved": "js/scenario_runner_cli", "link": true }, - "node_modules/@project-gauntlet/tools": { - "resolved": "tools", - "link": true - }, "node_modules/@project-gauntlet/typings": { "resolved": "js/typings", "link": true @@ -1175,27 +1199,32 @@ "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -1204,27 +1233,32 @@ "node_modules/@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dev": true }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true }, "node_modules/@rollup/plugin-alias": { "version": "5.1.1", @@ -1272,6 +1306,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, "dependencies": { "@rollup/pluginutils": "^5.1.0" }, @@ -1291,6 +1326,7 @@ "version": "15.2.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", @@ -1362,6 +1398,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", @@ -1380,220 +1417,289 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.4.tgz", - "integrity": "sha512-2Y3JT6f5MrQkICUyRVCw4oa0sutfAsgaSsb0Lmmy1Wi2y7X5vT9Euqw4gOsCyy0YfKURBg35nhUKZS4mDcfULw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", + "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", "cpu": [ "arm" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "android" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.4.tgz", - "integrity": "sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", + "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "android" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.4.tgz", - "integrity": "sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", + "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.4.tgz", - "integrity": "sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", + "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.4.tgz", - "integrity": "sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", + "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.4.tgz", - "integrity": "sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", + "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.4.tgz", - "integrity": "sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", + "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", "cpu": [ "arm" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.4.tgz", - "integrity": "sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", + "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", "cpu": [ "arm" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.4.tgz", - "integrity": "sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", + "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.4.tgz", - "integrity": "sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", + "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", + "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.4.tgz", - "integrity": "sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", + "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", "cpu": [ "ppc64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.4.tgz", - "integrity": "sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", + "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", "cpu": [ "riscv64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.4.tgz", - "integrity": "sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", + "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", "cpu": [ "s390x" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz", - "integrity": "sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", + "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.4.tgz", - "integrity": "sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", + "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.4.tgz", - "integrity": "sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", + "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.4.tgz", - "integrity": "sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", + "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", "cpu": [ "ia32" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.4.tgz", - "integrity": "sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", + "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@std/async": { "name": "@jsr/std__async", @@ -1632,7 +1738,8 @@ "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true }, "node_modules/@types/jsonwebtoken": { "version": "9.0.6", @@ -1684,7 +1791,8 @@ "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true }, "node_modules/@types/scheduler": { "version": "0.16.8", @@ -1692,12 +1800,6 @@ "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", "dev": true }, - "node_modules/@types/tail": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@types/tail/-/tail-2.2.3.tgz", - "integrity": "sha512-Hnf352egOlDR4nVTaGX0t/kmTNXHMdovF2C7PVDFtHTHJPFmIspOI1b86vEOxU7SfCq/dADS7ptbqgG/WGGxnA==", - "dev": true - }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -1714,6 +1816,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -1722,6 +1825,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1735,7 +1839,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/before-after-hook": { "version": "2.2.3", @@ -1751,20 +1856,9 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" + "balanced-match": "^1.0.0" } }, "node_modules/btoa-lite": { @@ -1781,6 +1875,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, "engines": { "node": ">=6" }, @@ -1789,9 +1884,11 @@ } }, "node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.0.tgz", + "integrity": "sha512-ZkD35Mx92acjB2yNJgziGqT9oKHEOxjTBTDRpOsRWtdecL/0jM3z5kM/CTzHWvHIen1GvkM85p6TuFfDGfc8/Q==", + "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -1811,6 +1908,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1824,6 +1922,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1834,7 +1933,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/commander": { "version": "11.1.0", @@ -1847,7 +1947,8 @@ "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -1888,6 +1989,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -1900,7 +2002,8 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", @@ -1913,12 +2016,14 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, "engines": { "node": ">=6" } @@ -1926,24 +2031,14 @@ "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", @@ -1960,6 +2055,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -1972,6 +2068,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -1987,6 +2084,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2006,11 +2104,13 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -2019,6 +2119,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2027,6 +2128,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -2053,12 +2155,14 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, "node_modules/hasown": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -2094,6 +2198,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, "dependencies": { "builtin-modules": "^3.3.0" }, @@ -2108,6 +2213,7 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -2119,6 +2225,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } @@ -2126,21 +2233,14 @@ "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, "dependencies": { "@types/estree": "*" } @@ -2150,20 +2250,6 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2173,6 +2259,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -2224,6 +2311,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -2239,7 +2327,8 @@ "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true }, "node_modules/lodash.includes": { "version": "4.3.0", @@ -2279,7 +2368,8 @@ "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "dev": true }, "node_modules/loose-envify": { "version": "1.4.0", @@ -2304,6 +2394,7 @@ "version": "0.30.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -2315,6 +2406,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, "dependencies": { "semver": "^6.0.0" }, @@ -2329,23 +2421,11 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } }, - "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -2362,6 +2442,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -2444,6 +2525,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -2458,6 +2540,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -2469,6 +2552,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, "engines": { "node": ">=6" } @@ -2476,12 +2560,14 @@ "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "engines": { "node": ">=8" } @@ -2497,27 +2583,14 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -2529,6 +2602,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, "dependencies": { "find-up": "^4.0.0" }, @@ -2540,6 +2614,7 @@ "version": "7.2.6", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "dev": true, "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -2589,6 +2664,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -2597,6 +2673,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -2610,9 +2687,11 @@ } }, "node_modules/rollup": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.4.tgz", - "integrity": "sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", + "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", + "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.6" }, @@ -2624,42 +2703,33 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.27.4", - "@rollup/rollup-android-arm64": "4.27.4", - "@rollup/rollup-darwin-arm64": "4.27.4", - "@rollup/rollup-darwin-x64": "4.27.4", - "@rollup/rollup-freebsd-arm64": "4.27.4", - "@rollup/rollup-freebsd-x64": "4.27.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.27.4", - "@rollup/rollup-linux-arm-musleabihf": "4.27.4", - "@rollup/rollup-linux-arm64-gnu": "4.27.4", - "@rollup/rollup-linux-arm64-musl": "4.27.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.27.4", - "@rollup/rollup-linux-riscv64-gnu": "4.27.4", - "@rollup/rollup-linux-s390x-gnu": "4.27.4", - "@rollup/rollup-linux-x64-gnu": "4.27.4", - "@rollup/rollup-linux-x64-musl": "4.27.4", - "@rollup/rollup-win32-arm64-msvc": "4.27.4", - "@rollup/rollup-win32-ia32-msvc": "4.27.4", - "@rollup/rollup-win32-x64-msvc": "4.27.4", + "@rollup/rollup-android-arm-eabi": "4.28.1", + "@rollup/rollup-android-arm64": "4.28.1", + "@rollup/rollup-darwin-arm64": "4.28.1", + "@rollup/rollup-darwin-x64": "4.28.1", + "@rollup/rollup-freebsd-arm64": "4.28.1", + "@rollup/rollup-freebsd-x64": "4.28.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", + "@rollup/rollup-linux-arm-musleabihf": "4.28.1", + "@rollup/rollup-linux-arm64-gnu": "4.28.1", + "@rollup/rollup-linux-arm64-musl": "4.28.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", + "@rollup/rollup-linux-riscv64-gnu": "4.28.1", + "@rollup/rollup-linux-s390x-gnu": "4.28.1", + "@rollup/rollup-linux-x64-gnu": "4.28.1", + "@rollup/rollup-linux-x64-musl": "4.28.1", + "@rollup/rollup-win32-arm64-msvc": "4.28.1", + "@rollup/rollup-win32-ia32-msvc": "4.28.1", + "@rollup/rollup-win32-x64-msvc": "4.28.1", "fsevents": "~2.3.2" } }, - "node_modules/rollup-plugin-cleandir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-2.0.0.tgz", - "integrity": "sha512-cTL/WbBJqHQkYBplhtiQ/yc0IqTuRR7EGw/S+XtQdaFhtv6+Xq/j8dxkk5lzTcxd0hCahUebZFYhLBRzSsgynw==", - "dependencies": { - "@mstssk/cleandir": "^1.2.0" - }, - "peerDependencies": { - "rollup": ">=2.0.0" - } - }, "node_modules/rollup-plugin-typescript2": { "version": "0.36.0", "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.36.0.tgz", "integrity": "sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw==", + "dev": true, "dependencies": { "@rollup/pluginutils": "^4.1.2", "find-cache-dir": "^3.3.2", @@ -2676,6 +2746,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" @@ -2745,6 +2816,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "engines": { "node": ">=14" }, @@ -2753,9 +2825,10 @@ } }, "node_modules/simple-git": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.25.0.tgz", - "integrity": "sha512-KIY5sBnzc4yEcJXW7Tdv4viEz8KyG+nU0hay+DWZasvdFOYKeUZ6Xc25LUHHjw0tinPT7O1eY6pzX7pRT1K8rw==", + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz", + "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==", + "license": "MIT", "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", @@ -2770,6 +2843,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2784,6 +2858,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2797,6 +2872,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2809,6 +2885,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2820,6 +2897,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -2831,31 +2909,23 @@ "version": "2.2.6", "resolved": "https://registry.npmjs.org/tail/-/tail-2.2.6.tgz", "integrity": "sha512-IQ6G4wK/t8VBauYiGPLx+d3fA5XjSVagjWV5SIYzvEvglbQjwEcukeYI68JOPpdydjxhZ9sIgzRlSmwSpphHyw==", + "dev": true, "engines": { "node": ">= 6.0.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/toml": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "dev": true }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" }, "node_modules/tunnel": { "version": "0.0.6", @@ -2869,6 +2939,7 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2911,6 +2982,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, "engines": { "node": ">= 10.0.0" } @@ -2937,21 +3009,11 @@ "node": ">= 8" } }, - "node_modules/workspace-root": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/workspace-root/-/workspace-root-3.2.0.tgz", - "integrity": "sha512-pa294ZcHkGVfVomqtgNZjbJXwae7awRDni8hVkQSBastuHS2M9LsZ75mToajutFmyYo0tvjO0RWoiKAf3To6ug==", - "dev": true, - "dependencies": { - "@node-kit/lerna-workspace-root": "3.2.0", - "@node-kit/pnpm-workspace-root": "3.2.0", - "@node-kit/yarn-workspace-root": "3.2.0" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2969,6 +3031,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2990,6 +3053,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "engines": { "node": ">=10" } @@ -2998,6 +3062,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -3015,34 +3080,27 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "engines": { "node": ">=12" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-validation-error": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.3.1.tgz", - "integrity": "sha512-uFzCZz7FQis256dqw4AhPQgD6f3pzNca/Zh62RNELavlumQB3nDIUFbF5JQfFLcMbO1s02Q7Xg/gpcOBlEnYZA==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">=18.0.0" }, @@ -3061,11 +3119,21 @@ }, "devDependencies": { "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, + "scenarios/plugins/docs_detail/node_modules/@mstssk/cleandir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", + "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", + "dev": true, + "license": "MIT", + "bin": { + "cleandir": "bin/cleandir.js" + } + }, "scenarios/plugins/docs_detail/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true @@ -3075,8 +3143,135 @@ "link": true }, "scenarios/plugins/docs_detail/node_modules/@project-gauntlet/tools": { - "resolved": "scenarios/tools", - "link": true + "version": "0.9.0", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "dev": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.5", + "@grpc/proto-loader": "^0.7.13", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "chalk": "^5.4.0", + "commander": "^12.1.0", + "rollup": "^4.28.1", + "rollup-plugin-cleandir": "^3.0.0", + "rollup-plugin-typescript2": "^0.36.0", + "simple-git": "^3.27.0", + "tail": "^2.2.6", + "toml": "^3.0.0", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0" + }, + "bin": { + "gauntlet": "bin/main.js" + } + }, + "scenarios/plugins/docs_detail/node_modules/@rollup/plugin-commonjs": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", + "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_detail/node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_detail/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "scenarios/plugins/docs_detail/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "scenarios/plugins/docs_detail/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "scenarios/plugins/docs_detail/node_modules/rollup-plugin-cleandir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", + "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mstssk/cleandir": "^2.0.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + } }, "scenarios/plugins/docs_form": { "name": "@project-gauntlet/docs-form", @@ -3085,11 +3280,21 @@ }, "devDependencies": { "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, + "scenarios/plugins/docs_form/node_modules/@mstssk/cleandir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", + "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", + "dev": true, + "license": "MIT", + "bin": { + "cleandir": "bin/cleandir.js" + } + }, "scenarios/plugins/docs_form/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true @@ -3099,8 +3304,135 @@ "link": true }, "scenarios/plugins/docs_form/node_modules/@project-gauntlet/tools": { - "resolved": "scenarios/tools", - "link": true + "version": "0.9.0", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "dev": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.5", + "@grpc/proto-loader": "^0.7.13", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "chalk": "^5.4.0", + "commander": "^12.1.0", + "rollup": "^4.28.1", + "rollup-plugin-cleandir": "^3.0.0", + "rollup-plugin-typescript2": "^0.36.0", + "simple-git": "^3.27.0", + "tail": "^2.2.6", + "toml": "^3.0.0", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0" + }, + "bin": { + "gauntlet": "bin/main.js" + } + }, + "scenarios/plugins/docs_form/node_modules/@rollup/plugin-commonjs": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", + "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_form/node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_form/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "scenarios/plugins/docs_form/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "scenarios/plugins/docs_form/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "scenarios/plugins/docs_form/node_modules/rollup-plugin-cleandir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", + "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mstssk/cleandir": "^2.0.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + } }, "scenarios/plugins/docs_grid": { "name": "@project-gauntlet/docs-grid", @@ -3109,11 +3441,21 @@ }, "devDependencies": { "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, + "scenarios/plugins/docs_grid/node_modules/@mstssk/cleandir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", + "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", + "dev": true, + "license": "MIT", + "bin": { + "cleandir": "bin/cleandir.js" + } + }, "scenarios/plugins/docs_grid/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true @@ -3123,8 +3465,135 @@ "link": true }, "scenarios/plugins/docs_grid/node_modules/@project-gauntlet/tools": { - "resolved": "scenarios/tools", - "link": true + "version": "0.9.0", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "dev": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.5", + "@grpc/proto-loader": "^0.7.13", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "chalk": "^5.4.0", + "commander": "^12.1.0", + "rollup": "^4.28.1", + "rollup-plugin-cleandir": "^3.0.0", + "rollup-plugin-typescript2": "^0.36.0", + "simple-git": "^3.27.0", + "tail": "^2.2.6", + "toml": "^3.0.0", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0" + }, + "bin": { + "gauntlet": "bin/main.js" + } + }, + "scenarios/plugins/docs_grid/node_modules/@rollup/plugin-commonjs": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", + "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_grid/node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_grid/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "scenarios/plugins/docs_grid/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "scenarios/plugins/docs_grid/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "scenarios/plugins/docs_grid/node_modules/rollup-plugin-cleandir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", + "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mstssk/cleandir": "^2.0.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + } }, "scenarios/plugins/docs_inline": { "name": "@project-gauntlet/docs-inline", @@ -3146,11 +3615,21 @@ }, "devDependencies": { "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, + "scenarios/plugins/docs_inline_separators/node_modules/@mstssk/cleandir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", + "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", + "dev": true, + "license": "MIT", + "bin": { + "cleandir": "bin/cleandir.js" + } + }, "scenarios/plugins/docs_inline_separators/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true @@ -3159,6 +3638,137 @@ "resolved": "scenarios/js/deno", "link": true }, + "scenarios/plugins/docs_inline_separators/node_modules/@project-gauntlet/tools": { + "version": "0.9.0", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "dev": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.5", + "@grpc/proto-loader": "^0.7.13", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "chalk": "^5.4.0", + "commander": "^12.1.0", + "rollup": "^4.28.1", + "rollup-plugin-cleandir": "^3.0.0", + "rollup-plugin-typescript2": "^0.36.0", + "simple-git": "^3.27.0", + "tail": "^2.2.6", + "toml": "^3.0.0", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0" + }, + "bin": { + "gauntlet": "bin/main.js" + } + }, + "scenarios/plugins/docs_inline_separators/node_modules/@rollup/plugin-commonjs": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", + "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_inline_separators/node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_inline_separators/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "scenarios/plugins/docs_inline_separators/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "scenarios/plugins/docs_inline_separators/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "scenarios/plugins/docs_inline_separators/node_modules/rollup-plugin-cleandir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", + "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mstssk/cleandir": "^2.0.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + } + }, "scenarios/plugins/docs_inline_three_sections": { "name": "@project-gauntlet/docs-inline-three-sections", "dependencies": { @@ -3166,11 +3776,21 @@ }, "devDependencies": { "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, + "scenarios/plugins/docs_inline_three_sections/node_modules/@mstssk/cleandir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", + "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", + "dev": true, + "license": "MIT", + "bin": { + "cleandir": "bin/cleandir.js" + } + }, "scenarios/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true @@ -3179,6 +3799,137 @@ "resolved": "scenarios/js/deno", "link": true }, + "scenarios/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/tools": { + "version": "0.9.0", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "dev": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.5", + "@grpc/proto-loader": "^0.7.13", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "chalk": "^5.4.0", + "commander": "^12.1.0", + "rollup": "^4.28.1", + "rollup-plugin-cleandir": "^3.0.0", + "rollup-plugin-typescript2": "^0.36.0", + "simple-git": "^3.27.0", + "tail": "^2.2.6", + "toml": "^3.0.0", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0" + }, + "bin": { + "gauntlet": "bin/main.js" + } + }, + "scenarios/plugins/docs_inline_three_sections/node_modules/@rollup/plugin-commonjs": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", + "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_inline_three_sections/node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_inline_three_sections/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "scenarios/plugins/docs_inline_three_sections/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "scenarios/plugins/docs_inline_three_sections/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "scenarios/plugins/docs_inline_three_sections/node_modules/rollup-plugin-cleandir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", + "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mstssk/cleandir": "^2.0.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + } + }, "scenarios/plugins/docs_inline_two_sections": { "name": "@project-gauntlet/docs-inline-two-sections", "dependencies": { @@ -3186,11 +3937,21 @@ }, "devDependencies": { "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, + "scenarios/plugins/docs_inline_two_sections/node_modules/@mstssk/cleandir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", + "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", + "dev": true, + "license": "MIT", + "bin": { + "cleandir": "bin/cleandir.js" + } + }, "scenarios/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true @@ -3199,6 +3960,137 @@ "resolved": "scenarios/js/deno", "link": true }, + "scenarios/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/tools": { + "version": "0.9.0", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "dev": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.5", + "@grpc/proto-loader": "^0.7.13", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "chalk": "^5.4.0", + "commander": "^12.1.0", + "rollup": "^4.28.1", + "rollup-plugin-cleandir": "^3.0.0", + "rollup-plugin-typescript2": "^0.36.0", + "simple-git": "^3.27.0", + "tail": "^2.2.6", + "toml": "^3.0.0", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0" + }, + "bin": { + "gauntlet": "bin/main.js" + } + }, + "scenarios/plugins/docs_inline_two_sections/node_modules/@rollup/plugin-commonjs": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", + "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_inline_two_sections/node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_inline_two_sections/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "scenarios/plugins/docs_inline_two_sections/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "scenarios/plugins/docs_inline_two_sections/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "scenarios/plugins/docs_inline_two_sections/node_modules/rollup-plugin-cleandir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", + "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mstssk/cleandir": "^2.0.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + } + }, "scenarios/plugins/docs_list": { "name": "@project-gauntlet/docs-list", "dependencies": { @@ -3206,11 +4098,21 @@ }, "devDependencies": { "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, + "scenarios/plugins/docs_list/node_modules/@mstssk/cleandir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", + "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", + "dev": true, + "license": "MIT", + "bin": { + "cleandir": "bin/cleandir.js" + } + }, "scenarios/plugins/docs_list/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true @@ -3220,15 +4122,140 @@ "link": true }, "scenarios/plugins/docs_list/node_modules/@project-gauntlet/tools": { - "resolved": "scenarios/tools", - "link": true + "version": "0.9.0", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "dev": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.5", + "@grpc/proto-loader": "^0.7.13", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "chalk": "^5.4.0", + "commander": "^12.1.0", + "rollup": "^4.28.1", + "rollup-plugin-cleandir": "^3.0.0", + "rollup-plugin-typescript2": "^0.36.0", + "simple-git": "^3.27.0", + "tail": "^2.2.6", + "toml": "^3.0.0", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0" + }, + "bin": { + "gauntlet": "bin/main.js" + } }, - "scenarios/tools": { - "dev": true + "scenarios/plugins/docs_list/node_modules/@rollup/plugin-commonjs": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", + "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_list/node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "scenarios/plugins/docs_list/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "scenarios/plugins/docs_list/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "scenarios/plugins/docs_list/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "scenarios/plugins/docs_list/node_modules/rollup-plugin-cleandir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", + "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mstssk/cleandir": "^2.0.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + } }, "tools": { "name": "@project-gauntlet/tools", "version": "0.9.0", + "extraneous": true, "dependencies": { "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", @@ -3256,71 +4283,6 @@ "@types/tail": "^2.2.3", "workspace-root": "^3.2.0" } - }, - "tools/node_modules/@rollup/plugin-commonjs": { - "version": "26.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.1.tgz", - "integrity": "sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "glob": "^10.4.1", - "is-reference": "1.2.1", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "tools/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "engines": { - "node": ">=18" - } - }, - "tools/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "tools/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } } } } diff --git a/package.json b/package.json index a33500c..c26abc2 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,14 @@ "name": "project-gauntlet", "private": true, "scripts": { - "build-all": "npm run build --workspace tools && npm run build --workspaces --if-present", - "build-scenarios": "npm run build --workspace tools && npm run build --workspace scenarios --if-present", - "build-dev-plugin": "npm run build --workspace tools && npm run build --workspace dev_plugin", - "build": "npm run build --workspace tools && npm run build --workspace js --workspace bundled_plugins --if-present", + "build-all": "npm run build --workspaces --if-present", + "build-scenarios": "npm run build --workspace scenarios --if-present", + "build-dev-plugin": "npm run build --workspace dev_plugin", + "build": "npm run build --workspace js --workspace bundled_plugins --if-present", "run-scenarios": "npm run run-scenarios --workspace js/scenario_runner_cli", "run-screenshot-gen": "npm run run-screenshot-gen --workspace js/scenario_runner_cli" }, "workspaces": [ - "tools", "dev_plugin", "bundled_plugins/*", "scenarios/plugins/*", diff --git a/scenarios/plugins/docs_detail/package.json b/scenarios/plugins/docs_detail/package.json index d0995ad..d138b3a 100644 --- a/scenarios/plugins/docs_detail/package.json +++ b/scenarios/plugins/docs_detail/package.json @@ -11,7 +11,7 @@ "devDependencies": { "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_form/package.json b/scenarios/plugins/docs_form/package.json index d134d67..d2b9349 100644 --- a/scenarios/plugins/docs_form/package.json +++ b/scenarios/plugins/docs_form/package.json @@ -11,7 +11,7 @@ "devDependencies": { "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_grid/package.json b/scenarios/plugins/docs_grid/package.json index d99c7c7..e0b50e9 100644 --- a/scenarios/plugins/docs_grid/package.json +++ b/scenarios/plugins/docs_grid/package.json @@ -11,7 +11,7 @@ "devDependencies": { "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_inline_separators/package.json b/scenarios/plugins/docs_inline_separators/package.json index 2a97183..e21a2f5 100644 --- a/scenarios/plugins/docs_inline_separators/package.json +++ b/scenarios/plugins/docs_inline_separators/package.json @@ -11,7 +11,7 @@ "devDependencies": { "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_inline_three_sections/package.json b/scenarios/plugins/docs_inline_three_sections/package.json index 0ba42d0..49a2d5d 100644 --- a/scenarios/plugins/docs_inline_three_sections/package.json +++ b/scenarios/plugins/docs_inline_three_sections/package.json @@ -11,7 +11,7 @@ "devDependencies": { "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_inline_two_sections/package.json b/scenarios/plugins/docs_inline_two_sections/package.json index 17ea73d..55e0c06 100644 --- a/scenarios/plugins/docs_inline_two_sections/package.json +++ b/scenarios/plugins/docs_inline_two_sections/package.json @@ -11,7 +11,7 @@ "devDependencies": { "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_list/package.json b/scenarios/plugins/docs_list/package.json index 663e982..020837d 100644 --- a/scenarios/plugins/docs_list/package.json +++ b/scenarios/plugins/docs_list/package.json @@ -11,7 +11,7 @@ "devDependencies": { "@types/react": "^18.2.14", "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "file:../../../tools", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", "typescript": "^5.7.2" } } diff --git a/tools b/tools deleted file mode 160000 index 7bc5ef7..0000000 --- a/tools +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7bc5ef7d8326172b4353d37763b3c55e4ace051f From e32a024f1a3b693d22d46736736d7615753b2c42 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 19 Dec 2024 22:13:49 +0100 Subject: [PATCH 219/540] Remove @project-gauntlet/deno, use @types/deno instead --- bundled_plugins/gauntlet/package.json | 4 +- bundled_plugins/gauntlet/tsconfig.json | 2 +- dev_plugin/package.json | 4 +- dev_plugin/tsconfig.json | 3 +- js/core/package.json | 2 +- js/core/tsconfig.json | 4 +- js/deno/.gitignore | 2 - js/deno/generator/index.ts | 16 - js/deno/package.json | 29 -- js/deno/tsconfig.json | 12 - js/react_renderer/package.json | 2 +- js/react_renderer/tsconfig.json | 4 +- package-lock.json | 351 ++++-------------- package.json | 1 - scenarios/plugins/docs_detail/package.json | 4 +- scenarios/plugins/docs_detail/tsconfig.json | 3 +- scenarios/plugins/docs_form/package.json | 4 +- scenarios/plugins/docs_form/tsconfig.json | 1 - scenarios/plugins/docs_grid/package.json | 4 +- scenarios/plugins/docs_grid/tsconfig.json | 3 +- .../docs_inline_separators/package.json | 4 +- .../docs_inline_separators/tsconfig.json | 3 +- .../docs_inline_three_sections/package.json | 4 +- .../docs_inline_three_sections/tsconfig.json | 3 +- .../docs_inline_two_sections/package.json | 4 +- .../docs_inline_two_sections/tsconfig.json | 3 +- scenarios/plugins/docs_list/package.json | 4 +- scenarios/plugins/docs_list/tsconfig.json | 3 +- 28 files changed, 103 insertions(+), 380 deletions(-) delete mode 100644 js/deno/.gitignore delete mode 100644 js/deno/generator/index.ts delete mode 100644 js/deno/package.json delete mode 100644 js/deno/tsconfig.json diff --git a/bundled_plugins/gauntlet/package.json b/bundled_plugins/gauntlet/package.json index 3c9d5d6..d507842 100644 --- a/bundled_plugins/gauntlet/package.json +++ b/bundled_plugins/gauntlet/package.json @@ -11,8 +11,8 @@ "@std/fs": "npm:@jsr/std__fs@^1.0.5" }, "devDependencies": { - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@types/deno": "^2.0.0", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", "@types/react": "^18.2.14", "typescript": "^5.7.2" } diff --git a/bundled_plugins/gauntlet/tsconfig.json b/bundled_plugins/gauntlet/tsconfig.json index 7f14964..e866b96 100644 --- a/bundled_plugins/gauntlet/tsconfig.json +++ b/bundled_plugins/gauntlet/tsconfig.json @@ -6,7 +6,7 @@ "target": "ES2022", "moduleResolution": "bundler", "jsx": "react-jsx", - "types": ["@project-gauntlet/typings", "@project-gauntlet/deno"] + "types": ["@project-gauntlet/typings", "@types/deno"] }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/dev_plugin/package.json b/dev_plugin/package.json index 7d820da..de33485 100644 --- a/dev_plugin/package.json +++ b/dev_plugin/package.json @@ -12,8 +12,8 @@ }, "devDependencies": { "@types/react": "^18.2.14", - "@project-gauntlet/deno": "file:../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@types/deno": "^2.0.0", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", "typescript": "^5.7.2" } } diff --git a/dev_plugin/tsconfig.json b/dev_plugin/tsconfig.json index cbe7961..f9bb627 100644 --- a/dev_plugin/tsconfig.json +++ b/dev_plugin/tsconfig.json @@ -5,8 +5,7 @@ "esModuleInterop": true, "target": "ES2022", "moduleResolution": "bundler", - "jsx": "react-jsx", - "types": ["@project-gauntlet/deno"] + "jsx": "react-jsx" }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/js/core/package.json b/js/core/package.json index 29400bb..564b223 100644 --- a/js/core/package.json +++ b/js/core/package.json @@ -6,12 +6,12 @@ }, "devDependencies": { "@project-gauntlet/api": "*", - "@project-gauntlet/deno": "*", "@project-gauntlet/typings": "*", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.5", + "@types/deno": "^2.0.0", "@types/react": "^18.2.35", "rollup": "^4.27.4", "tslib": "^2.6.2", diff --git a/js/core/tsconfig.json b/js/core/tsconfig.json index 7f14964..79c25ee 100644 --- a/js/core/tsconfig.json +++ b/js/core/tsconfig.json @@ -3,10 +3,10 @@ "strict": true, "module": "ES2022", "esModuleInterop": true, - "target": "ES2022", + "target": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", - "types": ["@project-gauntlet/typings", "@project-gauntlet/deno"] + "types": ["@project-gauntlet/typings", "@types/deno", "@types/node"] }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/js/deno/.gitignore b/js/deno/.gitignore deleted file mode 100644 index 18ce544..0000000 --- a/js/deno/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -builddist \ No newline at end of file diff --git a/js/deno/generator/index.ts b/js/deno/generator/index.ts deleted file mode 100644 index c29e34d..0000000 --- a/js/deno/generator/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { existsSync, mkdirSync, writeFileSync } from "node:fs"; - -// https://github.com/denoland/deno/releases/tag/v2.1.1 -const LIB_DENO_DECLARATION_URL = "https://github.com/denoland/deno/releases/download/v2.1.1/lib.deno.d.ts"; - -const res = await fetch(LIB_DENO_DECLARATION_URL); -const content = await res.text(); - -const fixedContent = content.replaceAll(/\/\/\/ /g, "") - -const distDir = "./dist"; -if (!existsSync(distDir)) { - mkdirSync(distDir); -} - -writeFileSync(`${distDir}/lib.deno.d.ts`, fixedContent) diff --git a/js/deno/package.json b/js/deno/package.json deleted file mode 100644 index 0ec399c..0000000 --- a/js/deno/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@project-gauntlet/deno", - "version": "0.11.0", - "type": "module", - "exports": { - ".": { - "types": "./dist/lib.deno.d.ts" - } - }, - "repository": { - "type": "git", - "url": "https://github.com/project-gauntlet/gauntlet.git", - "directory": "js/deno" - }, - "files": [ - "dist" - ], - "scripts": { - "build": "npm run run-generator-source", - "run-generator-source": "tsc --project tsconfig.json && node builddist/index.js" - }, - "devDependencies": { - "@types/node": "^18.19.67", - "typescript": "^5.7.2" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/js/deno/tsconfig.json b/js/deno/tsconfig.json deleted file mode 100644 index 2854811..0000000 --- a/js/deno/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "module": "ES2022", - "esModuleInterop": true, - "target": "ES2022", - "moduleResolution": "bundler", - "outDir": "./builddist" - }, - "lib": ["ES2020"], - "include": ["./generator"] -} \ No newline at end of file diff --git a/js/react_renderer/package.json b/js/react_renderer/package.json index a16067a..eb0c4e8 100644 --- a/js/react_renderer/package.json +++ b/js/react_renderer/package.json @@ -13,10 +13,10 @@ "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^6.0.1", "@rollup/plugin-typescript": "^11.1.5", + "@types/deno": "^2.0.0", "@types/react": "^18.2.35", "@types/react-reconciler": "^0.28.6", "@project-gauntlet/typings": "*", - "@project-gauntlet/deno": "*", "rollup": "^4.27.4", "tslib": "^2.6.2", "typescript": "^5.7.2" diff --git a/js/react_renderer/tsconfig.json b/js/react_renderer/tsconfig.json index 7f14964..292e4bb 100644 --- a/js/react_renderer/tsconfig.json +++ b/js/react_renderer/tsconfig.json @@ -3,10 +3,10 @@ "strict": true, "module": "ES2022", "esModuleInterop": true, - "target": "ES2022", + "target": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", - "types": ["@project-gauntlet/typings", "@project-gauntlet/deno"] + "types": ["@project-gauntlet/typings", "@types/deno"], }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7501333..dbe81bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "js/typings", "js/build", "js/api_build", - "js/deno", "js/api", "js/react", "js/core", @@ -29,26 +28,16 @@ "@std/fs": "npm:@jsr/std__fs@^1.0.5" }, "devDependencies": { - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@types/deno": "^2.0.0", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, - "bundled_plugins/gauntlet/node_modules/@mstssk/cleandir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", - "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", - "dev": true, - "license": "MIT", - "bin": { - "cleandir": "bin/cleandir.js" - } - }, "bundled_plugins/gauntlet/node_modules/@project-gauntlet/tools": { "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", - "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", "dev": true, "dependencies": { "@grpc/grpc-js": "^1.12.5", @@ -163,19 +152,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "bundled_plugins/gauntlet/node_modules/rollup-plugin-cleandir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", - "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mstssk/cleandir": "^2.0.0" - }, - "peerDependencies": { - "rollup": ">=4.0.0" - } - }, "dev_plugin": { "name": "@project-gauntlet/dev-plugin", "dependencies": { @@ -184,26 +160,16 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@project-gauntlet/deno": "file:../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@types/deno": "^2.0.0", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, - "dev_plugin/node_modules/@mstssk/cleandir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", - "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", - "dev": true, - "license": "MIT", - "bin": { - "cleandir": "bin/cleandir.js" - } - }, "dev_plugin/node_modules/@project-gauntlet/tools": { "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", - "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", "dev": true, "dependencies": { "@grpc/grpc-js": "^1.12.5", @@ -318,19 +284,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "dev_plugin/node_modules/rollup-plugin-cleandir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", - "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mstssk/cleandir": "^2.0.0" - }, - "peerDependencies": { - "rollup": ">=4.0.0" - } - }, "js/api": { "name": "@project-gauntlet/api", "version": "0.11.0", @@ -461,12 +414,12 @@ "name": "@project-gauntlet/core", "devDependencies": { "@project-gauntlet/api": "*", - "@project-gauntlet/deno": "*", "@project-gauntlet/typings": "*", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.5", + "@types/deno": "^2.0.0", "@types/react": "^18.2.35", "rollup": "^4.27.4", "tslib": "^2.6.2", @@ -476,6 +429,7 @@ "js/deno": { "name": "@project-gauntlet/deno", "version": "0.11.0", + "extraneous": true, "devDependencies": { "@types/node": "^18.19.67", "typescript": "^5.7.2" @@ -501,13 +455,13 @@ "react-reconciler": "^0.29.0" }, "devDependencies": { - "@project-gauntlet/deno": "*", "@project-gauntlet/typings": "*", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^6.0.1", "@rollup/plugin-typescript": "^11.1.5", + "@types/deno": "^2.0.0", "@types/react": "^18.2.35", "@types/react-reconciler": "^0.28.6", "rollup": "^4.27.4", @@ -720,6 +674,16 @@ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" }, + "node_modules/@mstssk/cleandir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", + "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", + "dev": true, + "license": "MIT", + "bin": { + "cleandir": "bin/cleandir.js" + } + }, "node_modules/@octokit/app": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.1.0.tgz", @@ -1144,10 +1108,6 @@ "resolved": "js/core", "link": true }, - "node_modules/@project-gauntlet/deno": { - "resolved": "js/deno", - "link": true - }, "node_modules/@project-gauntlet/dev-plugin": { "resolved": "dev_plugin", "link": true @@ -1735,6 +1695,13 @@ "@types/node": "*" } }, + "node_modules/@types/deno": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/deno/-/deno-2.0.0.tgz", + "integrity": "sha512-O9/jRVlq93kqfkl4sYR5N7+Pz4ukzXVIbMnE/VgvpauNHsvjQ9iBVnJ3X0gAvMa2khcoFD8DSO7mQVCuiuDMPg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2725,6 +2692,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-cleandir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", + "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mstssk/cleandir": "^2.0.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + } + }, "node_modules/rollup-plugin-typescript2": { "version": "0.36.0", "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.36.0.tgz", @@ -3110,7 +3090,7 @@ }, "scenarios/js/api": {}, "scenarios/js/deno": { - "dev": true + "extraneous": true }, "scenarios/plugins/docs_detail": { "name": "@project-gauntlet/docs-detailt", @@ -3118,34 +3098,20 @@ "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@types/deno": "^2.0.0", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, - "scenarios/plugins/docs_detail/node_modules/@mstssk/cleandir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", - "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", - "dev": true, - "license": "MIT", - "bin": { - "cleandir": "bin/cleandir.js" - } - }, "scenarios/plugins/docs_detail/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_detail/node_modules/@project-gauntlet/deno": { - "resolved": "scenarios/js/deno", - "link": true - }, "scenarios/plugins/docs_detail/node_modules/@project-gauntlet/tools": { "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", - "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", "dev": true, "dependencies": { "@grpc/grpc-js": "^1.12.5", @@ -3260,53 +3226,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "scenarios/plugins/docs_detail/node_modules/rollup-plugin-cleandir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", - "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mstssk/cleandir": "^2.0.0" - }, - "peerDependencies": { - "rollup": ">=4.0.0" - } - }, "scenarios/plugins/docs_form": { "name": "@project-gauntlet/docs-form", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@types/deno": "^2.0.0", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, - "scenarios/plugins/docs_form/node_modules/@mstssk/cleandir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", - "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", - "dev": true, - "license": "MIT", - "bin": { - "cleandir": "bin/cleandir.js" - } - }, "scenarios/plugins/docs_form/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_form/node_modules/@project-gauntlet/deno": { - "resolved": "scenarios/js/deno", - "link": true - }, "scenarios/plugins/docs_form/node_modules/@project-gauntlet/tools": { "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", - "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", "dev": true, "dependencies": { "@grpc/grpc-js": "^1.12.5", @@ -3421,53 +3360,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "scenarios/plugins/docs_form/node_modules/rollup-plugin-cleandir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", - "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mstssk/cleandir": "^2.0.0" - }, - "peerDependencies": { - "rollup": ">=4.0.0" - } - }, "scenarios/plugins/docs_grid": { "name": "@project-gauntlet/docs-grid", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@types/deno": "^2.0.0", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, - "scenarios/plugins/docs_grid/node_modules/@mstssk/cleandir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", - "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", - "dev": true, - "license": "MIT", - "bin": { - "cleandir": "bin/cleandir.js" - } - }, "scenarios/plugins/docs_grid/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_grid/node_modules/@project-gauntlet/deno": { - "resolved": "scenarios/js/deno", - "link": true - }, "scenarios/plugins/docs_grid/node_modules/@project-gauntlet/tools": { "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", - "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", "dev": true, "dependencies": { "@grpc/grpc-js": "^1.12.5", @@ -3582,19 +3494,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "scenarios/plugins/docs_grid/node_modules/rollup-plugin-cleandir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", - "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mstssk/cleandir": "^2.0.0" - }, - "peerDependencies": { - "rollup": ">=4.0.0" - } - }, "scenarios/plugins/docs_inline": { "name": "@project-gauntlet/docs-inline", "extraneous": true, @@ -3602,8 +3501,8 @@ "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/deno": "file:../../js/deno", "@project-gauntlet/tools": "file:../../../tools", + "@types/deno": "^2.0.0", "@types/react": "^18.2.14", "typescript": "^5.3.3" } @@ -3614,34 +3513,20 @@ "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@types/deno": "^2.0.0", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, - "scenarios/plugins/docs_inline_separators/node_modules/@mstssk/cleandir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", - "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", - "dev": true, - "license": "MIT", - "bin": { - "cleandir": "bin/cleandir.js" - } - }, "scenarios/plugins/docs_inline_separators/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_inline_separators/node_modules/@project-gauntlet/deno": { - "resolved": "scenarios/js/deno", - "link": true - }, "scenarios/plugins/docs_inline_separators/node_modules/@project-gauntlet/tools": { "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", - "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", "dev": true, "dependencies": { "@grpc/grpc-js": "^1.12.5", @@ -3756,53 +3641,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "scenarios/plugins/docs_inline_separators/node_modules/rollup-plugin-cleandir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", - "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mstssk/cleandir": "^2.0.0" - }, - "peerDependencies": { - "rollup": ">=4.0.0" - } - }, "scenarios/plugins/docs_inline_three_sections": { "name": "@project-gauntlet/docs-inline-three-sections", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@types/deno": "^2.0.0", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, - "scenarios/plugins/docs_inline_three_sections/node_modules/@mstssk/cleandir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", - "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", - "dev": true, - "license": "MIT", - "bin": { - "cleandir": "bin/cleandir.js" - } - }, "scenarios/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/deno": { - "resolved": "scenarios/js/deno", - "link": true - }, "scenarios/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/tools": { "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", - "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", "dev": true, "dependencies": { "@grpc/grpc-js": "^1.12.5", @@ -3917,53 +3775,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "scenarios/plugins/docs_inline_three_sections/node_modules/rollup-plugin-cleandir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", - "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mstssk/cleandir": "^2.0.0" - }, - "peerDependencies": { - "rollup": ">=4.0.0" - } - }, "scenarios/plugins/docs_inline_two_sections": { "name": "@project-gauntlet/docs-inline-two-sections", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@types/deno": "^2.0.0", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, - "scenarios/plugins/docs_inline_two_sections/node_modules/@mstssk/cleandir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", - "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", - "dev": true, - "license": "MIT", - "bin": { - "cleandir": "bin/cleandir.js" - } - }, "scenarios/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/deno": { - "resolved": "scenarios/js/deno", - "link": true - }, "scenarios/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/tools": { "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", - "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", "dev": true, "dependencies": { "@grpc/grpc-js": "^1.12.5", @@ -4078,53 +3909,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "scenarios/plugins/docs_inline_two_sections/node_modules/rollup-plugin-cleandir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", - "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mstssk/cleandir": "^2.0.0" - }, - "peerDependencies": { - "rollup": ">=4.0.0" - } - }, "scenarios/plugins/docs_list": { "name": "@project-gauntlet/docs-list", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@types/deno": "^2.0.0", "@types/react": "^18.2.14", "typescript": "^5.7.2" } }, - "scenarios/plugins/docs_list/node_modules/@mstssk/cleandir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@mstssk/cleandir/-/cleandir-2.0.0.tgz", - "integrity": "sha512-CidYeaV4VQLIMbZlYqs0SJaZe/DyI0E4nsbFmPtCa2koKzMjZj/BThTCb+bvzcGhzp2A4Js1c4jDg6lqaqapyQ==", - "dev": true, - "license": "MIT", - "bin": { - "cleandir": "bin/cleandir.js" - } - }, "scenarios/plugins/docs_list/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_list/node_modules/@project-gauntlet/deno": { - "resolved": "scenarios/js/deno", - "link": true - }, "scenarios/plugins/docs_list/node_modules/@project-gauntlet/tools": { "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", - "integrity": "sha512-TfoUHFRis1q0MIJN/VfPJhHDczLkPbXVUgMNQMiWh+/l9r4YSobofJWxFzGaH9Sru+CmWJrXrS0lvlo3bGahRw==", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", "dev": true, "dependencies": { "@grpc/grpc-js": "^1.12.5", @@ -4239,19 +4043,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "scenarios/plugins/docs_list/node_modules/rollup-plugin-cleandir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-cleandir/-/rollup-plugin-cleandir-3.0.0.tgz", - "integrity": "sha512-+1AlxObWpWechyLVcnpjbxBiYlQg7bXF7vw5fu6P9B0B8R4meQliG7aGCnK8MvtdXzKsjeI0lJc43d0UcQj1fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mstssk/cleandir": "^2.0.0" - }, - "peerDependencies": { - "rollup": ">=4.0.0" - } - }, "tools": { "name": "@project-gauntlet/tools", "version": "0.9.0", diff --git a/package.json b/package.json index c26abc2..d1e35bf 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "js/typings", "js/build", "js/api_build", - "js/deno", "js/api", "js/react", "js/core", diff --git a/scenarios/plugins/docs_detail/package.json b/scenarios/plugins/docs_detail/package.json index d138b3a..1ebfe64 100644 --- a/scenarios/plugins/docs_detail/package.json +++ b/scenarios/plugins/docs_detail/package.json @@ -10,8 +10,8 @@ }, "devDependencies": { "@types/react": "^18.2.14", - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@types/deno": "^2.0.0", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_detail/tsconfig.json b/scenarios/plugins/docs_detail/tsconfig.json index cbe7961..f9bb627 100644 --- a/scenarios/plugins/docs_detail/tsconfig.json +++ b/scenarios/plugins/docs_detail/tsconfig.json @@ -5,8 +5,7 @@ "esModuleInterop": true, "target": "ES2022", "moduleResolution": "bundler", - "jsx": "react-jsx", - "types": ["@project-gauntlet/deno"] + "jsx": "react-jsx" }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/scenarios/plugins/docs_form/package.json b/scenarios/plugins/docs_form/package.json index d2b9349..0972129 100644 --- a/scenarios/plugins/docs_form/package.json +++ b/scenarios/plugins/docs_form/package.json @@ -10,8 +10,8 @@ }, "devDependencies": { "@types/react": "^18.2.14", - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@types/deno": "^2.0.0", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_form/tsconfig.json b/scenarios/plugins/docs_form/tsconfig.json index cbe7961..c700979 100644 --- a/scenarios/plugins/docs_form/tsconfig.json +++ b/scenarios/plugins/docs_form/tsconfig.json @@ -6,7 +6,6 @@ "target": "ES2022", "moduleResolution": "bundler", "jsx": "react-jsx", - "types": ["@project-gauntlet/deno"] }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/scenarios/plugins/docs_grid/package.json b/scenarios/plugins/docs_grid/package.json index e0b50e9..7d631b3 100644 --- a/scenarios/plugins/docs_grid/package.json +++ b/scenarios/plugins/docs_grid/package.json @@ -10,8 +10,8 @@ }, "devDependencies": { "@types/react": "^18.2.14", - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@types/deno": "^2.0.0", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_grid/tsconfig.json b/scenarios/plugins/docs_grid/tsconfig.json index cbe7961..f9bb627 100644 --- a/scenarios/plugins/docs_grid/tsconfig.json +++ b/scenarios/plugins/docs_grid/tsconfig.json @@ -5,8 +5,7 @@ "esModuleInterop": true, "target": "ES2022", "moduleResolution": "bundler", - "jsx": "react-jsx", - "types": ["@project-gauntlet/deno"] + "jsx": "react-jsx" }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/scenarios/plugins/docs_inline_separators/package.json b/scenarios/plugins/docs_inline_separators/package.json index e21a2f5..9715fcb 100644 --- a/scenarios/plugins/docs_inline_separators/package.json +++ b/scenarios/plugins/docs_inline_separators/package.json @@ -10,8 +10,8 @@ }, "devDependencies": { "@types/react": "^18.2.14", - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@types/deno": "^2.0.0", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_inline_separators/tsconfig.json b/scenarios/plugins/docs_inline_separators/tsconfig.json index cbe7961..f9bb627 100644 --- a/scenarios/plugins/docs_inline_separators/tsconfig.json +++ b/scenarios/plugins/docs_inline_separators/tsconfig.json @@ -5,8 +5,7 @@ "esModuleInterop": true, "target": "ES2022", "moduleResolution": "bundler", - "jsx": "react-jsx", - "types": ["@project-gauntlet/deno"] + "jsx": "react-jsx" }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/scenarios/plugins/docs_inline_three_sections/package.json b/scenarios/plugins/docs_inline_three_sections/package.json index 49a2d5d..29a5f6d 100644 --- a/scenarios/plugins/docs_inline_three_sections/package.json +++ b/scenarios/plugins/docs_inline_three_sections/package.json @@ -10,8 +10,8 @@ }, "devDependencies": { "@types/react": "^18.2.14", - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@types/deno": "^2.0.0", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_inline_three_sections/tsconfig.json b/scenarios/plugins/docs_inline_three_sections/tsconfig.json index cbe7961..f9bb627 100644 --- a/scenarios/plugins/docs_inline_three_sections/tsconfig.json +++ b/scenarios/plugins/docs_inline_three_sections/tsconfig.json @@ -5,8 +5,7 @@ "esModuleInterop": true, "target": "ES2022", "moduleResolution": "bundler", - "jsx": "react-jsx", - "types": ["@project-gauntlet/deno"] + "jsx": "react-jsx" }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/scenarios/plugins/docs_inline_two_sections/package.json b/scenarios/plugins/docs_inline_two_sections/package.json index 55e0c06..bf55c1c 100644 --- a/scenarios/plugins/docs_inline_two_sections/package.json +++ b/scenarios/plugins/docs_inline_two_sections/package.json @@ -10,8 +10,8 @@ }, "devDependencies": { "@types/react": "^18.2.14", - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@types/deno": "^2.0.0", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_inline_two_sections/tsconfig.json b/scenarios/plugins/docs_inline_two_sections/tsconfig.json index cbe7961..f9bb627 100644 --- a/scenarios/plugins/docs_inline_two_sections/tsconfig.json +++ b/scenarios/plugins/docs_inline_two_sections/tsconfig.json @@ -5,8 +5,7 @@ "esModuleInterop": true, "target": "ES2022", "moduleResolution": "bundler", - "jsx": "react-jsx", - "types": ["@project-gauntlet/deno"] + "jsx": "react-jsx" }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/scenarios/plugins/docs_list/package.json b/scenarios/plugins/docs_list/package.json index 020837d..cc87c1f 100644 --- a/scenarios/plugins/docs_list/package.json +++ b/scenarios/plugins/docs_list/package.json @@ -10,8 +10,8 @@ }, "devDependencies": { "@types/react": "^18.2.14", - "@project-gauntlet/deno": "file:../../js/deno", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#b2e5981942bc963102439441879571f54bae7fe5", + "@types/deno": "^2.0.0", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", "typescript": "^5.7.2" } } diff --git a/scenarios/plugins/docs_list/tsconfig.json b/scenarios/plugins/docs_list/tsconfig.json index cbe7961..f9bb627 100644 --- a/scenarios/plugins/docs_list/tsconfig.json +++ b/scenarios/plugins/docs_list/tsconfig.json @@ -5,8 +5,7 @@ "esModuleInterop": true, "target": "ES2022", "moduleResolution": "bundler", - "jsx": "react-jsx", - "types": ["@project-gauntlet/deno"] + "jsx": "react-jsx" }, "lib": ["ES2020"] } \ No newline at end of file From 536661e7d02c3dd18a50838f62f63c2236610779 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:24:05 +0100 Subject: [PATCH 220/540] Update iced-layershell to fix choppy loading bar and missing window focused event --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f83ada..a1a636b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5191,7 +5191,7 @@ dependencies = [ [[package]] name = "iced_layershell" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#bc47b24351a4a0946b03a1dec7e586ddde408b7d" +source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#7df6d555dd018b0df400dfc814f664da8f01958e" dependencies = [ "futures", "iced", @@ -5210,7 +5210,7 @@ dependencies = [ [[package]] name = "iced_layershell_macros" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#bc47b24351a4a0946b03a1dec7e586ddde408b7d" +source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#7df6d555dd018b0df400dfc814f664da8f01958e" dependencies = [ "darling", "manyhow", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "layershellev" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#bc47b24351a4a0946b03a1dec7e586ddde408b7d" +source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#7df6d555dd018b0df400dfc814f664da8f01958e" dependencies = [ "bitflags 2.6.0", "calloop 0.14.1", @@ -11915,7 +11915,7 @@ dependencies = [ [[package]] name = "waycrate_xkbkeycode" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#bc47b24351a4a0946b03a1dec7e586ddde408b7d" +source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#7df6d555dd018b0df400dfc814f664da8f01958e" dependencies = [ "bitflags 2.6.0", "calloop 0.14.1", From a54a760cd4384a7ed9d9a2baf697d478cba4c53b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:36:54 +0100 Subject: [PATCH 221/540] Update all rust dependencies --- Cargo.lock | 2837 +++++++---------- Cargo.toml | 47 +- js/api_build/package.json | 2 +- js/scenario_runner_cli/src/main.ts | 4 +- rust/cli/Cargo.toml | 27 +- rust/cli/src/lib.rs | 7 +- rust/client/Cargo.toml | 45 +- rust/client/build.rs | 15 +- rust/client/src/global_shortcut.rs | 2 +- rust/client/src/lib.rs | 8 +- rust/client/src/model.rs | 2 +- rust/client/src/ui/client_context.rs | 2 +- rust/client/src/ui/mod.rs | 12 +- rust/client/src/ui/search_list.rs | 2 +- rust/client/src/ui/state/main_view.rs | 2 +- rust/client/src/ui/state/mod.rs | 2 +- rust/client/src/ui/state/plugin_view.rs | 2 +- rust/client/src/ui/theme/container.rs | 8 +- rust/client/src/ui/theme/mod.rs | 2 +- rust/client/src/ui/theme/row.rs | 4 +- rust/client/src/ui/widget.rs | 6 +- rust/client/src/ui/widget_container.rs | 2 +- rust/common/Cargo.toml | 44 +- rust/common/build.rs | 4 +- rust/common/src/rpc/backend_api.rs | 7 +- rust/common/src/rpc/frontend_api.rs | 2 +- rust/common_ui/Cargo.toml | 9 +- rust/common_ui/src/lib.rs | 2 +- rust/component_model/Cargo.toml | 12 +- rust/component_model/src/main.rs | 2 +- rust/management_client/Cargo.toml | 23 +- .../src/components/shortcut_selector.rs | 4 +- rust/management_client/src/main.rs | 2 +- rust/management_client/src/ui.rs | 8 +- rust/management_client/src/views/general.rs | 4 +- rust/management_client/src/views/plugins.rs | 6 +- .../src/views/plugins/preferences.rs | 2 +- .../src/views/plugins/table.rs | 2 +- rust/plugin_runtime/Cargo.toml | 51 +- rust/plugin_runtime/src/api.rs | 4 +- rust/plugin_runtime/src/component_model.rs | 2 +- rust/plugin_runtime/src/deno.rs | 2 +- rust/plugin_runtime/src/events.rs | 2 +- rust/plugin_runtime/src/lib.rs | 4 +- rust/plugin_runtime/src/model.rs | 2 +- rust/plugin_runtime/src/permissions.rs | 2 +- rust/plugin_runtime/src/plugin_data.rs | 2 +- rust/plugin_runtime/src/preferences.rs | 2 +- rust/plugin_runtime/src/ui.rs | 6 +- rust/scenario_runner/Cargo.toml | 20 +- rust/scenario_runner/src/frontend_mock.rs | 17 +- rust/scenario_runner/src/lib.rs | 4 +- rust/server/Cargo.toml | 77 +- rust/server/src/lib.rs | 16 +- rust/server/src/model.rs | 2 +- rust/server/src/plugins/clipboard.rs | 2 +- rust/server/src/plugins/config_reader.rs | 2 +- rust/server/src/plugins/data_db_repository.rs | 4 +- rust/server/src/plugins/download_status.rs | 2 +- rust/server/src/plugins/icon_cache.rs | 2 +- rust/server/src/plugins/image_gatherer.rs | 4 +- rust/server/src/plugins/js.rs | 12 +- rust/server/src/plugins/loader.rs | 6 +- rust/server/src/plugins/mod.rs | 12 +- rust/server/src/plugins/run_status.rs | 2 +- rust/server/src/rpc.rs | 6 +- rust/server/src/search.rs | 4 +- rust/utils/Cargo.toml | 9 +- src/main.rs | 2 +- 69 files changed, 1573 insertions(+), 1884 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1a636b..e3d6917 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "ab_glyph" -version = "0.2.25" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f90148830dac590fac7ccfe78ec4a8ea404c60f75a24e16407a71f0f40de775" +checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -30,9 +30,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-activity" @@ -197,7 +197,7 @@ dependencies = [ "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -223,50 +223,51 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -289,28 +290,27 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" [[package]] name = "arboard" -version = "3.2.1" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac57f2b058a76363e357c056e4f74f1945bf734d37b8b3ef49066c4787dde0fc" +checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" dependencies = [ - "clipboard-win 4.5.0", - "core-graphics 0.22.3", - "image 0.24.9", + "clipboard-win", + "core-graphics 0.23.2", + "image 0.25.5", "log", - "objc", - "objc-foundation", - "objc_id", + "objc2", + "objc2-app-kit", + "objc2-foundation", "parking_lot 0.12.3", - "thiserror", - "winapi", + "windows-sys 0.48.0", "wl-clipboard-rs", - "x11rb 0.10.1", + "x11rb", ] [[package]] @@ -327,14 +327,14 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" @@ -366,7 +366,7 @@ version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ - "libloading 0.8.3", + "libloading 0.8.6", ] [[package]] @@ -381,7 +381,7 @@ dependencies = [ "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -417,7 +417,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -446,11 +446,11 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.8" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" dependencies = [ - "brotli 4.0.0", + "brotli 7.0.0", "flate2", "futures-core", "memchr", @@ -539,7 +539,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -562,9 +562,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -573,13 +573,13 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -590,20 +590,20 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "atk" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" dependencies = [ "atk-sys", "glib", @@ -612,9 +612,9 @@ dependencies = [ [[package]] name = "atk-sys" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" dependencies = [ "glib-sys", "gobject-sys", @@ -643,7 +643,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "184f5e6cce583a9db6b6f8d772a42cfce5b78e7c3ef26118cec3ce4c8c14969b" dependencies = [ - "http 1.1.0", + "http 1.2.0", "log", "rustls 0.22.4", "url", @@ -657,15 +657,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" dependencies = [ "dirs 4.0.0", - "thiserror", + "thiserror 1.0.69", "winreg 0.10.1", ] [[package]] name = "autocfg" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "av1-grain" @@ -683,52 +683,24 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" dependencies = [ "arrayvec", ] [[package]] name = "axum" -version = "0.6.20" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core 0.3.4", - "bitflags 1.3.2", + "axum-core", "bytes", "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper 0.1.2", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "http-body-util", "itoa", @@ -739,25 +711,8 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper 1.0.2", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "mime", - "rustversion", + "sync_wrapper", + "tower 0.5.2", "tower-layer", "tower-service", ] @@ -771,30 +726,30 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.2", + "sync_wrapper", "tower-layer", "tower-service", ] [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide 0.7.2", + "miniz_oxide 0.8.2", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -817,9 +772,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64-simd" @@ -857,9 +812,9 @@ dependencies = [ [[package]] name = "better_scoped_tls" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794edcc9b3fb07bb4aecaa11f093fd45663b4feadb782d68303a2268bc2701de" +checksum = "297b153aa5e573b5863108a6ddc9d5c968bd0b20e75cc614ee9821d2f45679c7" dependencies = [ "scoped-tls", ] @@ -909,7 +864,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -974,9 +929,9 @@ dependencies = [ [[package]] name = "bitstream-io" -version = "2.2.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06c9989a51171e2e81038ab168b6ae22886fe9ded214430dbb4f41c28cf176da" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" [[package]] name = "bitvec" @@ -1052,18 +1007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17d4f95e880cfd28c4ca5a006cf7f6af52b4bcb7b5866f573b2faa126fb7affb" dependencies = [ "quote", - "syn 2.0.89", -] - -[[package]] -name = "brotli" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor 3.0.0", + "syn 2.0.90", ] [[package]] @@ -1074,17 +1018,18 @@ checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", - "brotli-decompressor 4.0.1", + "brotli-decompressor", ] [[package]] -name = "brotli-decompressor" -version = "3.0.0" +name = "brotli" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", + "brotli-decompressor", ] [[package]] @@ -1099,9 +1044,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.9.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" dependencies = [ "memchr", "serde", @@ -1109,9 +1054,9 @@ dependencies = [ [[package]] name = "built" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41bfbdb21256b87a8b5e80fab81a8eed158178e812fd7ba451907518b2742f16" +checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" [[package]] name = "bumpalo" @@ -1122,6 +1067,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "bytemuck" version = "1.20.0" @@ -1133,13 +1084,13 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" +checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -1156,9 +1107,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cacao" @@ -1195,7 +1146,7 @@ dependencies = [ "glib", "libc", "once_cell", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1220,7 +1171,7 @@ dependencies = [ "polling", "rustix", "slab", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1234,14 +1185,14 @@ dependencies = [ "polling", "rustix", "slab", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "calloop" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ead1e1514bce44c0f40e027899fbc595907fc112635bed21b3b5d975c0a5e7" +checksum = "10929724661d1c43856fd87c7a127ae944ec55579134fb485e4136fb6a46fdcb" dependencies = [ "bitflags 2.6.0", "polling", @@ -1259,7 +1210,7 @@ dependencies = [ "calloop 0.12.4", "rustix", "wayland-backend", - "wayland-client 0.31.7", + "wayland-client", ] [[package]] @@ -1271,7 +1222,7 @@ dependencies = [ "calloop 0.13.0", "rustix", "wayland-backend", - "wayland-client 0.31.7", + "wayland-client", ] [[package]] @@ -1280,10 +1231,10 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a7a1dbbe026a55ef47a500b123af5a9a0914520f061d467914cf21be95daf" dependencies = [ - "calloop 0.14.1", + "calloop 0.14.2", "rustix", "wayland-backend", - "wayland-client 0.31.7", + "wayland-client", ] [[package]] @@ -1297,25 +1248,25 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" dependencies = [ "serde", ] [[package]] name = "cargo_metadata" -version = "0.18.1" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +checksum = "8769706aad5d996120af43197bf46ef6ad0fda35216b4505f926a365a232d924" dependencies = [ "camino", "cargo-platform", - "semver 1.0.22", + "semver 1.0.24", "serde", "serde_json", - "thiserror", + "thiserror 2.0.8", ] [[package]] @@ -1329,9 +1280,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.3" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" dependencies = [ "jobserver", "libc", @@ -1420,14 +1371,14 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.3", + "libloading 0.8.6", ] [[package]] name = "clap" -version = "4.5.4" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -1435,9 +1386,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -1447,83 +1398,29 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" - -[[package]] -name = "cli" -version = "0.0.0" -dependencies = [ - "anyhow", - "auto-launch", - "clap", - "client", - "management_client", - "server", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "client" -version = "0.0.0" -dependencies = [ - "anyhow", - "bytes", - "common", - "common-ui", - "component_model", - "convert_case", - "global-hotkey", - "iced", - "iced_aw", - "iced_fonts", - "iced_layershell", - "image 0.25.1", - "itertools 0.12.1", - "once_cell", - "serde", - "serde_json", - "thiserror", - "tokio", - "tonic 0.11.0", - "tracing", - "tracing-subscriber", - "tray-icon", - "utils", -] +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clipboard-win" -version = "4.5.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ - "error-code 2.3.1", - "str-buf", - "winapi", -] - -[[package]] -name = "clipboard-win" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" -dependencies = [ - "error-code 3.2.0", + "error-code", ] [[package]] @@ -1552,38 +1449,8 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" dependencies = [ - "thiserror", - "x11rb 0.13.1", -] - -[[package]] -name = "cocoa" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" -dependencies = [ - "bitflags 2.6.0", - "block", - "cocoa-foundation", - "core-foundation 0.10.0", - "core-graphics 0.24.0", - "foreign-types 0.5.0", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" -dependencies = [ - "bitflags 2.6.0", - "block", - "core-foundation 0.10.0", - "core-graphics-types 0.2.0", - "libc", - "objc", + "thiserror 1.0.69", + "x11rb", ] [[package]] @@ -1614,7 +1481,7 @@ dependencies = [ "nom 7.1.3", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -1625,9 +1492,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "combine" @@ -1639,50 +1506,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "common" -version = "0.0.0" -dependencies = [ - "anyhow", - "base64 0.22.0", - "bincode 2.0.0-rc.3", - "bytes", - "component_model", - "convert_case", - "directories", - "gix-url", - "indexmap 2.2.6", - "itertools 0.10.5", - "prost 0.12.4", - "serde", - "serde_json", - "thiserror", - "tokio", - "tonic 0.11.0", - "tonic-build", - "utils", -] - -[[package]] -name = "common-ui" -version = "0.0.0" -dependencies = [ - "common", - "iced", - "iced_aw", - "iced_fonts", -] - -[[package]] -name = "component_model" -version = "0.0.0" -dependencies = [ - "anyhow", - "indexmap 2.2.6", - "serde", - "serde_json", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1845,9 +1668,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -1869,27 +1692,27 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1906,18 +1729,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -1979,8 +1802,8 @@ dependencies = [ "cpufeatures", "curve25519-dalek-derive", "digest", - "fiat-crypto 0.2.7", - "rustc_version 0.4.0", + "fiat-crypto 0.2.9", + "rustc_version 0.4.1", "subtle", "zeroize", ] @@ -1993,7 +1816,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -2003,7 +1826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" dependencies = [ "bitflags 2.6.0", - "libloading 0.8.3", + "libloading 0.8.6", "winapi", ] @@ -2044,7 +1867,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -2055,7 +1878,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -2068,14 +1891,14 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.9", + "parking_lot_core 0.9.10", ] [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "data-url" @@ -2145,7 +1968,7 @@ dependencies = [ "swc_visit", "swc_visit_macros", "text_lines", - "thiserror", + "thiserror 1.0.69", "unicode-width", "url", ] @@ -2158,7 +1981,7 @@ checksum = "348ecdacfdd262e6b2f9740d07a41e8f4d79d06a670378a060515d0208495c9f" dependencies = [ "async-trait", "deno_core", - "thiserror", + "thiserror 1.0.69", "tokio", "uuid", ] @@ -2174,7 +1997,7 @@ dependencies = [ "rusqlite", "serde", "sha2", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -2188,7 +2011,7 @@ dependencies = [ "deno_webgpu", "image 0.24.9", "serde", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2216,9 +2039,9 @@ dependencies = [ "deno_ops", "deno_unsync", "futures", - "indexmap 2.2.6", + "indexmap 2.7.0", "libc", - "memoffset 0.9.1", + "memoffset", "parking_lot 0.12.3", "percent-encoding", "pin-project", @@ -2251,7 +2074,7 @@ dependencies = [ "chrono", "deno_core", "saffron", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -2288,7 +2111,7 @@ dependencies = [ "sha2", "signature", "spki", - "thiserror", + "thiserror 1.0.69", "tokio", "uuid", "x25519-dalek", @@ -2309,9 +2132,9 @@ dependencies = [ "dyn-clone", "error_reporter", "hickory-resolver", - "http 1.1.0", + "http 1.2.0", "http-body-util", - "hyper 1.5.1", + "hyper 1.5.2", "hyper-rustls", "hyper-util", "ipnet", @@ -2319,12 +2142,12 @@ dependencies = [ "rustls-webpki", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-rustls", "tokio-socks", "tokio-util", - "tower", + "tower 0.4.13", "tower-http", "tower-service", ] @@ -2346,7 +2169,7 @@ dependencies = [ "serde", "serde-value", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", "winapi", ] @@ -2371,7 +2194,7 @@ dependencies = [ "rand", "rayon", "serde", - "thiserror", + "thiserror 1.0.69", "winapi", "windows-sys 0.52.0", ] @@ -2393,10 +2216,10 @@ dependencies = [ "deno_websocket", "flate2", "http 0.2.12", - "http 1.1.0", + "http 1.2.0", "httparse", - "hyper 0.14.28", - "hyper 1.5.1", + "hyper 0.14.32", + "hyper 1.5.2", "hyper-util", "itertools 0.10.5", "memmem", @@ -2409,7 +2232,7 @@ dependencies = [ "scopeguard", "serde", "smallvec", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-util", ] @@ -2457,17 +2280,17 @@ dependencies = [ "denokv_proto", "denokv_remote", "denokv_sqlite", - "faster-hex 0.9.0", - "http 1.1.0", + "faster-hex", + "http 1.2.0", "http-body-util", "log", "num-bigint", - "prost 0.13.4", - "prost-build 0.13.4", + "prost", + "prost-build", "rand", "rusqlite", "serde", - "thiserror", + "thiserror 1.0.69", "url", ] @@ -2494,7 +2317,7 @@ dependencies = [ "libloading 0.7.4", "log", "napi_sym", - "thiserror", + "thiserror 1.0.69", "windows-sys 0.52.0", ] @@ -2526,7 +2349,7 @@ dependencies = [ "rustls-tokio-stream", "serde", "socket2", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -2565,16 +2388,16 @@ dependencies = [ "ed25519-dalek", "elliptic-curve", "errno 0.2.8", - "faster-hex 0.9.0", + "faster-hex", "h2 0.4.7", "hkdf", "home", - "http 1.1.0", + "http 1.2.0", "http-body-util", - "hyper 1.5.1", + "hyper 1.5.2", "hyper-util", "idna", - "indexmap 2.2.6", + "indexmap 2.7.0", "ipnetwork", "k256", "lazy-regex", @@ -2612,7 +2435,7 @@ dependencies = [ "sm3", "spki", "stable_deref_trait", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-eld", "url", @@ -2636,8 +2459,8 @@ dependencies = [ "stringcase", "strum", "strum_macros", - "syn 2.0.89", - "thiserror", + "syn 2.0.90", + "thiserror 1.0.69", ] [[package]] @@ -2647,10 +2470,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6cbc4c4d3eb0960b58e8f43f9fc2d3f620fcac9a03cd85203e08db5b04e83c1f" dependencies = [ "deno_semver", - "indexmap 2.2.6", + "indexmap 2.7.0", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "url", ] @@ -2661,7 +2484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff25f6e08e7a0214bbacdd6f7195c7f1ebcd850c87a624e4ff06326b68b42d99" dependencies = [ "percent-encoding", - "thiserror", + "thiserror 1.0.69", "url", ] @@ -2680,7 +2503,7 @@ dependencies = [ "once_cell", "percent-encoding", "serde", - "thiserror", + "thiserror 1.0.69", "which 4.4.2", "winapi", ] @@ -2724,10 +2547,10 @@ dependencies = [ "encoding_rs", "fastwebsockets", "flate2", - "http 1.1.0", + "http 1.2.0", "http-body-util", - "hyper 0.14.28", - "hyper 1.5.1", + "hyper 0.14.32", + "hyper 1.5.2", "hyper-util", "libc", "log", @@ -2751,7 +2574,7 @@ dependencies = [ "signal-hook", "signal-hook-registry", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-metrics", "twox-hash", @@ -2770,7 +2593,7 @@ dependencies = [ "monch", "once_cell", "serde", - "thiserror", + "thiserror 1.0.69", "url", ] @@ -2807,7 +2630,7 @@ dependencies = [ "rustls-tokio-stream", "rustls-webpki", "serde", - "thiserror", + "thiserror 1.0.69", "tokio", "webpki-roots", ] @@ -2830,7 +2653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad9a108794e505f2b07665e19ff336c1bcba6adcf7182c90c1d3a6c741d7fcd0" dependencies = [ "deno_core", - "thiserror", + "thiserror 1.0.69", "urlpattern", ] @@ -2849,7 +2672,7 @@ dependencies = [ "flate2", "futures", "serde", - "thiserror", + "thiserror 1.0.69", "tokio", "uuid", ] @@ -2863,7 +2686,7 @@ dependencies = [ "deno_core", "raw-window-handle", "serde", - "thiserror", + "thiserror 1.0.69", "tokio", "wgpu-core 0.21.1", "wgpu-types 0.20.0", @@ -2891,14 +2714,14 @@ dependencies = [ "deno_tls", "fastwebsockets", "h2 0.4.7", - "http 1.1.0", + "http 1.2.0", "http-body-util", - "hyper 1.5.1", + "hyper 1.5.2", "hyper-util", "once_cell", "rustls-tokio-stream", "serde", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -2911,7 +2734,7 @@ dependencies = [ "deno_core", "deno_web", "rusqlite", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2935,7 +2758,7 @@ dependencies = [ "chrono", "futures", "num-bigint", - "prost 0.13.4", + "prost", "serde", "uuid", ] @@ -2953,9 +2776,9 @@ dependencies = [ "chrono", "denokv_proto", "futures", - "http 1.1.0", + "http 1.2.0", "log", - "prost 0.13.4", + "prost", "rand", "serde", "serde_json", @@ -2983,7 +2806,7 @@ dependencies = [ "rand", "rusqlite", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "uuid", @@ -3024,7 +2847,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -3039,44 +2862,44 @@ dependencies = [ [[package]] name = "derive-new" -version = "0.5.9" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" +checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.90", ] [[package]] name = "derive_builder" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "derive_builder_macro" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -3155,13 +2978,13 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "displaydoc" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -3170,7 +2993,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.8.3", + "libloading 0.8.6", ] [[package]] @@ -3205,7 +3028,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -3312,7 +3135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" dependencies = [ "libc", - "linux-raw-sys 0.6.4", + "linux-raw-sys 0.6.5", ] [[package]] @@ -3426,9 +3249,9 @@ dependencies = [ [[package]] name = "either" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" dependencies = [ "serde", ] @@ -3487,7 +3310,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -3508,7 +3331,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -3540,12 +3363,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3560,19 +3383,9 @@ dependencies = [ [[package]] name = "error-code" -version = "2.3.1" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" -dependencies = [ - "libc", - "str-buf", -] - -[[package]] -name = "error-code" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "error_reporter" @@ -3582,9 +3395,9 @@ checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8" [[package]] name = "etagere" -version = "0.2.10" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "306960881d6c46bd0dd6b7f07442a441418c08d0d3e63d8d080b0f64c6343e4e" +checksum = "0e2f1e3be19fb10f549be8c1bf013e8675b4066c445e36eb76d2ebb2f54ee495" dependencies = [ "euclid", "svg_fmt", @@ -3603,9 +3416,9 @@ dependencies = [ [[package]] name = "euclid" -version = "0.22.9" +version = "0.22.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" dependencies = [ "num-traits", ] @@ -3623,9 +3436,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ "event-listener", "pin-project-lite", @@ -3633,15 +3446,14 @@ dependencies = [ [[package]] name = "exr" -version = "1.72.0" +version = "1.73.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" dependencies = [ "bit_field", - "flume", "half", "lebe", - "miniz_oxide 0.7.2", + "miniz_oxide 0.8.2", "rayon-core", "smallvec", "zune-inflate", @@ -3667,18 +3479,9 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "fastdivide" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59668941c55e5c186b8b58c391629af56774ec768f73c08bbcd56f09348eb00b" - -[[package]] -name = "faster-hex" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239f7bfb930f820ab16a9cd95afc26f88264cf6905c960b340a615384aa3338a" -dependencies = [ - "serde", -] +checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" [[package]] name = "faster-hex" @@ -3691,9 +3494,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fastwebsockets" @@ -3704,13 +3507,13 @@ dependencies = [ "base64 0.21.7", "bytes", "http-body-util", - "hyper 1.5.1", + "hyper 1.5.2", "hyper-util", "pin-project", "rand", "sha1", "simdutf8", - "thiserror", + "thiserror 1.0.69", "tokio", "utf-8", ] @@ -3728,9 +3531,9 @@ dependencies = [ [[package]] name = "fdeflate" -version = "0.3.4" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] @@ -3753,9 +3556,9 @@ checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" [[package]] name = "fiat-crypto" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "field-offset" @@ -3763,28 +3566,22 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ - "memoffset 0.9.1", - "rustc_version 0.4.0", + "memoffset", + "rustc_version 0.4.1", ] [[package]] name = "filetime" -version = "0.2.23" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", - "windows-sys 0.52.0", + "libredox", + "windows-sys 0.59.0", ] -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "fixedbitset" version = "0.4.2" @@ -3798,7 +3595,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide 0.8.2", ] [[package]] @@ -3818,13 +3615,13 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin", ] [[package]] @@ -3833,6 +3630,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "font-types" version = "0.7.3" @@ -3844,9 +3647,9 @@ dependencies = [ [[package]] name = "fontconfig-parser" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" +checksum = "c1fcfcd44ca6e90c921fee9fa665d530b21ef1327a4c1a6c5250ea44b776ada7" dependencies = [ "roxmltree", ] @@ -3892,7 +3695,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -3931,7 +3734,7 @@ dependencies = [ "dirs 5.0.1", "once_cell", "rust-ini 0.20.0", - "thiserror", + "thiserror 1.0.69", "xdg", ] @@ -3942,7 +3745,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4" dependencies = [ "nom 7.1.3", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3953,7 +3756,7 @@ checksum = "32016f1242eb82af5474752d00fd8ebcd9004bd69b462b1c91de833972d08ed4" dependencies = [ "proc-macro2", "swc_macros_common", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -4083,7 +3886,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -4120,14 +3923,209 @@ dependencies = [ name = "gauntlet" version = "0.0.0" dependencies = [ - "cli", + "gauntlet-cli", +] + +[[package]] +name = "gauntlet-cli" +version = "0.0.0" +dependencies = [ + "anyhow", + "auto-launch", + "clap", + "gauntlet-client", + "gauntlet-management-client", + "gauntlet-server", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "gauntlet-client" +version = "0.0.0" +dependencies = [ + "anyhow", + "convert_case", + "gauntlet-common", + "gauntlet-common-ui", + "gauntlet-component-model", + "gauntlet-utils", + "global-hotkey", + "iced", + "iced_aw", + "iced_fonts", + "iced_layershell", + "image 0.25.5", + "itertools 0.13.0", + "once_cell", + "serde", + "serde_json", + "tokio", + "tracing", + "tray-icon", +] + +[[package]] +name = "gauntlet-common" +version = "0.0.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bincode 2.0.0-rc.3", + "bytes", + "convert_case", + "directories", + "gauntlet-component-model", + "gauntlet-utils", + "gix-url", + "indexmap 2.7.0", + "itertools 0.13.0", + "prost", + "serde", + "serde_json", + "thiserror 2.0.8", + "tokio", + "tonic", + "tonic-build", +] + +[[package]] +name = "gauntlet-common-ui" +version = "0.0.0" +dependencies = [ + "gauntlet-common", + "iced", + "iced_aw", + "iced_fonts", +] + +[[package]] +name = "gauntlet-component-model" +version = "0.0.0" +dependencies = [ + "anyhow", + "indexmap 2.7.0", + "serde", + "serde_json", +] + +[[package]] +name = "gauntlet-management-client" +version = "0.0.0" +dependencies = [ + "anyhow", + "gauntlet-common", + "gauntlet-common-ui", + "iced", + "iced_aw", + "iced_fonts", + "iced_table", + "itertools 0.13.0", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "gauntlet-plugin-runtime" +version = "0.1.0" +dependencies = [ + "anyhow", + "bincode 2.0.0-rc.3", + "bytes", + "cacao", + "deno_core", + "deno_runtime", + "freedesktop-icons", + "freedesktop_entry_parser", + "futures", + "gauntlet-common", + "gauntlet-component-model", + "gauntlet-utils", + "icns", + "image 0.25.5", + "indexmap 2.7.0", + "interprocess", + "libc", + "numbat", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "plist", + "regex", + "resvg", + "serde", + "tokio", + "tokio-util", + "tracing", + "typed-path", + "walkdir", +] + +[[package]] +name = "gauntlet-scenario-runner" +version = "0.0.0" +dependencies = [ + "anyhow", + "gauntlet-common", + "gauntlet-utils", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "gauntlet-server" +version = "0.0.0" +dependencies = [ + "anyhow", + "arboard", + "bytes", + "futures", + "gauntlet-client", + "gauntlet-common", + "gauntlet-plugin-runtime", + "gauntlet-scenario-runner", + "gauntlet-utils", + "git2", + "image 0.25.5", + "include_dir", + "interprocess", + "itertools 0.13.0", + "once_cell", + "open", + "regex", + "serde", + "sqlx", + "tantivy", + "tempfile", + "tokio", + "tokio-util", + "toml", + "tonic", + "tracing", + "typed-path", + "ureq", + "url", + "uuid", + "vergen-gitcl", + "vergen-pretty", + "walkdir", +] + +[[package]] +name = "gauntlet-utils" +version = "0.0.0" +dependencies = [ + "thiserror 2.0.8", + "tokio", ] [[package]] name = "gdk" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -4166,9 +4164,9 @@ dependencies = [ [[package]] name = "gdk-sys" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -4181,19 +4179,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "generator" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows 0.48.0", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -4205,16 +4190,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "gethostname" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "gethostname" version = "0.4.3" @@ -4227,9 +4202,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", @@ -4260,9 +4235,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gio" @@ -4280,7 +4255,7 @@ dependencies = [ "once_cell", "pin-project-lite", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -4313,9 +4288,9 @@ dependencies = [ [[package]] name = "gix-features" -version = "0.33.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f77decb545f63a52852578ef5f66ecd71017ffc1983d551d5fa2328d6d9817f" +checksum = "7d85d673f2e022a340dba4713bed77ef2cf4cd737d2f3e0f159d45e0935fd81f" dependencies = [ "gix-hash", "gix-trace", @@ -4324,44 +4299,43 @@ dependencies = [ [[package]] name = "gix-hash" -version = "0.12.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d4796bac3aaf0c2f8bea152ca924ae3bdc5f135caefe6431116bcd67e98eab9" +checksum = "0b5eccc17194ed0e67d49285e4853307e4147e95407f91c1c3e4a13ba9f4e4ce" dependencies = [ - "faster-hex 0.8.1", - "thiserror", + "faster-hex", + "thiserror 2.0.8", ] [[package]] name = "gix-path" -version = "0.9.0" +version = "0.10.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764b31ac54472e796f08be376eaeea3e30800949650566620809659d39969dbd" +checksum = "afc292ef1a51e340aeb0e720800338c805975724c1dfbd243185452efd8645b7" dependencies = [ "bstr", "gix-trace", "home", "once_cell", - "thiserror", + "thiserror 2.0.8", ] [[package]] name = "gix-trace" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f924267408915fddcd558e3f37295cc7d6a3e50f8bd8b606cee0808c3915157e" +checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952" [[package]] name = "gix-url" -version = "0.22.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6059e15828df32027a7db9097e5a9baf320d2dcc10a4e1598ffe05be8dfd1fa6" +checksum = "e09f97db3618fb8e473d7d97e77296b50aaee0ddcd6a867f07443e3e87391099" dependencies = [ "bstr", "gix-features", "gix-path", - "home", - "thiserror", + "thiserror 2.0.8", "url", ] @@ -4402,7 +4376,7 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -4416,7 +4390,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -4437,15 +4411,17 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "global-hotkey" -version = "0.4.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a00fd7c31d82029f99ce2481a8de1ae974758017d6a55ebbe7f22edcd1617" +checksum = "b00d88f1be7bf4cd2e61623ce08e84be2dfa4eab458e5d632d3dab95f16c1f64" dependencies = [ "crossbeam-channel", "keyboard-types", + "objc2", + "objc2-app-kit", "once_cell", - "thiserror", - "windows-sys 0.52.0", + "thiserror 1.0.69", + "windows-sys 0.59.0", "x11-dl", ] @@ -4499,7 +4475,7 @@ dependencies = [ "cosmic-text", "etagere", "lru", - "rustc-hash 2.0.0", + "rustc-hash 2.1.0", "wgpu", ] @@ -4541,19 +4517,19 @@ checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" dependencies = [ "log", "presser", - "thiserror", - "windows 0.58.0", + "thiserror 1.0.69", + "windows", ] [[package]] name = "gpu-descriptor" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" +checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca" dependencies = [ "bitflags 2.6.0", "gpu-descriptor-types", - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] @@ -4578,9 +4554,9 @@ dependencies = [ [[package]] name = "gtk" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" dependencies = [ "atk", "cairo-rs", @@ -4599,9 +4575,9 @@ dependencies = [ [[package]] name = "gtk-sys" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" dependencies = [ "atk-sys", "cairo-sys-rs", @@ -4617,15 +4593,15 @@ dependencies = [ [[package]] name = "gtk3-macros" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -4659,7 +4635,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -4677,8 +4653,8 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.1.0", - "indexmap 2.2.6", + "http 1.2.0", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -4726,9 +4702,14 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" @@ -4774,6 +4755,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -4804,7 +4791,7 @@ dependencies = [ "once_cell", "rand", "serde", - "thiserror", + "thiserror 1.0.69", "tinyvec", "tokio", "tracing", @@ -4828,7 +4815,7 @@ dependencies = [ "resolv-conf", "serde", "smallvec", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -4853,11 +4840,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4904,9 +4891,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -4931,7 +4918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http 1.2.0", ] [[package]] @@ -4942,16 +4929,16 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -4970,9 +4957,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -4994,15 +4981,15 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", "futures-util", "h2 0.4.7", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "httparse", "httpdate", @@ -5015,13 +5002,13 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "f6884a48c6826ec44f524c7456b163cebe9e55a18d7b5e307cb4f100371cc767" dependencies = [ "futures-util", - "http 1.1.0", - "hyper 1.5.1", + "http 1.2.0", + "hyper 1.5.2", "hyper-util", "rustls 0.23.20", "rustls-pki-types", @@ -5030,25 +5017,13 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-timeout" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" -dependencies = [ - "hyper 0.14.28", - "pin-project-lite", - "tokio", - "tokio-io-timeout", -] - [[package]] name = "hyper-timeout" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" dependencies = [ - "hyper 1.5.1", + "hyper 1.5.2", "hyper-util", "pin-project-lite", "tokio", @@ -5064,22 +5039,22 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", - "hyper 1.5.1", + "hyper 1.5.2", "pin-project-lite", "socket2", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -5109,7 +5084,7 @@ dependencies = [ "iced_widget", "iced_winit", "image 0.24.9", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5139,9 +5114,9 @@ dependencies = [ "num-traits", "once_cell", "palette", - "rustc-hash 2.0.0", + "rustc-hash 2.1.0", "smol_str", - "thiserror", + "thiserror 1.0.69", "web-time", ] @@ -5161,7 +5136,7 @@ dependencies = [ "futures", "iced_core", "log", - "rustc-hash 2.0.0", + "rustc-hash 2.1.0", "tokio", "wasm-bindgen-futures", "wasm-timer", @@ -5183,8 +5158,8 @@ dependencies = [ "log", "once_cell", "raw-window-handle", - "rustc-hash 2.0.0", - "thiserror", + "rustc-hash 2.1.0", + "thiserror 1.0.69", "unicode-segmentation", ] @@ -5202,7 +5177,7 @@ dependencies = [ "iced_renderer", "iced_runtime", "layershellev", - "thiserror", + "thiserror 1.0.69", "tracing", "window_clipboard", ] @@ -5216,7 +5191,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -5228,7 +5203,7 @@ dependencies = [ "iced_tiny_skia", "iced_wgpu", "log", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5240,7 +5215,7 @@ dependencies = [ "iced_core", "iced_futures", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5262,7 +5237,7 @@ dependencies = [ "iced_graphics", "kurbo 0.10.4", "log", - "rustc-hash 2.0.0", + "rustc-hash 2.1.0", "softbuffer", "tiny-skia", ] @@ -5281,8 +5256,8 @@ dependencies = [ "iced_graphics", "log", "once_cell", - "rustc-hash 2.0.0", - "thiserror", + "rustc-hash 2.1.0", + "thiserror 1.0.69", "wgpu", ] @@ -5296,8 +5271,8 @@ dependencies = [ "num-traits", "once_cell", "ouroboros", - "rustc-hash 2.0.0", - "thiserror", + "rustc-hash 2.1.0", + "thiserror 1.0.69", "unicode-segmentation", ] @@ -5310,8 +5285,8 @@ dependencies = [ "iced_graphics", "iced_runtime", "log", - "rustc-hash 2.0.0", - "thiserror", + "rustc-hash 2.1.0", + "thiserror 1.0.69", "tracing", "wasm-bindgen-futures", "web-sys", @@ -5445,7 +5420,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -5494,25 +5469,25 @@ dependencies = [ "gif", "jpeg-decoder", "num-traits", - "png 0.17.13", + "png 0.17.16", "qoi", "tiff", ] [[package]] name = "image" -version = "0.25.1" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", - "byteorder", + "byteorder-lite", "color_quant", "exr", "gif", "image-webp", "num-traits", - "png 0.17.13", + "png 0.17.16", "qoi", "ravif", "rayon", @@ -5524,40 +5499,40 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d730b085583c4d789dfd07fdcf185be59501666a90c97c40162b37e4fdad272d" +checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" dependencies = [ "byteorder-lite", - "thiserror", + "quick-error 2.0.1", ] [[package]] name = "imagesize" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "imgref" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "include_dir" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" dependencies = [ "include_dir_macros", ] [[package]] name = "include_dir_macros" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" dependencies = [ "proc-macro2", "quote", @@ -5576,12 +5551,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.2", "serde", ] @@ -5617,9 +5592,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", "js-sys", @@ -5635,7 +5610,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -5667,9 +5642,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "ipnetwork" @@ -5691,14 +5666,14 @@ dependencies = [ [[package]] name = "is-macro" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a85abdc13717906baccb5a1e435556ce0df215f242892f721dff62bf25288f" +checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4" dependencies = [ - "Inflector", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -5711,6 +5686,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -5740,15 +5721,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jiff" -version = "0.1.13" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a45489186a6123c128fdf6016183fcfab7113e1820eb813127e036e287233fb" +checksum = "db69f08d4fb10524cacdb074c10b296299d71274ddbc830a8ee65666867002e9" dependencies = [ "jiff-tzdb-platform", "js-sys", @@ -5782,7 +5763,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -5795,9 +5776,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.30" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685a7d121ee3f65ae4fddd72b25a04bb36b6af81bc0828f7d5434c0fe60fa3a2" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] @@ -5813,10 +5794,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -5880,7 +5862,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.8.3", + "libloading 0.8.6", "pkg-config", ] @@ -5936,15 +5918,15 @@ version = "0.13.99" source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#7df6d555dd018b0df400dfc814f664da8f01958e" dependencies = [ "bitflags 2.6.0", - "calloop 0.14.1", + "calloop 0.14.2", "calloop-wayland-source 0.4.0", "raw-window-handle", "tempfile", - "thiserror", + "thiserror 1.0.69", "tracing", "waycrate_xkbkeycode", "wayland-backend", - "wayland-client 0.31.7", + "wayland-client", "wayland-cursor", "wayland-protocols 0.32.5", "wayland-protocols-misc", @@ -5953,9 +5935,9 @@ dependencies = [ [[package]] name = "lazy-regex" -version = "3.1.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c" +checksum = "8d8e41c97e6bc7ecb552016274b99fbb5d035e8de288c582d9b933af6677bfda" dependencies = [ "lazy-regex-proc_macros", "once_cell", @@ -5964,23 +5946,23 @@ dependencies = [ [[package]] name = "lazy-regex-proc_macros" -version = "3.1.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b" +checksum = "76e1d8b05d672c53cb9c7b920bbba8783845ae4f0b076e02a3db1d02c81b4163" dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin", ] [[package]] @@ -6021,9 +6003,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.164" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libffi" @@ -6046,13 +6028,12 @@ dependencies = [ [[package]] name = "libfuzzer-sys" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" dependencies = [ "arbitrary", "cc", - "once_cell", ] [[package]] @@ -6081,9 +6062,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.52.6", @@ -6091,20 +6072,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - -[[package]] -name = "libredox" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" -dependencies = [ - "bitflags 2.6.0", - "libc", - "redox_syscall 0.4.1", -] +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -6114,6 +6084,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", + "redox_syscall 0.5.8", ] [[package]] @@ -6153,12 +6124,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "line-wrap" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -6173,9 +6138,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "linux-raw-sys" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b5399f6804fbab912acbd8878ed3532d506b7c951b8f9f164ef90fef39e3f4" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" [[package]] name = "litemap" @@ -6191,9 +6156,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -6205,20 +6170,6 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" -[[package]] -name = "loom" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if", - "generator", - "pin-utils", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "loop9" version = "0.1.5" @@ -6230,11 +6181,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] @@ -6261,26 +6212,6 @@ dependencies = [ "libc", ] -[[package]] -name = "management_client" -version = "0.0.0" -dependencies = [ - "anyhow", - "clap", - "common", - "common-ui", - "iced", - "iced_aw", - "iced_fonts", - "iced_table", - "itertools 0.12.1", - "thiserror", - "tokio", - "tonic 0.11.0", - "tracing", - "tracing-subscriber", -] - [[package]] name = "manyhow" version = "0.11.4" @@ -6291,7 +6222,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -6357,9 +6288,9 @@ dependencies = [ [[package]] name = "measure_time" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" +checksum = "dbefd235b0aadd181626f281e1d684e116972988c14c264e42069d5e8a5775cc" dependencies = [ "instant", "log", @@ -6395,15 +6326,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -6415,9 +6337,9 @@ dependencies = [ [[package]] name = "mendeleev" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8312069eadd5b2cb9ff1bbd3874927d0cc60007ef608507cf8a59920b8a140fd" +checksum = "7f8dd6ec5207f7f69db7abb42466511394956dc85faf163de1fe393246c8b7e4" dependencies = [ "serde", ] @@ -6485,21 +6407,21 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", - "simd-adler32", ] [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -6514,6 +6436,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "monch" version = "0.5.0" @@ -6522,19 +6455,20 @@ checksum = "b52c1b33ff98142aecea13138bd399b68aa7ab5d9546c300988c345004001eea" [[package]] name = "muda" -version = "0.14.1" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" +checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" dependencies = [ - "cocoa", "crossbeam-channel", "dpi 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "gtk", "keyboard-types", - "objc", + "objc2", + "objc2-app-kit", + "objc2-foundation", "once_cell", - "png 0.17.13", - "thiserror", + "png 0.17.16", + "thiserror 1.0.69", "windows-sys 0.59.0", ] @@ -6567,22 +6501,22 @@ dependencies = [ "bitflags 2.6.0", "codespan-reporting", "hexf-parse", - "indexmap 2.2.6", + "indexmap 2.7.0", "log", "num-traits", "rustc-hash 1.1.0", "serde", "spirv", "termcolor", - "thiserror", + "thiserror 1.0.69", "unicode-xid", ] [[package]] name = "naga" -version = "23.0.0" +version = "23.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d5941e45a15b53aad4375eedf02033adb7a28931eedc31117faffa52e6a857e" +checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f" dependencies = [ "arrayvec", "bit-set 0.8.0", @@ -6590,12 +6524,12 @@ dependencies = [ "cfg_aliases 0.1.1", "codespan-reporting", "hexf-parse", - "indexmap 2.2.6", + "indexmap 2.7.0", "log", "rustc-hash 1.1.0", "spirv", "termcolor", - "thiserror", + "thiserror 1.0.69", "unicode-xid", ] @@ -6608,7 +6542,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -6623,7 +6557,7 @@ dependencies = [ "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -6675,18 +6609,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "nix" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - [[package]] name = "nix" version = "0.27.1" @@ -6698,6 +6620,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases 0.1.1", + "libc", +] + [[package]] name = "nix" version = "0.29.0" @@ -6708,7 +6642,7 @@ dependencies = [ "cfg-if", "cfg_aliases 0.2.1", "libc", - "memoffset 0.9.1", + "memoffset", ] [[package]] @@ -6729,7 +6663,7 @@ dependencies = [ "path-clean", "regex", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", "url", ] @@ -6774,7 +6708,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio", + "mio 0.8.11", "walkdir", "windows-sys 0.48.0", ] @@ -6800,11 +6734,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", "rand", @@ -6843,7 +6776,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -6867,9 +6800,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -6878,11 +6811,10 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", "num-traits", @@ -6904,29 +6836,29 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] [[package]] name = "num_enum" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 3.1.0", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -6946,7 +6878,7 @@ checksum = "5124c7a716bd197d4ad501237fa890771f69f38b34eb87f4514fdebf0cdcaf5b" dependencies = [ "codespan-reporting", "heck 0.4.1", - "indexmap 2.2.6", + "indexmap 2.7.0", "itertools 0.12.1", "jiff", "libc", @@ -6962,7 +6894,7 @@ dependencies = [ "rust-embed", "strfmt", "strsim", - "thiserror", + "thiserror 1.0.69", "unicode-ident", "unicode-width", "walkdir", @@ -6987,17 +6919,6 @@ dependencies = [ "malloc_buf", ] -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -7212,9 +7133,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] @@ -7234,26 +7155,11 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" -[[package]] -name = "once_map" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed29bb6f7d6ac14023acb332a356f3891265d780e254057c866dbe7a909d2d2d" -dependencies = [ - "ahash 0.8.11", - "hashbrown 0.15.0", - "parking_lot 0.12.3", - "stable_deref_trait", -] - [[package]] name = "oneshot" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4" -dependencies = [ - "loom", -] +checksum = "e296cf87e61c9cfc1a61c3c63a0f7f286ed4554e0e22be84e8a38e1d264a2a29" [[package]] name = "opaque-debug" @@ -7263,9 +7169,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "open" -version = "5.1.2" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449f0ff855d85ddbf1edd5b646d65249ead3f5e422aaa86b7d2d0b049b103e32" +checksum = "3ecd52f0b8d15c40ce4820aa251ed5de032e5d91fab27f7db2f40d42a8bdf69c" dependencies = [ "is-wsl", "libc", @@ -7280,18 +7186,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.2+3.3.2" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -7310,7 +7216,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror", + "thiserror 1.0.69", "tracing", ] @@ -7322,7 +7228,7 @@ checksum = "10a8a7f5f6ba7c1b286c2fbca0454eaba116f63bbe69ed250b642d36fbb04d80" dependencies = [ "async-trait", "bytes", - "http 1.1.0", + "http 1.2.0", "opentelemetry", ] @@ -7334,16 +7240,16 @@ checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" dependencies = [ "async-trait", "futures-core", - "http 1.1.0", + "http 1.2.0", "opentelemetry", "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", - "prost 0.13.4", + "prost", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", - "tonic 0.12.3", + "tonic", "tracing", ] @@ -7356,9 +7262,9 @@ dependencies = [ "hex", "opentelemetry", "opentelemetry_sdk", - "prost 0.13.4", + "prost", "serde", - "tonic 0.12.3", + "tonic", ] [[package]] @@ -7382,7 +7288,7 @@ dependencies = [ "percent-encoding", "rand", "serde_json", - "thiserror", + "thiserror 1.0.69", "tracing", ] @@ -7394,11 +7300,11 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orbclient" -version = "0.3.47" +version = "0.3.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" dependencies = [ - "libredox 0.0.2", + "libredox", ] [[package]] @@ -7442,9 +7348,9 @@ dependencies = [ [[package]] name = "os_info" -version = "3.8.2" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" +checksum = "e5ca711d8b83edbb00b44d504503cd247c9c0bd8b0fa2694f2a1a3d8165379ce" dependencies = [ "log", "serde", @@ -7463,9 +7369,9 @@ dependencies = [ [[package]] name = "ouroboros" -version = "0.18.3" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b7be5a8a3462b752f4be3ff2b2bf2f7f1d00834902e46be2a4d68b87b0573c" +checksum = "944fa20996a25aded6b4795c6d63f10014a7a83f8be9828a11860b08c5fc4a67" dependencies = [ "aliasable", "ouroboros_macro", @@ -7474,16 +7380,16 @@ dependencies = [ [[package]] name = "ouroboros_macro" -version = "0.18.3" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b645dcde5f119c2c454a92d0dfa271a2a3b205da92e4292a68ead4bdbfde1f33" +checksum = "39b0deead1528fd0e5947a8546a9642a9777c25f6e1e26f34c97b204bbb465bd" dependencies = [ "heck 0.4.1", "itertools 0.12.1", "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -7506,11 +7412,11 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owned_ttf_parser" -version = "0.20.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7" +checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" dependencies = [ - "ttf-parser 0.20.0", + "ttf-parser 0.25.1", ] [[package]] @@ -7574,9 +7480,9 @@ dependencies = [ [[package]] name = "palette" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebfc23a4b76642983d57e4ad00bb4504eb30a8ce3c70f4aee1f725610e36d97a" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" dependencies = [ "approx", "fast-srgb8", @@ -7586,13 +7492,14 @@ dependencies = [ [[package]] name = "palette_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8890702dbec0bad9116041ae586f84805b13eecd1d8b1df27c29998a9969d6d" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" dependencies = [ + "by_address", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -7644,7 +7551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core 0.9.9", + "parking_lot_core 0.9.10", ] [[package]] @@ -7663,15 +7570,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall 0.5.8", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -7687,9 +7594,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "path-clean" @@ -7699,9 +7606,9 @@ checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pbkdf2" @@ -7730,12 +7637,12 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap 2.7.0", ] [[package]] @@ -7768,7 +7675,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -7788,29 +7695,29 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -7869,20 +7776,19 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plist" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ - "base64 0.21.7", - "indexmap 2.2.6", - "line-wrap", - "quick-xml 0.31.0", + "base64 0.22.1", + "indexmap 2.7.0", + "quick-xml 0.32.0", "serde", "time", ] @@ -7914,44 +7820,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.89", -] - -[[package]] -name = "plugin_runtime" -version = "0.1.0" -dependencies = [ - "anyhow", - "bincode 2.0.0-rc.3", - "bytes", - "cacao", - "common", - "component_model", - "deno_core", - "deno_runtime", - "freedesktop-icons", - "freedesktop_entry_parser", - "futures", - "icns", - "image 0.25.1", - "indexmap 2.2.6", - "interprocess", - "libc", - "numbat", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "once_cell", - "plist", - "regex", - "resvg", - "serde", - "tokio", - "tokio-util", - "tracing", - "typed-path", - "utils", - "walkdir", + "syn 2.0.90", ] [[package]] @@ -7968,30 +7837,30 @@ dependencies = [ [[package]] name = "png" -version = "0.17.13" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", - "miniz_oxide 0.7.2", + "miniz_oxide 0.8.2", ] [[package]] name = "polling" -version = "3.6.0" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.4.0", "pin-project-lite", "rustix", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -8014,9 +7883,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "presser" @@ -8035,12 +7907,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.19" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -8073,11 +7945,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.21.1", + "toml_edit 0.22.22", ] [[package]] @@ -8112,7 +7984,7 @@ checksum = "07c277e4e643ef00c1233393c673f655e3672cf7eb3ba08a00bdd0ea59139b5f" dependencies = [ "proc-macro-rules-macros", "proc-macro2", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -8124,7 +7996,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -8155,38 +8027,28 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "version_check", "yansi", ] [[package]] name = "profiling" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" dependencies = [ "quote", - "syn 2.0.89", -] - -[[package]] -name = "prost" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" -dependencies = [ - "bytes", - "prost-derive 0.12.4", + "syn 2.0.90", ] [[package]] @@ -8196,28 +8058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" dependencies = [ "bytes", - "prost-derive 0.13.4", -] - -[[package]] -name = "prost-build" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80b776a1b2dc779f5ee0641f8ade0125bc1298dd41a9a0c16d8bd57b42d222b1" -dependencies = [ - "bytes", - "heck 0.5.0", - "itertools 0.12.1", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost 0.12.4", - "prost-types 0.12.4", - "regex", - "syn 2.0.89", - "tempfile", + "prost-derive", ] [[package]] @@ -8233,26 +8074,13 @@ dependencies = [ "once_cell", "petgraph", "prettyplease", - "prost 0.13.4", - "prost-types 0.13.4", + "prost", + "prost-types", "regex", - "syn 2.0.89", + "syn 2.0.90", "tempfile", ] -[[package]] -name = "prost-derive" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.89", -] - [[package]] name = "prost-derive" version = "0.13.4" @@ -8263,16 +8091,7 @@ dependencies = [ "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.89", -] - -[[package]] -name = "prost-types" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" -dependencies = [ - "prost 0.12.4", + "syn 2.0.90", ] [[package]] @@ -8281,14 +8100,14 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" dependencies = [ - "prost 0.13.4", + "prost", ] [[package]] name = "psm" -version = "0.1.21" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" dependencies = [ "cc", ] @@ -8343,6 +8162,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.36.2" @@ -8459,16 +8287,16 @@ dependencies = [ "rand_chacha", "simd_helpers", "system-deps", - "thiserror", + "thiserror 1.0.69", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" -version = "0.11.5" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc13288f5ab39e6d7c9d501759712e6969fcc9734220846fc9ed26cae2cc4234" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" dependencies = [ "avif-serialize", "imgref", @@ -8481,9 +8309,9 @@ dependencies = [ [[package]] name = "raw-window-handle" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" @@ -8507,9 +8335,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.22.5" +version = "0.22.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a04b892cb6f91951f144c33321843790c8574c825aafdb16d815fd7183b5229" +checksum = "69aacb76b5c29acfb7f90155d39759a29496aebb49395830e928a9703d2eec2f" dependencies = [ "bytemuck", "font-types", @@ -8541,22 +8369,22 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", - "libredox 0.1.3", - "thiserror", + "libredox", + "thiserror 1.0.69", ] [[package]] @@ -8576,7 +8404,7 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -8587,7 +8415,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.8", + "regex-automata 0.4.9", "regex-syntax 0.8.5", ] @@ -8602,9 +8430,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -8641,9 +8469,9 @@ dependencies = [ [[package]] name = "resvg" -version = "0.41.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2327ced609dadeed3e9702fec3e6b2ddd208758a9268d13e06566c6101ba533" +checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958" dependencies = [ "log", "pico-args", @@ -8665,9 +8493,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.37" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" dependencies = [ "bytemuck", ] @@ -8682,20 +8510,19 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", + "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rinja" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28580fecce391f3c0e65a692e5f2b5db258ba2346ee04f355ae56473ab973dc" +checksum = "3dc4940d00595430b3d7d5a01f6222b5e5b51395d1120bdb28d854bb8abb17a5" dependencies = [ "humansize", "itoa", - "num-traits", "percent-encoding", "rinja_derive", "serde", @@ -8704,28 +8531,27 @@ dependencies = [ [[package]] name = "rinja_derive" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f1ae91455a4c82892d9513fcfa1ac8faff6c523602d0041536341882714aede" +checksum = "08d9ed0146aef6e2825f1b1515f074510549efba38d71f4554eec32eb36ba18b" dependencies = [ "basic-toml", "memchr", "mime", "mime_guess", - "once_map", "proc-macro2", "quote", "rinja_parser", - "rustc-hash 2.0.0", + "rustc-hash 2.1.0", "serde", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "rinja_parser" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea17639e1f35032e1c67539856e498c04cd65fe2a45f55ec437ec55e4be941" +checksum = "93f9a866e2e00a7a1fb27e46e9e324a6f7c0e7edc4543cae1d38f4e4a100c610" dependencies = [ "memchr", "nom 7.1.3", @@ -8755,15 +8581,15 @@ dependencies = [ [[package]] name = "roxmltree" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" dependencies = [ "const-oid", "digest", @@ -8795,9 +8621,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.3.0" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb78f46d0066053d16d4ca7b898e9343bc3530f71c61d5ad84cd404ada068745" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -8806,23 +8632,23 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.3.0" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", "shellexpand", - "syn 2.0.89", + "syn 2.0.90", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.3.0" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f69089032567ffff4eada41c573fc43ff466c7db7c5688b2e7969584345581" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" dependencies = [ "sha2", "walkdir", @@ -8860,9 +8686,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -8872,9 +8698,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustc_version" @@ -8887,11 +8713,11 @@ dependencies = [ [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.22", + "semver 1.0.24", ] [[package]] @@ -8905,15 +8731,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.41" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags 2.6.0", - "errno 0.3.8", + "errno 0.3.10", "libc", "linux-raw-sys 0.4.14", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -8969,9 +8795,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" [[package]] name = "rustls-tokio-stream" @@ -8998,9 +8824,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "rustybuzz" @@ -9027,7 +8853,7 @@ checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86" dependencies = [ "bitflags 2.6.0", "cfg-if", - "clipboard-win 5.3.1", + "clipboard-win", "fd-lock", "home", "libc", @@ -9043,9 +8869,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "ryu-js" @@ -9087,26 +8913,13 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scenario_runner" -version = "0.0.0" -dependencies = [ - "anyhow", - "common", - "serde", - "serde_json", - "tokio", - "tonic 0.11.0", - "utils", -] - [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -9163,11 +8976,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -9176,9 +8989,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" dependencies = [ "core-foundation-sys", "libc", @@ -9186,9 +8999,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" +checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" [[package]] name = "semver" @@ -9201,9 +9014,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" dependencies = [ "serde", ] @@ -9216,9 +9029,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] @@ -9235,32 +9048,33 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.7.0", "itoa", + "memchr", "ryu", "serde", ] @@ -9273,14 +9087,14 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -9306,21 +9120,21 @@ dependencies = [ "num-bigint", "serde", "smallvec", - "thiserror", + "thiserror 1.0.69", "v8", ] [[package]] name = "serde_with" -version = "3.8.1" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.7.0", "serde", "serde_derive", "serde_json", @@ -9330,14 +9144,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.1" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -9350,49 +9164,6 @@ dependencies = [ "serde", ] -[[package]] -name = "server" -version = "0.0.0" -dependencies = [ - "anyhow", - "arboard", - "async-stream", - "bytes", - "client", - "common", - "futures", - "git2", - "image 0.25.1", - "include_dir", - "indexmap 2.2.6", - "interprocess", - "itertools 0.10.5", - "once_cell", - "open", - "plugin_runtime", - "regex", - "scenario_runner", - "serde", - "sqlx", - "tantivy", - "tempfile", - "thiserror", - "tokio", - "tokio-util", - "toml", - "tonic 0.11.0", - "tracing", - "tracing-subscriber", - "typed-path", - "ureq", - "url", - "utils", - "uuid", - "vergen-gitcl", - "vergen-pretty", - "walkdir", -] - [[package]] name = "sha1" version = "0.10.6" @@ -9461,9 +9232,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -9519,9 +9290,9 @@ dependencies = [ [[package]] name = "simdutf8" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "simplecss" @@ -9624,14 +9395,14 @@ dependencies = [ "log", "memmap2 0.9.5", "rustix", - "thiserror", + "thiserror 1.0.69", "wayland-backend", - "wayland-client 0.31.7", + "wayland-client", "wayland-csd-frame", "wayland-cursor", "wayland-protocols 0.31.2", "wayland-protocols-wlr 0.2.0", - "wayland-scanner 0.31.5", + "wayland-scanner", "xkeysym", ] @@ -9649,14 +9420,14 @@ dependencies = [ "log", "memmap2 0.9.5", "rustix", - "thiserror", + "thiserror 1.0.69", "wayland-backend", - "wayland-client 0.31.7", + "wayland-client", "wayland-csd-frame", "wayland-cursor", "wayland-protocols 0.32.5", "wayland-protocols-wlr 0.3.5", - "wayland-scanner 0.31.5", + "wayland-scanner", "xkeysym", ] @@ -9682,9 +9453,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -9710,16 +9481,16 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall 0.5.7", + "redox_syscall 0.5.8", "rustix", "tiny-xlib", "wasm-bindgen", "wayland-backend", - "wayland-client 0.31.7", - "wayland-sys 0.31.5", + "wayland-client", + "wayland-sys", "web-sys", "windows-sys 0.59.0", - "x11rb 0.13.1", + "x11rb", ] [[package]] @@ -9760,12 +9531,6 @@ dependencies = [ "url", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -9796,11 +9561,10 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.3" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ - "itertools 0.12.1", "nom 7.1.3", "unicode_categories", ] @@ -9839,7 +9603,7 @@ dependencies = [ "hashbrown 0.14.5", "hashlink", "hex", - "indexmap 2.2.6", + "indexmap 2.7.0", "log", "memchr", "once_cell", @@ -9850,7 +9614,7 @@ dependencies = [ "sha2", "smallvec", "sqlformat", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "tracing", @@ -9867,7 +9631,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -9890,7 +9654,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.89", + "syn 2.0.90", "tempfile", "tokio", "url", @@ -9903,7 +9667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" dependencies = [ "atoi", - "base64 0.22.0", + "base64 0.22.1", "bitflags 2.6.0", "byteorder", "bytes", @@ -9933,7 +9697,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "tracing", "whoami", ] @@ -9945,7 +9709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" dependencies = [ "atoi", - "base64 0.22.0", + "base64 0.22.1", "bitflags 2.6.0", "byteorder", "crc", @@ -9971,7 +9735,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "tracing", "whoami", ] @@ -10007,15 +9771,15 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stacker" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" dependencies = [ "cc", "cfg-if", "libc", "psm", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -10024,12 +9788,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "str-buf" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" - [[package]] name = "strfmt" version = "0.2.4" @@ -10054,7 +9812,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -10065,13 +9823,13 @@ checksum = "04028eeb851ed08af6aba5caa29f2d59a13ed168cee4d6bd753aeefcf1d636b0" [[package]] name = "stringprep" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "finl_unicode", "unicode-bidi", "unicode-normalization", + "unicode-properties", ] [[package]] @@ -10099,20 +9857,20 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "svg_fmt" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83ba502a3265efb76efb89b0a2f7782ad6f2675015d4ce37e4b547dda42b499" +checksum = "ce5d813d71d82c4cbc1742135004e4a79fd870214c155443451c139c9470a0aa" [[package]] name = "svgtypes" @@ -10208,7 +9966,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4740e53eaf68b101203c1df0937d5161a29f3c13bceed0836ddfe245b72dd000" dependencies = [ "anyhow", - "indexmap 2.2.6", + "indexmap 2.7.0", "serde", "serde_json", "swc_cached", @@ -10224,7 +9982,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -10273,7 +10031,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -10320,7 +10078,7 @@ checksum = "65f21494e75d0bd8ef42010b47cabab9caaed8f2207570e809f6f4eb51a710d1" dependencies = [ "better_scoped_tls", "bitflags 2.6.0", - "indexmap 2.2.6", + "indexmap 2.7.0", "once_cell", "phf", "rustc-hash 1.1.0", @@ -10358,7 +10116,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -10389,7 +10147,7 @@ checksum = "76c76d8b9792ce51401d38da0fa62158d61f6d80d16d68fe5b03ce4bf5fba383" dependencies = [ "base64 0.21.7", "dashmap", - "indexmap 2.2.6", + "indexmap 2.7.0", "once_cell", "serde", "sha1", @@ -10429,7 +10187,7 @@ version = "0.134.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029eec7dd485923a75b5a45befd04510288870250270292fc2c1b3a9e7547408" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.7.0", "num_cpus", "once_cell", "rustc-hash 1.1.0", @@ -10465,7 +10223,7 @@ checksum = "63db0adcff29d220c3d151c5b25c0eabe7e32dd936212b84cdaa1392e3130497" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -10476,7 +10234,7 @@ checksum = "f486687bfb7b5c560868f69ed2d458b880cebc9babebcb67e49f31b55c5bf847" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -10499,7 +10257,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -10515,21 +10273,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.89" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -10556,14 +10308,14 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "sys-locale" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" dependencies = [ "libc", ] @@ -10589,7 +10341,7 @@ checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856" dependencies = [ "aho-corasick", "arc-swap", - "base64 0.22.0", + "base64 0.22.1", "bitpacking", "byteorder", "census", @@ -10626,7 +10378,7 @@ dependencies = [ "tantivy-stacker", "tantivy-tokenizer-api", "tempfile", - "thiserror", + "thiserror 1.0.69", "time", "uuid", "winapi", @@ -10730,9 +10482,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "target-lexicon" -version = "0.12.14" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" @@ -10771,7 +10523,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a" +dependencies = [ + "thiserror-impl 2.0.8", ] [[package]] @@ -10782,7 +10543,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", ] [[package]] @@ -10808,9 +10580,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -10831,9 +10603,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -10859,7 +10631,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png 0.17.13", + "png 0.17.16", "tiny-skia-path", ] @@ -10882,7 +10654,7 @@ checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" dependencies = [ "as-raw-xcb-connection", "ctor-lite", - "libloading 0.8.3", + "libloading 0.8.6", "pkg-config", "tracing", ] @@ -10899,9 +10671,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -10914,21 +10686,20 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.3", "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -10941,25 +10712,15 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -10986,13 +10747,13 @@ dependencies = [ [[package]] name = "tokio-socks" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", "futures-util", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -11009,9 +10770,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -11026,21 +10787,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.9", + "toml_edit 0.22.22", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] @@ -11051,7 +10812,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.7.0", "toml_datetime", "winnow 0.5.40", ] @@ -11062,60 +10823,22 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.7.0", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.21.1" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.2.6", - "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.22.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" -dependencies = [ - "indexmap 2.2.6", + "indexmap 2.7.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.6", -] - -[[package]] -name = "tonic" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.6.20", - "base64 0.21.7", - "bytes", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", - "hyper-timeout 0.4.1", - "percent-encoding", - "pin-project", - "prost 0.12.4", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", + "winnow 0.6.20", ] [[package]] @@ -11126,23 +10849,23 @@ checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-stream", "async-trait", - "axum 0.7.5", - "base64 0.22.0", + "axum", + "base64 0.22.1", "bytes", "h2 0.4.7", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.1", - "hyper-timeout 0.5.1", + "hyper 1.5.2", + "hyper-timeout", "hyper-util", "percent-encoding", "pin-project", - "prost 0.13.4", + "prost", "socket2", "tokio", "tokio-stream", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -11150,15 +10873,16 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.11.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4ef6dd70a610078cb4e338a0f79d06bc759ff1b22d2120c2ff02ae264ba9c2" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" dependencies = [ "prettyplease", "proc-macro2", - "prost-build 0.12.4", + "prost-build", + "prost-types", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -11181,6 +10905,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.6.2" @@ -11191,7 +10929,7 @@ dependencies = [ "bitflags 2.6.0", "bytes", "futures-core", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "http-body-util", "pin-project-lite", @@ -11209,15 +10947,15 @@ checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -11227,20 +10965,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -11259,9 +10997,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -11277,9 +11015,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.15.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b92252d649d771105448969f2b2dda4342ba48b77731b60d37c93665e26615b" +checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b" dependencies = [ "core-graphics 0.24.0", "crossbeam-channel", @@ -11290,19 +11028,18 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "once_cell", - "png 0.17.13", - "thiserror", + "png 0.17.16", + "thiserror 1.0.69", "windows-sys 0.59.0", ] [[package]] name = "tree_magic_mini" -version = "3.1.5" +version = "3.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469a727cac55b41448315cc10427c069c618ac59bb6a4480283fcd811749bdc2" +checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63" dependencies = [ "fnv", - "home", "memchr", "nom 7.1.3", "once_cell", @@ -11337,6 +11074,12 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "twox-hash" version = "1.6.3" @@ -11356,9 +11099,9 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typed-path" -version = "0.9.2" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c0c7479c430935701ff2532e3091e6680ec03f2f89ffcd9988b08e885b90a5" +checksum = "41713888c5ccfd99979fcd1afd47b71652e331b3d4a0e19d30769e80fec76cce" [[package]] name = "typeid" @@ -11378,7 +11121,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ - "memoffset 0.9.1", + "memoffset", "tempfile", "winapi", ] @@ -11426,18 +11169,15 @@ dependencies = [ [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi-mirroring" @@ -11453,9 +11193,9 @@ checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" [[package]] name = "unicode-id" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" +checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" [[package]] name = "unicode-id-start" @@ -11465,9 +11205,9 @@ checksum = "2f322b60f6b9736017344fa0635d64be2f458fbc04eef65f6be22976dd1ffd5b" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-linebreak" @@ -11477,30 +11217,30 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-script" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" @@ -11538,11 +11278,11 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.10.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "flate2", "log", "once_cell", @@ -11578,11 +11318,11 @@ dependencies = [ [[package]] name = "usvg" -version = "0.41.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c704361d822337cfc00387672c7b59eaa72a1f0744f62b2a68aa228a0c6927d" +checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "data-url", "flate2", "imagesize", @@ -11624,24 +11364,15 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - -[[package]] -name = "utils" -version = "0.0.0" -dependencies = [ - "log", - "thiserror", - "tokio", -] +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.8.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", "serde", @@ -11658,7 +11389,7 @@ dependencies = [ "fslock", "gzip-header", "home", - "miniz_oxide 0.7.2", + "miniz_oxide 0.7.4", "once_cell", "paste", "which 6.0.3", @@ -11672,10 +11403,10 @@ checksum = "97599c400fc79925922b58303e98fcb8fa88f573379a08ddb652e72cbd2e70f6" dependencies = [ "bitflags 2.6.0", "encoding_rs", - "indexmap 2.2.6", + "indexmap 2.7.0", "num-bigint", "serde", - "thiserror", + "thiserror 1.0.69", "wtf8", ] @@ -11716,9 +11447,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "9.0.1" +version = "9.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349ed9e45296a581f455bc18039878f409992999bc1d5da12a6800eb18c8752f" +checksum = "31f25fc8f8f05df455c7941e87f093ad22522a9ff33d7a027774815acf6f0639" dependencies = [ "anyhow", "cargo_metadata", @@ -11731,9 +11462,9 @@ dependencies = [ [[package]] name = "vergen-gitcl" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a3a7f91caabecefc3c249fd864b11d4abe315c166fbdb568964421bccfd2b7a" +checksum = "0227006d09f98ab00ea69e9a5e055e676a813cfbed4232986176c86a6080b997" dependencies = [ "anyhow", "derive_builder", @@ -11745,9 +11476,9 @@ dependencies = [ [[package]] name = "vergen-lib" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229eaddb0050920816cf051e619affaf18caa3dd512de8de5839ccbc8e53abb0" +checksum = "c0c767e6751c09fc85cde58722cf2f1007e80e4c8d5a4321fc90d83dc54ca147" dependencies = [ "anyhow", "derive_builder", @@ -11756,9 +11487,9 @@ dependencies = [ [[package]] name = "vergen-pretty" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9579e7d75e528471646aa8c9bf11392154f3f8e0b02d94211bc5d57701d010f0" +checksum = "69f0cedcb598e15120e748b19e97426e376be2ec27987570e11bbd350d0bdb09" dependencies = [ "anyhow", "convert_case", @@ -11774,9 +11505,9 @@ checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "virtue" @@ -11823,9 +11554,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -11834,36 +11565,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.45" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -11871,22 +11602,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wasm-timer" @@ -11909,7 +11640,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f270206a91783fd90625c8bb0d8fbd459d0b1d1bf209b656f713f01ae7c04b8" dependencies = [ - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -11918,12 +11649,12 @@ version = "0.13.99" source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#7df6d555dd018b0df400dfc814f664da8f01958e" dependencies = [ "bitflags 2.6.0", - "calloop 0.14.1", + "calloop 0.14.2", "memmap2 0.9.5", "smol_str", "tracing", "wayland-backend", - "wayland-client 0.31.7", + "wayland-client", "xkbcommon-dl", ] @@ -11938,22 +11669,7 @@ dependencies = [ "rustix", "scoped-tls", "smallvec", - "wayland-sys 0.31.5", -] - -[[package]] -name = "wayland-client" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" -dependencies = [ - "bitflags 1.3.2", - "downcast-rs", - "libc", - "nix 0.24.3", - "wayland-commons", - "wayland-scanner 0.29.5", - "wayland-sys 0.29.5", + "wayland-sys", ] [[package]] @@ -11965,19 +11681,7 @@ dependencies = [ "bitflags 2.6.0", "rustix", "wayland-backend", - "wayland-scanner 0.31.5", -] - -[[package]] -name = "wayland-commons" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" -dependencies = [ - "nix 0.24.3", - "once_cell", - "smallvec", - "wayland-sys 0.29.5", + "wayland-scanner", ] [[package]] @@ -11998,22 +11702,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" dependencies = [ "rustix", - "wayland-client 0.31.7", + "wayland-client", "xcursor", ] -[[package]] -name = "wayland-protocols" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" -dependencies = [ - "bitflags 1.3.2", - "wayland-client 0.29.5", - "wayland-commons", - "wayland-scanner 0.29.5", -] - [[package]] name = "wayland-protocols" version = "0.31.2" @@ -12022,8 +11714,8 @@ checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ "bitflags 2.6.0", "wayland-backend", - "wayland-client 0.31.7", - "wayland-scanner 0.31.5", + "wayland-client", + "wayland-scanner", ] [[package]] @@ -12034,8 +11726,8 @@ checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" dependencies = [ "bitflags 2.6.0", "wayland-backend", - "wayland-client 0.31.7", - "wayland-scanner 0.31.5", + "wayland-client", + "wayland-scanner", ] [[package]] @@ -12046,9 +11738,9 @@ checksum = "da2e42969764e469a115d4bb1c16e9588ef8b75b127ba7a2c9ddf1e140b25ca7" dependencies = [ "bitflags 2.6.0", "wayland-backend", - "wayland-client 0.31.7", + "wayland-client", "wayland-protocols 0.32.5", - "wayland-scanner 0.31.5", + "wayland-scanner", ] [[package]] @@ -12059,9 +11751,9 @@ checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" dependencies = [ "bitflags 2.6.0", "wayland-backend", - "wayland-client 0.31.7", + "wayland-client", "wayland-protocols 0.31.2", - "wayland-scanner 0.31.5", + "wayland-scanner", ] [[package]] @@ -12072,9 +11764,9 @@ checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ "bitflags 2.6.0", "wayland-backend", - "wayland-client 0.31.7", + "wayland-client", "wayland-protocols 0.31.2", - "wayland-scanner 0.31.5", + "wayland-scanner", ] [[package]] @@ -12085,20 +11777,9 @@ checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" dependencies = [ "bitflags 2.6.0", "wayland-backend", - "wayland-client 0.31.7", + "wayland-client", "wayland-protocols 0.32.5", - "wayland-scanner 0.31.5", -] - -[[package]] -name = "wayland-scanner" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" -dependencies = [ - "proc-macro2", - "quote", - "xml-rs", + "wayland-scanner", ] [[package]] @@ -12112,15 +11793,6 @@ dependencies = [ "quote", ] -[[package]] -name = "wayland-sys" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" -dependencies = [ - "pkg-config", -] - [[package]] name = "wayland-sys" version = "0.31.5" @@ -12135,9 +11807,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", @@ -12164,9 +11836,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" dependencies = [ "rustls-pki-types", ] @@ -12179,16 +11851,16 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "wgpu" -version = "23.0.0" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ab52f2d3d18b70d5ab8dd270a1cff3ebe6dbe4a7d13c1cc2557138a9777fdc" +checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a" dependencies = [ "arrayvec", "cfg_aliases 0.1.1", "document-features", "js-sys", "log", - "naga 23.0.0", + "naga 23.1.0", "parking_lot 0.12.3", "profiling", "raw-window-handle", @@ -12197,8 +11869,8 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "wgpu-core 23.0.0", - "wgpu-hal 23.0.0", + "wgpu-core 23.0.1", + "wgpu-hal 23.0.1", "wgpu-types 23.0.0", ] @@ -12214,7 +11886,7 @@ dependencies = [ "cfg_aliases 0.1.1", "codespan-reporting", "document-features", - "indexmap 2.2.6", + "indexmap 2.7.0", "log", "naga 0.20.0", "once_cell", @@ -12225,7 +11897,7 @@ dependencies = [ "rustc-hash 1.1.0", "serde", "smallvec", - "thiserror", + "thiserror 1.0.69", "web-sys", "wgpu-hal 0.21.1", "wgpu-types 0.20.0", @@ -12233,26 +11905,26 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "23.0.0" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c68e7b6322a03ee5b83fcd92caeac5c2a932f6457818179f4652ad2a9c065" +checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a" dependencies = [ "arrayvec", "bit-vec 0.8.0", "bitflags 2.6.0", "cfg_aliases 0.1.1", "document-features", - "indexmap 2.2.6", + "indexmap 2.7.0", "log", - "naga 23.0.0", + "naga 23.1.0", "once_cell", "parking_lot 0.12.3", "profiling", "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror", - "wgpu-hal 23.0.0", + "thiserror 1.0.69", + "wgpu-hal 23.0.1", "wgpu-types 23.0.0", ] @@ -12278,7 +11950,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.3", + "libloading 0.8.6", "log", "metal 0.28.0", "naga 0.20.0", @@ -12291,7 +11963,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror", + "thiserror 1.0.69", "wasm-bindgen", "web-sys", "wgpu-types 0.20.0", @@ -12300,9 +11972,9 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "23.0.0" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de6e7266b869de56c7e3ed72a954899f71d14fec6cc81c102b7530b92947601b" +checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821" dependencies = [ "android_system_properties", "arrayvec", @@ -12321,10 +11993,10 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.3", + "libloading 0.8.6", "log", "metal 0.29.0", - "naga 23.0.0", + "naga 23.1.0", "ndk-sys 0.5.0+25.2.9519653", "objc", "once_cell", @@ -12335,11 +12007,11 @@ dependencies = [ "renderdoc-sys", "rustc-hash 1.1.0", "smallvec", - "thiserror", + "thiserror 1.0.69", "wasm-bindgen", "web-sys", "wgpu-types 23.0.0", - "windows 0.58.0", + "windows", "windows-core 0.58.0", ] @@ -12392,11 +12064,11 @@ dependencies = [ [[package]] name = "whoami" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall 0.4.1", + "redox_syscall 0.5.8", "wasite", "web-sys", ] @@ -12425,20 +12097,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-wsapoll" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eafc5f679c576995526e81635d0cf9695841736712b4e892f87abbe6fed3f28" -dependencies = [ - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -12453,21 +12116,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d692d46038c433f9daee7ad8757e002a4248c20b0a3fbc991d99521d3bcb6d" dependencies = [ - "clipboard-win 5.3.1", + "clipboard-win", "clipboard_macos", "clipboard_wayland", "clipboard_x11", "raw-window-handle", - "thiserror", -] - -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.5", + "thiserror 1.0.69", ] [[package]] @@ -12510,7 +12164,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -12521,7 +12175,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -12797,14 +12451,14 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "wayland-backend", - "wayland-client 0.31.7", + "wayland-client", "wayland-protocols 0.31.2", "wayland-protocols-plasma", "web-sys", "web-time", "windows-sys 0.52.0", "x11-dl", - "x11rb 0.13.1", + "x11rb", "xkbcommon-dl", ] @@ -12819,9 +12473,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -12853,20 +12507,22 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wl-clipboard-rs" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "981a303dfbb75d659f6612d05a14b2e363c103d24f676a2d44a00d18507a1ad9" +checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb" dependencies = [ "derive-new", "libc", "log", - "nix 0.24.3", + "nix 0.28.0", "os_pipe", "tempfile", - "thiserror", + "thiserror 1.0.69", "tree_magic_mini", - "wayland-client 0.29.5", - "wayland-protocols 0.29.5", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.31.2", + "wayland-protocols-wlr 0.2.0", ] [[package]] @@ -12907,19 +12563,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "x11rb" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" -dependencies = [ - "gethostname 0.2.3", - "nix 0.24.3", - "winapi", - "winapi-wsapoll", - "x11rb-protocol 0.10.0", -] - [[package]] name = "x11rb" version = "0.13.1" @@ -12927,21 +12570,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "as-raw-xcb-connection", - "gethostname 0.4.3", + "gethostname", "libc", - "libloading 0.8.3", + "libloading 0.8.6", "once_cell", "rustix", - "x11rb-protocol 0.13.1", -] - -[[package]] -name = "x11rb-protocol" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" -dependencies = [ - "nix 0.24.3", + "x11rb-protocol", ] [[package]] @@ -12975,15 +12609,15 @@ dependencies = [ "nom 7.1.3", "oid-registry", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time", ] [[package]] name = "xcursor" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" [[package]] name = "xdg" @@ -13016,15 +12650,15 @@ dependencies = [ [[package]] name = "xkeysym" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.20" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" +checksum = "ea8b391c9a790b496184c29f7f93b9ed5b16abb306c05415b68bcc16e4d06432" [[package]] name = "xmlwriter" @@ -13064,7 +12698,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "synstructure 0.13.1", ] @@ -13112,10 +12746,10 @@ version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" dependencies = [ - "proc-macro-crate 3.1.0", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "zvariant_utils", ] @@ -13138,22 +12772,23 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -13173,15 +12808,15 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "synstructure 0.13.1", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] @@ -13194,7 +12829,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -13216,32 +12851,32 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "zstd" -version = "0.13.0" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.0.0" +version = "7.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.13+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" dependencies = [ "cc", "pkg-config", @@ -13264,9 +12899,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" dependencies = [ "zune-core", ] @@ -13290,10 +12925,10 @@ version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" dependencies = [ - "proc-macro-crate 3.1.0", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "zvariant_utils", ] @@ -13305,5 +12940,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] diff --git a/Cargo.toml b/Cargo.toml index 24e5748..4d116b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gauntlet" -edition = "2021" +edition.workspace = true repository = "https://github.com/project-gauntlet/gauntlet" [workspace] @@ -16,7 +16,11 @@ members = [ "rust/scenario_runner", "rust/plugin_runtime", ] +[workspace.package] +edition = "2021" + [workspace.dependencies] +# iced #iced = { version = "0.13.99", features = ["tokio", "lazy", "advanced", "image"] } iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", features = ["tokio", "lazy", "advanced", "image"] } #iced_aw = { version = "0.11.99", features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } @@ -28,9 +32,44 @@ iced_fonts = { git = "https://github.com/project-gauntlet/iced_fonts.git", bran #iced_layershell = "0.13.99" iced_layershell = { git = "https://github.com/project-gauntlet/exwlshelleventloop.git", branch = "gauntlet-0.13" } +# workspaces +gauntlet-common = { path = "./rust/common" } +gauntlet-common-ui = { path = "./rust/common_ui" } +gauntlet-management-client = { path = "./rust/management_client" } +gauntlet-client = { path = "./rust/client" } +gauntlet-server = { path = "./rust/server" } +gauntlet-utils = { path = "./rust/utils" } +gauntlet-plugin-runtime = { path = "./rust/plugin_runtime" } +gauntlet-component-model = { path = "./rust/component_model" } +gauntlet-scenario-runner = { path = "./rust/scenario_runner" } + +# shared +anyhow = { version = "1", features = ["backtrace"] } +tracing = { version = "0.1" } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tokio = { version = "1.42" } +tokio-util = "0.7" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0" } +bincode = { version = "2.0.0-rc.3" } +thiserror = { version = "2" } +indexmap = { version = "2.1", features = ["serde"] } +itertools = { version = "0.13" } +regex = { version = "1.9.3" } +futures = { version = "0.3.31" } +image = { version = "0.25" } +once_cell = { version = "1.19" } +tonic = { version = "0.12.3" } +tonic-build = { version = "0.12.3" } +prost = { version = "0.13.4" } +bytes = { version = "1.6.0" } +walkdir = { version = "2.4.0" } +typed-path = { version = "0.10.0" } +interprocess = { version = "2.2.2", features = ["tokio"] } + [dependencies] -cli = { path = "rust/cli" } +gauntlet-cli = { path = "rust/cli" } [features] -release = ["cli/release"] -scenario_runner = ["cli/scenario_runner"] +release = ["gauntlet-cli/release"] +scenario_runner = ["gauntlet-cli/scenario_runner"] diff --git a/js/api_build/package.json b/js/api_build/package.json index bfc1b57..de4a532 100644 --- a/js/api_build/package.json +++ b/js/api_build/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "build": "npm run generate-json && npm run build-generator && npm run run-generator", - "generate-json": "cd ../.. && cargo run --package component_model -- ./js/api_build/component_model.json", + "generate-json": "cd ../.. && cargo run --package gauntlet-component-model -- ./js/api_build/component_model.json", "build-generator": "tsc", "run-generator": "node dist/index.js" }, diff --git a/js/scenario_runner_cli/src/main.ts b/js/scenario_runner_cli/src/main.ts index bb4b93f..ccaa248 100644 --- a/js/scenario_runner_cli/src/main.ts +++ b/js/scenario_runner_cli/src/main.ts @@ -54,7 +54,7 @@ async function runScenarios(expectedPlugin: string | undefined) { stdio: "inherit", cwd: projectRoot, env: Object.assign(process.env, { - RUST_LOG: "server=info", + RUST_LOG: "gauntlet-server=INFO", GAUNTLET_SCENARIO_RUNNER_TYPE: "scenario_runner", GAUNTLET_SCENARIOS_DIR: scenarios, GAUNTLET_SCENARIO_PLUGIN_NAME: pluginName, @@ -120,7 +120,7 @@ async function runScreenshotGen(expectedPlugin: string | undefined, expectedEntr stdio: "inherit", cwd: projectRoot, env: Object.assign(process.env, { - RUST_LOG: "client=info", + RUST_LOG: "gauntlet-client=INFO", GAUNTLET_SCENARIO_RUNNER_TYPE: "screenshot_gen", GAUNTLET_SCREENSHOT_GEN_IN: scenarioFile, GAUNTLET_SCREENSHOT_GEN_OUT: path.join(scenarios, "out-screenshot", plugin, entrypoint, scenarioName + ".png"), diff --git a/rust/cli/Cargo.toml b/rust/cli/Cargo.toml index 7b741b2..ec7f4fa 100644 --- a/rust/cli/Cargo.toml +++ b/rust/cli/Cargo.toml @@ -1,19 +1,24 @@ [package] -name = "cli" -edition = "2021" +name = "gauntlet-cli" +edition.workspace = true [dependencies] -clap = { version = "4.3.22", features = ["derive"] } -tracing = "0.1" -tracing-subscriber = "0.3" -management_client = { path = "../management_client" } -client = { path = "../client" } -server = { path = "../server" } -anyhow = { version = "1", features = ["backtrace"] } +# workspaces +gauntlet-management-client.workspace = true +gauntlet-client.workspace = true +gauntlet-server.workspace = true + +# shared +tracing.workspace = true +tracing-subscriber.workspace = true +anyhow.workspace = true + +# other +clap = { version = "4.5", features = ["derive"] } [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] auto-launch = "0.5.0" [features] -release = ["server/release"] -scenario_runner = ["server/scenario_runner", "client/scenario_runner"] +release = ["gauntlet-server/release"] +scenario_runner = ["gauntlet-server/scenario_runner", "gauntlet-client/scenario_runner"] diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs index c774bce..11b00fd 100644 --- a/rust/cli/src/lib.rs +++ b/rust/cli/src/lib.rs @@ -1,9 +1,8 @@ use anyhow::{anyhow, Context}; use clap::Parser; - -use client::{generate_simple_theme_sample, generate_complex_theme_sample, open_window}; -use management_client::start_management_client; -use server::start; +use gauntlet_client::{generate_complex_theme_sample, generate_simple_theme_sample, open_window}; +use gauntlet_management_client::start_management_client; +use gauntlet_server::start; #[derive(Debug, clap::Parser)] struct Cli { diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index 3e33a88..a153a6b 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -1,38 +1,39 @@ [package] -name = "client" -edition = "2021" +name = "gauntlet-client" +edition.workspace = true [dependencies] -tokio = "1.28.1" -anyhow = { version = "1", features = ["backtrace"] } -thiserror = "1" +# workspaces +gauntlet-common.workspace = true +gauntlet-common-ui.workspace = true +gauntlet-utils.workspace = true +gauntlet-component-model.workspace = true + +# shared +tokio.workspace = true +anyhow.workspace = true iced.workspace = true iced_aw.workspace = true iced_fonts.workspace = true -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -common = { path = "../common" } -common-ui = { path = "../common_ui" } -utils = { path = "../utils" } -tonic = "0.11.0" -itertools = "0.12.1" -component_model = { path = "../component_model" } -image = "0.25" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -once_cell = "1.19" -bytes = "1.6.0" -global-hotkey = "0.4.2" +tracing.workspace = true +itertools.workspace = true +serde.workspace = true +serde_json.workspace = true +image.workspace = true +once_cell.workspace = true + +# other +global-hotkey = "0.6.3" [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] -tray-icon = { version = "0.15.1", default-features = false } +tray-icon = { version = "0.19.2", default-features = false } [target.'cfg(target_os = "linux")'.dependencies] iced_layershell.workspace = true [build-dependencies] -component_model = { path = "../component_model" } -anyhow = { version = "1", features = ["backtrace"] } +gauntlet-component-model.workspace = true +anyhow.workspace = true convert_case = "0.6.0" [features] diff --git a/rust/client/build.rs b/rust/client/build.rs index 39c63d2..dee3262 100644 --- a/rust/client/build.rs +++ b/rust/client/build.rs @@ -5,7 +5,7 @@ use std::path::Path; use convert_case::{Case, Casing}; -use component_model::{create_component_model, Component, ComponentName, Property, PropertyType}; +use gauntlet_component_model::{create_component_model, Component, ComponentName, Property, PropertyType}; fn main() -> anyhow::Result<()> { let out_dir = env::var("OUT_DIR")?; @@ -40,23 +40,23 @@ fn main() -> anyhow::Result<()> { match arg.property_type { PropertyType::String => { if arg.optional { - output.push_str(&format!(" {}.map(|{}| common::model::UiPropertyValue::String({})).unwrap_or_else(|| common::model::UiPropertyValue::Undefined),\n", arg.name, arg.name, arg.name)); + output.push_str(&format!(" {}.map(|{}| gauntlet_common::model::UiPropertyValue::String({})).unwrap_or_else(|| gauntlet_common::model::UiPropertyValue::Undefined),\n", arg.name, arg.name, arg.name)); } else { - output.push_str(&format!(" common::model::UiPropertyValue::String({}),\n", arg.name)); + output.push_str(&format!(" gauntlet_common::model::UiPropertyValue::String({}),\n", arg.name)); } } PropertyType::Number => { if arg.optional { - output.push_str(&format!(" {}.map(|{}| common::model::UiPropertyValue::Number({})).unwrap_or_else(|| common::model::UiPropertyValue::Undefined),\n", arg.name, arg.name, arg.name)); + output.push_str(&format!(" {}.map(|{}| gauntlet_common::model::UiPropertyValue::Number({})).unwrap_or_else(|| gauntlet_common::model::UiPropertyValue::Undefined),\n", arg.name, arg.name, arg.name)); } else { - output.push_str(&format!(" common::model::UiPropertyValue::Number({}),\n", arg.name)); + output.push_str(&format!(" gauntlet_common::model::UiPropertyValue::Number({}),\n", arg.name)); } } PropertyType::Boolean => { if arg.optional { - output.push_str(&format!(" {}.map(|{}| common::model::UiPropertyValue::Bool({})).unwrap_or_else(|| common::model::UiPropertyValue::Undefined),\n", arg.name, arg.name, arg.name)); + output.push_str(&format!(" {}.map(|{}| gauntlet_common::model::UiPropertyValue::Bool({})).unwrap_or_else(|| gauntlet_common::model::UiPropertyValue::Undefined),\n", arg.name, arg.name, arg.name)); } else { - output.push_str(&format!(" common::model::UiPropertyValue::Bool({}),\n", arg.name)); + output.push_str(&format!(" gauntlet_common::model::UiPropertyValue::Bool({}),\n", arg.name)); } } _ => { @@ -104,7 +104,6 @@ fn generate_required_type(property_type: &PropertyType, union_name: Option panic!("client doesn't know about functions in properties"), PropertyType::Component { reference } => format!("{}Widget", reference.component_name.to_string()), PropertyType::SharedTypeRef { name } => name.to_owned(), - // PropertyType::ImageSource => "bytes::Bytes".to_owned(), PropertyType::Union { .. } => { match union_name { None => panic!("should not be used"), diff --git a/rust/client/src/global_shortcut.rs b/rust/client/src/global_shortcut.rs index 581b6a5..bcc0693 100644 --- a/rust/client/src/global_shortcut.rs +++ b/rust/client/src/global_shortcut.rs @@ -2,7 +2,7 @@ use global_hotkey::hotkey::{Code, HotKey, Modifiers}; use iced::futures::channel::mpsc::Sender; use iced::futures::SinkExt; use tokio::runtime::Handle; -use common::model::{PhysicalKey, PhysicalShortcut}; +use gauntlet_common::model::{PhysicalKey, PhysicalShortcut}; use crate::ui::AppMsg; pub fn register_listener(msg_sender: Sender) { diff --git a/rust/client/src/lib.rs b/rust/client/src/lib.rs index c8dde8d..601ab25 100644 --- a/rust/client/src/lib.rs +++ b/rust/client/src/lib.rs @@ -1,7 +1,7 @@ -use common::dirs::Dirs; -use common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; -use common::rpc::backend_api::BackendApi; -use utils::channel::{RequestReceiver, RequestSender}; +use gauntlet_common::dirs::Dirs; +use gauntlet_common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; +use gauntlet_common::rpc::backend_api::BackendApi; +use gauntlet_utils::channel::{RequestReceiver, RequestSender}; use crate::ui::GauntletComplexTheme; pub(in crate) mod ui; diff --git a/rust/client/src/model.rs b/rust/client/src/model.rs index 06a866a..981e5e5 100644 --- a/rust/client/src/model.rs +++ b/rust/client/src/model.rs @@ -1,4 +1,4 @@ -use common::model::{UiPropertyValue, UiWidgetId}; +use gauntlet_common::model::{UiPropertyValue, UiWidgetId}; use crate::ui::AppMsg; #[derive(Debug, Clone)] diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index 6c46b67..346df6a 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -2,7 +2,7 @@ use crate::model::UiViewEvent; use crate::ui::widget::{ActionPanel, ComponentWidgetEvent}; use crate::ui::widget_container::PluginWidgetContainer; use crate::ui::AppMsg; -use common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiWidgetId}; +use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiWidgetId}; use iced::Task; use std::collections::HashMap; use std::sync::Arc; diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 238987d..45b2422 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -24,12 +24,12 @@ use serde::Deserialize; use tokio::sync::{Mutex as TokioMutex, RwLock as TokioRwLock}; use client_context::ClientContext; -use common::model::{BackendRequestData, BackendResponseData, EntrypointId, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; -use common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFrontendApiError}; -use common::scenario_convert::{ui_render_location_from_scenario}; -use common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; -use common_ui::physical_key_model; -use utils::channel::{RequestReceiver, RequestSender, Responder}; +use gauntlet_common::model::{BackendRequestData, BackendResponseData, EntrypointId, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; +use gauntlet_common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFrontendApiError}; +use gauntlet_common::scenario_convert::{ui_render_location_from_scenario}; +use gauntlet_common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; +use gauntlet_common_ui::physical_key_model; +use gauntlet_utils::channel::{RequestReceiver, RequestSender, Responder}; use crate::model::UiViewEvent; use crate::ui::search_list::search_list; diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index 3fa01a4..cc33100 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -6,7 +6,7 @@ use iced::widget::component; use iced::widget::row; use iced::widget::text; use iced::widget::text::Shaping; -use common::model::SearchResult; +use gauntlet_common::model::SearchResult; use crate::ui::scroll_handle::ScrollHandle; use crate::ui::theme::{Element, GauntletComplexTheme, ThemableWidget}; use crate::ui::theme::button::ButtonStyle; diff --git a/rust/client/src/ui/state/main_view.rs b/rust/client/src/ui/state/main_view.rs index 5502cb1..7815ee9 100644 --- a/rust/client/src/ui/state/main_view.rs +++ b/rust/client/src/ui/state/main_view.rs @@ -1,5 +1,5 @@ use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_ACTION_ITEM_HEIGHT}; -use common::model::{SearchResultEntrypointAction, UiWidgetId}; +use gauntlet_common::model::{SearchResultEntrypointAction, UiWidgetId}; pub enum MainViewState { None, diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 39e6121..2abe893 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -6,7 +6,7 @@ use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_MAIN_LIST_ITEM_HEIGHT}; pub use crate::ui::state::main_view::MainViewState; pub use crate::ui::state::plugin_view::PluginViewState; use crate::ui::AppMsg; -use common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult}; +use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult}; use iced::widget::text_input; use iced::widget::text_input::focus; use iced::Task; diff --git a/rust/client/src/ui/state/plugin_view.rs b/rust/client/src/ui/state/plugin_view.rs index e9924e2..cb5b2e8 100644 --- a/rust/client/src/ui/state/plugin_view.rs +++ b/rust/client/src/ui/state/plugin_view.rs @@ -1,5 +1,5 @@ use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_ACTION_ITEM_HEIGHT}; -use common::model::UiWidgetId; +use gauntlet_common::model::UiWidgetId; #[derive(Debug, Clone)] pub enum PluginViewState { diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index e1ea86c..3bc487f 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -203,7 +203,7 @@ impl container::Catalog for GauntletComplexTheme { Style { background: Some(panel_theme.background_color.to_iced().into()), border: Border { - radius: common_ui::radius(0.0, 0.0, root_theme.border_radius, root_theme.border_radius), + radius: gauntlet_common_ui::radius(0.0, 0.0, root_theme.border_radius, root_theme.border_radius), width: root_theme.border_width, color: root_theme.border_color.to_iced(), }, @@ -261,7 +261,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau } ContainerStyle::ActionShortcutModifiersInit => { let horizontal_spacing = theme.action_shortcut_modifier.spacing; - self.padding(common_ui::padding(0.0, horizontal_spacing, 0.0, 0.0)) + self.padding(gauntlet_common_ui::padding(0.0, horizontal_spacing, 0.0, 0.0)) } ContainerStyle::ActionPanel => { self.class(ContainerStyleInner::ActionPanel) @@ -406,14 +406,14 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau self.padding(theme.root_bottom_panel_primary_action_text.padding.to_iced()) } ContainerStyle::RootBottomPanelPrimaryActionButton => { - self.padding(common_ui::padding(0.0, theme.root_bottom_panel.spacing, 0.0, 0.0)) + self.padding(gauntlet_common_ui::padding(0.0, theme.root_bottom_panel.spacing, 0.0, 0.0)) } ContainerStyle::TextAccessory => { self.padding(theme.text_accessory.padding.to_iced()) } ContainerStyle::TextAccessoryIcon => { let horizontal_spacing = theme.text_accessory.spacing; - self.padding(common_ui::padding(0.0, horizontal_spacing, 0.0, 0.0)) + self.padding(gauntlet_common_ui::padding(0.0, horizontal_spacing, 0.0, 0.0)) } ContainerStyle::IconAccessory => { self.padding(theme.icon_accessory.padding.to_iced()) diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 474ecf8..46d7a1c 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -4,7 +4,7 @@ use iced::{application, Color, Padding}; use iced::application::DefaultStyle; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use common::dirs::Dirs; +use gauntlet_common::dirs::Dirs; pub mod button; pub mod text_input; diff --git a/rust/client/src/ui/theme/row.rs b/rust/client/src/ui/theme/row.rs index 7bc0d7f..57c2c0a 100644 --- a/rust/client/src/ui/theme/row.rs +++ b/rust/client/src/ui/theme/row.rs @@ -37,12 +37,12 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletC } RowStyle::ListFirstSectionTitle => { let padding = theme.list_section_title.padding.to_iced(); - self.padding(common_ui::padding(padding.bottom, padding.right, padding.bottom, padding.left)) + self.padding(gauntlet_common_ui::padding(padding.bottom, padding.right, padding.bottom, padding.left)) .spacing(theme.list_section_title.spacing) } RowStyle::GridFirstSectionTitle => { let padding = theme.grid_section_title.padding.to_iced(); - self.padding(common_ui::padding(0.0, padding.right, padding.bottom, padding.left)) + self.padding(gauntlet_common_ui::padding(0.0, padding.right, padding.bottom, padding.left)) .spacing(theme.grid_section_title.spacing) } RowStyle::GridItemTitle => { diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 56a81aa..c578a99 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -15,8 +15,8 @@ use crate::ui::theme::text_input::TextInputStyle; use crate::ui::theme::tooltip::TooltipStyle; use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::AppMsg; -use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Icons, Image, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiWidgetId}; -use common_ui::shortcut_to_text; +use gauntlet_common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Icons, Image, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiWidgetId}; +use gauntlet_common_ui::shortcut_to_text; use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; use iced::widget::image::Handle; @@ -2567,7 +2567,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( let action_panel = render_action_panel(action_panel, on_action_click, action_panel_scroll_handle); let action_panel: Element<_>= container(action_panel) - .padding(common_ui::padding(0.0, 8.0, 48.0, 0.0)) + .padding(gauntlet_common_ui::padding(0.0, 8.0, 48.0, 0.0)) .align_right(Length::Fill) .align_bottom(Length::Fill) .into(); diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 1ee3b71..84d98c3 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -3,7 +3,7 @@ use crate::model::UiViewEvent; use crate::ui::state::PluginViewState; use crate::ui::theme::Element; use crate::ui::widget::{create_state, ActionPanel, ComponentWidgetEvent, ComponentWidgetState, ComponentWidgets}; -use common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiWidgetId}; +use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiWidgetId}; use std::collections::HashMap; use std::mem; use std::ops::DerefMut; diff --git a/rust/common/Cargo.toml b/rust/common/Cargo.toml index 49c5b08..7318e97 100644 --- a/rust/common/Cargo.toml +++ b/rust/common/Cargo.toml @@ -1,28 +1,38 @@ [package] -name = "common" -edition = "2021" +name = "gauntlet-common" +edition.workspace = true [dependencies] -gix-url = { version = "0.22.0" } -anyhow = { version = "1", features = ["backtrace"] } -tonic = "0.11.0" -prost = "0.12.3" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -tokio = "1.37.0" +# worspaces +gauntlet-utils.workspace = true + +# shared +anyhow.workspace = true +tokio.workspace = true +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +bincode.workspace = true +tonic.workspace = true +prost.workspace = true +bytes.workspace = true + +# other +gix-url = { version = "0.28.1" } base64 = "0.22" -utils = { path = "../utils" } -bytes = "1.6.0" -thiserror = "1" directories = "5.0" -bincode = { version = "2.0.0-rc.3", features = ["serde"] } [build-dependencies] -tonic-build = "0.11.0" -component_model = { path = "../component_model" } +# workspaces +gauntlet-component-model.workspace = true + +# shared +itertools.workspace = true +indexmap.workspace = true +tonic-build.workspace = true + +# other convert_case = "0.6.0" -itertools = "0.10.5" -indexmap = "2.1.0" [features] release = [] diff --git a/rust/common/build.rs b/rust/common/build.rs index 179c2bb..b187d16 100644 --- a/rust/common/build.rs +++ b/rust/common/build.rs @@ -3,7 +3,7 @@ use std::fs::File; use std::io::Write; use std::ops::Deref; use std::path::Path; -use component_model::{create_component_model, Arity, Children, Component, ComponentName, ComponentRef, Property, PropertyKind, PropertyType, SharedType}; +use gauntlet_component_model::{create_component_model, Arity, Children, Component, ComponentName, ComponentRef, Property, PropertyKind, PropertyType, SharedType}; use itertools::Itertools; use convert_case::{Case, Casing}; @@ -12,7 +12,7 @@ use indexmap::IndexMap; fn main() -> Result<(), Box> { tonic_build::configure() .protoc_arg("--experimental_allow_proto3_optional") - .compile( + .compile_protos( &["./../../schema/backend.proto"], &["./../../schema/"], )?; diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index c3d980f..6dec40b 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -1,10 +1,9 @@ use std::collections::HashMap; -use anyhow::anyhow; use thiserror::Error; use tonic::{Code, Request}; use tonic::transport::Channel; -use utils::channel::{RequestError, RequestSender}; +use gauntlet_utils::channel::{RequestError, RequestSender}; use crate::model::{BackendRequestData, BackendResponseData, DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, UiPropertyValue, UiWidgetId}; use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadStatus, RpcDownloadStatusRequest, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcPingRequest, RpcPluginsRequest, RpcRemovePluginRequest, RpcSaveLocalPluginRequest, RpcSetEntrypointStateRequest, RpcSetGlobalShortcutRequest, RpcSetPluginStateRequest, RpcSetPreferenceValueRequest, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowWindowRequest}; @@ -227,8 +226,8 @@ impl From for BackendApiError { } } -impl From for BackendApiError { - fn from(error: prost::DecodeError) -> BackendApiError { +impl From for BackendApiError { + fn from(error: prost::UnknownEnumValue) -> BackendApiError { BackendApiError::Internal { display: format!("{}", error) } diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index 83d07eb..150d4d1 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use anyhow::anyhow; use thiserror::Error; -use utils::channel::{RequestError, RequestSender}; +use gauntlet_utils::channel::{RequestError, RequestSender}; use crate::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; diff --git a/rust/common_ui/Cargo.toml b/rust/common_ui/Cargo.toml index a7f6ec2..5bf4da6 100644 --- a/rust/common_ui/Cargo.toml +++ b/rust/common_ui/Cargo.toml @@ -1,9 +1,12 @@ [package] -name = "common-ui" -edition = "2021" +name = "gauntlet-common-ui" +edition.workspace = true [dependencies] -common = { path = "../common" } +# workspaces +gauntlet-common.workspace = true + +# shared iced.workspace = true iced_aw.workspace = true iced_fonts.workspace = true diff --git a/rust/common_ui/src/lib.rs b/rust/common_ui/src/lib.rs index 555336f..ab3b4a3 100644 --- a/rust/common_ui/src/lib.rs +++ b/rust/common_ui/src/lib.rs @@ -4,7 +4,7 @@ use iced::keyboard::Modifiers; use iced::widget::{text, value}; use iced_aw::iced_fonts::{bootstrap, Bootstrap, BOOTSTRAP_FONT}; -use common::model::{PhysicalKey, PhysicalShortcut}; +use gauntlet_common::model::{PhysicalKey, PhysicalShortcut}; pub fn padding(top: impl Into, right: impl Into, bottom: impl Into, left: impl Into) -> Padding { Padding { diff --git a/rust/component_model/Cargo.toml b/rust/component_model/Cargo.toml index e2e675f..39828ee 100644 --- a/rust/component_model/Cargo.toml +++ b/rust/component_model/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "component_model" -edition = "2021" +name = "gauntlet-component-model" +edition.workspace = true [dependencies] -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0" } -anyhow = { version = "1", features = ["backtrace"] } -indexmap = { version = "2.1.0", features = ["serde"] } +serde.workspace = true +serde_json.workspace = true +anyhow.workspace = true +indexmap.workspace = true diff --git a/rust/component_model/src/main.rs b/rust/component_model/src/main.rs index 51edd16..4ff086f 100644 --- a/rust/component_model/src/main.rs +++ b/rust/component_model/src/main.rs @@ -1,6 +1,6 @@ use std::{env, fs}; use anyhow::anyhow; -use component_model::create_component_model; +use gauntlet_component_model::create_component_model; fn main() -> anyhow::Result<()> { let args: Vec = env::args().collect(); diff --git a/rust/management_client/Cargo.toml b/rust/management_client/Cargo.toml index c7eb68b..32120a8 100644 --- a/rust/management_client/Cargo.toml +++ b/rust/management_client/Cargo.toml @@ -1,19 +1,18 @@ [package] -name = "management_client" -edition = "2021" +name = "gauntlet-management-client" +edition.workspace = true [dependencies] -tokio = "1.28.1" -clap = { version = "4.3.22", features = ["derive"] } -anyhow = { version = "1", features = ["backtrace"] } -thiserror = "1.0.48" +# workspaces +gauntlet-common.workspace = true +gauntlet-common-ui.workspace = true + +# shared +anyhow.workspace = true iced.workspace = true iced_aw.workspace = true iced_table.workspace = true iced_fonts.workspace = true -tracing = "0.1" -tracing-subscriber = "0.3" -common = { path = "../common" } -common-ui = { path = "../common_ui" } -itertools = "0.12.1" -tonic = "0.11.0" \ No newline at end of file +tracing.workspace = true +tracing-subscriber.workspace = true +itertools.workspace = true diff --git a/rust/management_client/src/components/shortcut_selector.rs b/rust/management_client/src/components/shortcut_selector.rs index bfe118f..3ef1576 100644 --- a/rust/management_client/src/components/shortcut_selector.rs +++ b/rust/management_client/src/components/shortcut_selector.rs @@ -7,8 +7,8 @@ use iced::mouse::Button; use iced::widget::{container, row, text}; use iced::widget::container::{draw_background, layout}; -use common::model::PhysicalShortcut; -use common_ui::{physical_key_model, shortcut_to_text}; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common_ui::{physical_key_model, shortcut_to_text}; pub struct ShortcutSelector<'a, Message, Theme> where diff --git a/rust/management_client/src/main.rs b/rust/management_client/src/main.rs index d307bf9..ec3b659 100644 --- a/rust/management_client/src/main.rs +++ b/rust/management_client/src/main.rs @@ -1,5 +1,5 @@ fn main() { tracing_subscriber::fmt::init(); - management_client::start_management_client(); + gauntlet_management_client::start_management_client(); } diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index bc7afd7..306b6fb 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -8,9 +8,9 @@ use iced_aw::Spinner; use iced_fonts::{Bootstrap, BOOTSTRAP_FONT, BOOTSTRAP_FONT_BYTES}; use itertools::Itertools; -use common::model::{DownloadStatus, PluginId}; -use common::rpc::backend_api::{BackendApi, BackendApiError}; -use common_ui::padding; +use gauntlet_common::model::{DownloadStatus, PluginId}; +use gauntlet_common::rpc::backend_api::{BackendApi, BackendApiError}; +use gauntlet_common_ui::padding; use crate::theme::{Element, GauntletSettingsTheme}; use crate::theme::button::ButtonStyle; use crate::theme::container::ContainerStyle; @@ -731,7 +731,7 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .into(); container(content) - .padding(common_ui::padding(8.0, 60.0, 0.0, 0.0)) + .padding(gauntlet_common_ui::padding(8.0, 60.0, 0.0, 0.0)) .align_right(Length::Fill) .align_top(Length::Fill) .into() diff --git a/rust/management_client/src/views/general.rs b/rust/management_client/src/views/general.rs index 8b0ba93..fbfa96e 100644 --- a/rust/management_client/src/views/general.rs +++ b/rust/management_client/src/views/general.rs @@ -1,8 +1,8 @@ use crate::components::shortcut_selector::ShortcutSelector; use crate::theme::text::TextStyle; use crate::theme::Element; -use common::model::PhysicalShortcut; -use common::rpc::backend_api::{BackendApi, BackendApiError}; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::rpc::backend_api::{BackendApi, BackendApiError}; use iced::alignment::Horizontal; use iced::widget::text::Shaping; use iced::widget::tooltip::Position; diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index 06444da..4f87258 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -6,9 +6,9 @@ use iced::{padding, Alignment, Length, Padding, Task}; use iced::widget::{button, column, container, row, scrollable, text, text_input, value, vertical_rule}; use iced::widget::text::Shaping; use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; -use common::{settings_env_data_from_string, SettingsEnvData}; -use common::model::{EntrypointId, PluginId, PluginPreferenceUserData, SettingsPlugin}; -use common::rpc::backend_api::{BackendApi, BackendApiError}; +use gauntlet_common::{settings_env_data_from_string, SettingsEnvData}; +use gauntlet_common::model::{EntrypointId, PluginId, PluginPreferenceUserData, SettingsPlugin}; +use gauntlet_common::rpc::backend_api::{BackendApi, BackendApiError}; use crate::theme::button::ButtonStyle; use crate::theme::Element; diff --git a/rust/management_client/src/views/plugins/preferences.rs b/rust/management_client/src/views/plugins/preferences.rs index b604f25..f923d44 100644 --- a/rust/management_client/src/views/plugins/preferences.rs +++ b/rust/management_client/src/views/plugins/preferences.rs @@ -3,7 +3,7 @@ use crate::theme::container::ContainerStyle; use crate::theme::text::TextStyle; use crate::theme::Element; use crate::views::plugins::PluginPreferenceUserDataState; -use common::model::{EntrypointId, PluginId, PluginPreference}; +use gauntlet_common::model::{EntrypointId, PluginId, PluginPreference}; use iced::widget::{button, checkbox, column, container, pick_list, row, text, text_input}; use iced::{padding, widget, Length, Padding}; use iced_aw::number_input; diff --git a/rust/management_client/src/views/plugins/table.rs b/rust/management_client/src/views/plugins/table.rs index a524e7c..f653033 100644 --- a/rust/management_client/src/views/plugins/table.rs +++ b/rust/management_client/src/views/plugins/table.rs @@ -8,7 +8,7 @@ use iced::widget::scrollable::Id; use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; use iced_table::table; -use common::model::{EntrypointId, PluginId, SettingsEntrypointType, SettingsPlugin}; +use gauntlet_common::model::{EntrypointId, PluginId, SettingsEntrypointType, SettingsPlugin}; use crate::theme::{Element, GauntletSettingsTheme}; use crate::theme::button::ButtonStyle; diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml index c1b7429..0812dbe 100644 --- a/rust/plugin_runtime/Cargo.toml +++ b/rust/plugin_runtime/Cargo.toml @@ -1,34 +1,39 @@ [package] -name = "plugin_runtime" +name = "gauntlet-plugin-runtime" version = "0.1.0" -edition = "2021" +edition.workspace = true [dependencies] +# workspaces +gauntlet-common.workspace = true +gauntlet-component-model.workspace = true +gauntlet-utils.workspace = true + +# shared +anyhow.workspace = true +tokio.workspace = true +tokio-util.workspace = true +serde.workspace = true +tracing.workspace = true +indexmap.workspace = true +bincode.workspace = true +regex.workspace = true +futures.workspace = true +image.workspace = true +once_cell.workspace = true +bytes.workspace = true +walkdir.workspace = true +typed-path.workspace = true +interprocess.workspace = true + +# other deno_core = { version = "0.321.0" } # deno 2.1.1 deno_runtime = { version = "0.188.0" } -tokio = "1.28.1" -anyhow = { version = "1", features = ["backtrace"] } -regex = "1.9.3" -once_cell = "1.18.0" -serde = { version = "1.0", features = ["derive"] } -tracing = "0.1" -common = { path = "../common" } -typed-path = "0.9" -indexmap = { version = "2.1.0", features = ["serde"] } -component_model = { path = "../component_model" } -bytes = "1.6.0" -image = "0.25" -resvg = { version = "0.41", default-features = false} -walkdir = "2.4.0" +resvg = { version = "0.44.0", default-features = false} numbat = "1.14.0" -interprocess = { version = "2.2.2", features = ["tokio"] } -tokio-util = "0.7.11" -bincode = "2.0.0-rc.3" -utils = { path = "../utils" } -futures = "0.3.31" [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] -libc = "0.2.153" +libc = "0.2" [target.'cfg(target_os = "linux")'.dependencies] freedesktop_entry_parser = "1.3" @@ -36,7 +41,7 @@ freedesktop-icons = "0.2" [target.'cfg(target_os = "macos")'.dependencies] cacao = "0.3.2" -plist = "1.6.1" +plist = "1.7.0" icns = "0.3.1" objc2-app-kit = { version = "0.2.2", features = ["NSWorkspace", "NSImage", "NSImageRep", "NSBitmapImageRep", "NSGraphics", "NSGraphicsContext"] } objc2-foundation = { version = "0.2.2", features = ["NSString"] } diff --git a/rust/plugin_runtime/src/api.rs b/rust/plugin_runtime/src/api.rs index 235bd95..4e48422 100644 --- a/rust/plugin_runtime/src/api.rs +++ b/rust/plugin_runtime/src/api.rs @@ -1,9 +1,9 @@ use crate::model::{JsAdditionalSearchItem, JsClipboardData, JsPreferenceUserData}; use crate::{JsRequest, JsResponse, JsUiRenderLocation}; -use common::model::{EntrypointId, RootWidget, UiRenderLocation}; +use gauntlet_common::model::{EntrypointId, RootWidget, UiRenderLocation}; use std::collections::HashMap; use anyhow::anyhow; -use utils::channel::RequestSender; +use gauntlet_utils::channel::RequestSender; #[allow(async_fn_in_trait)] pub trait BackendForPluginRuntimeApi { diff --git a/rust/plugin_runtime/src/component_model.rs b/rust/plugin_runtime/src/component_model.rs index 10edcb9..6ea6668 100644 --- a/rust/plugin_runtime/src/component_model.rs +++ b/rust/plugin_runtime/src/component_model.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use component_model::{create_component_model, Component}; +use gauntlet_component_model::{create_component_model, Component}; pub struct ComponentModel { components: HashMap, diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index 0206909..5ed7710 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -16,7 +16,7 @@ use once_cell::sync::Lazy; use regex::Regex; use tokio::runtime::Handle; use tokio::sync::mpsc::Receiver; -use common::model::PluginId; +use gauntlet_common::model::PluginId; use crate::api::BackendForPluginRuntimeApiProxy; use crate::assets::{asset_data, asset_data_blocking}; use crate::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; diff --git a/rust/plugin_runtime/src/events.rs b/rust/plugin_runtime/src/events.rs index 8324208..3d988ca 100644 --- a/rust/plugin_runtime/src/events.rs +++ b/rust/plugin_runtime/src/events.rs @@ -7,7 +7,7 @@ use deno_core::{op2, OpState}; use deno_core::futures::{Stream, StreamExt}; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::Receiver; -use common::model::UiWidgetId; +use gauntlet_common::model::UiWidgetId; #[derive(Debug, Deserialize, Serialize, Encode, Decode)] #[serde(tag = "type")] diff --git a/rust/plugin_runtime/src/lib.rs b/rust/plugin_runtime/src/lib.rs index 00139e6..4741d94 100644 --- a/rust/plugin_runtime/src/lib.rs +++ b/rust/plugin_runtime/src/lib.rs @@ -39,7 +39,7 @@ use tokio::runtime::Handle; use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::{oneshot, Mutex, MutexGuard}; use tokio_util::sync::CancellationToken; -use utils::channel::{Payload, RequestReceiver}; +use gauntlet_utils::channel::{Payload, RequestReceiver}; pub use api::BackendForPluginRuntimeApi; pub use events::JsEvent; @@ -73,7 +73,7 @@ async fn run_outer(socket_name: String) -> anyhow::Result<()> { let (mut recver, mut sender) = conn.split(); - let (request_sender, mut request_receiver) = utils::channel::channel::>(); + let (request_sender, mut request_receiver) = gauntlet_utils::channel::channel::>(); let (event_sender, event_receiver) = channel::(10); let response_oneshot = Mutex::new(None); diff --git a/rust/plugin_runtime/src/model.rs b/rust/plugin_runtime/src/model.rs index c8cfa34..6dc001f 100644 --- a/rust/plugin_runtime/src/model.rs +++ b/rust/plugin_runtime/src/model.rs @@ -1,5 +1,5 @@ use crate::JsEvent; -use common::model::{EntrypointId, PluginId, RootWidget}; +use gauntlet_common::model::{EntrypointId, PluginId, RootWidget}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt; diff --git a/rust/plugin_runtime/src/permissions.rs b/rust/plugin_runtime/src/permissions.rs index 3da8f0a..7edcdd1 100644 --- a/rust/plugin_runtime/src/permissions.rs +++ b/rust/plugin_runtime/src/permissions.rs @@ -10,7 +10,7 @@ use deno_runtime::permissions::RuntimePermissionDescriptorParser; use once_cell::sync::Lazy; use regex::Regex; use typed_path::Utf8TypedPath; -use common::dirs::Dirs; +use gauntlet_common::dirs::Dirs; use crate::{JsPluginPermissions, JsPluginPermissionsExec}; pub static PERMISSIONS_VARIABLE_PATTERN: Lazy = Lazy::new(|| Regex::new(r"\{(?.+?):(?.+?)}").expect("invalid regex")); diff --git a/rust/plugin_runtime/src/plugin_data.rs b/rust/plugin_runtime/src/plugin_data.rs index b06c8b7..8ac1ff3 100644 --- a/rust/plugin_runtime/src/plugin_data.rs +++ b/rust/plugin_runtime/src/plugin_data.rs @@ -1,5 +1,5 @@ use std::path::PathBuf; -use common::model::PluginId; +use gauntlet_common::model::PluginId; pub struct PluginData { plugin_id: PluginId, diff --git a/rust/plugin_runtime/src/preferences.rs b/rust/plugin_runtime/src/preferences.rs index 30d8514..2309d76 100644 --- a/rust/plugin_runtime/src/preferences.rs +++ b/rust/plugin_runtime/src/preferences.rs @@ -1,4 +1,4 @@ -use common::model::EntrypointId; +use gauntlet_common::model::EntrypointId; use deno_core::futures::executor::block_on; use deno_core::{op2, OpState}; use std::cell::RefCell; diff --git a/rust/plugin_runtime/src/ui.rs b/rust/plugin_runtime/src/ui.rs index 7bbb57b..f720b1c 100644 --- a/rust/plugin_runtime/src/ui.rs +++ b/rust/plugin_runtime/src/ui.rs @@ -9,9 +9,9 @@ use indexmap::IndexMap; use serde::{de, Deserialize, Deserializer, Serialize}; use serde::de::Error; use tokio::runtime::Handle; -use common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, EntrypointId, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Image, ImageSource, ImageSourceAsset, ImageSourceUrl, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectItemWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiPropertyValue, UiRenderLocation, UiWidgetId, WidgetVisitor}; -use component_model::{Component, Property, PropertyType, SharedType}; -use component_model::Component::Root; +use gauntlet_common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, EntrypointId, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Image, ImageSource, ImageSourceAsset, ImageSourceUrl, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectItemWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiPropertyValue, UiRenderLocation, UiWidgetId, WidgetVisitor}; +use gauntlet_component_model::{Component, Property, PropertyType, SharedType}; +use gauntlet_component_model::Component::Root; use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; use crate::component_model::ComponentModel; use crate::model::JsUiRenderLocation; diff --git a/rust/scenario_runner/Cargo.toml b/rust/scenario_runner/Cargo.toml index 4938fcc..837dca2 100644 --- a/rust/scenario_runner/Cargo.toml +++ b/rust/scenario_runner/Cargo.toml @@ -1,13 +1,15 @@ [package] -name = "scenario_runner" -edition = "2021" +name = "gauntlet-scenario-runner" +edition.workspace = true [dependencies] -tokio = { version = "1.28.1", features = ["rt", "rt-multi-thread", "macros"] } -tonic = "0.11.0" -common = { path = "../common" } -utils = { path = "../utils" } -anyhow = { version = "1", features = ["backtrace"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +# workspaces +gauntlet-common.workspace = true +gauntlet-utils.workspace = true + +# shared +tokio.workspace = true +anyhow.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/rust/scenario_runner/src/frontend_mock.rs b/rust/scenario_runner/src/frontend_mock.rs index 8eca567..aa78a71 100644 --- a/rust/scenario_runner/src/frontend_mock.rs +++ b/rust/scenario_runner/src/frontend_mock.rs @@ -1,12 +1,12 @@ use std::fs; use std::path::Path; -use common::model::{BackendRequestData, BackendResponseData, EntrypointId, PluginId, UiRequestData, UiResponseData}; -use common::rpc::backend_api::{BackendApi, BackendForFrontendApi}; -use common::rpc::backend_server::wait_for_backend_server; -use common::scenario_convert::{ui_render_location_to_scenario}; -use common::scenario_model::ScenarioFrontendEvent; -use utils::channel::{RequestReceiver, RequestSender}; +use gauntlet_common::model::{BackendRequestData, BackendResponseData, EntrypointId, PluginId, UiRequestData, UiResponseData}; +use gauntlet_common::rpc::backend_api::{BackendApi, BackendForFrontendApi}; +use gauntlet_common::rpc::backend_server::wait_for_backend_server; +use gauntlet_common::scenario_convert::{ui_render_location_to_scenario}; +use gauntlet_common::scenario_model::ScenarioFrontendEvent; +use gauntlet_utils::channel::{RequestReceiver, RequestSender}; use crate::model::ScenarioBackendEvent; @@ -164,15 +164,14 @@ async fn request_loop(mut request_receiver: RequestReceiver { let event = ScenarioFrontendEvent::ReplaceView { entrypoint_id: entrypoint_id.to_string(), render_location: ui_render_location_to_scenario(render_location), top_level_view, - container: container_value, + container, images, }; diff --git a/rust/scenario_runner/src/lib.rs b/rust/scenario_runner/src/lib.rs index 1f73c85..c3f0714 100644 --- a/rust/scenario_runner/src/lib.rs +++ b/rust/scenario_runner/src/lib.rs @@ -1,5 +1,5 @@ -use common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; -use utils::channel::{RequestReceiver, RequestSender}; +use gauntlet_common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; +use gauntlet_utils::channel::{RequestReceiver, RequestSender}; pub mod frontend_mock; mod model; diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 6eb6414..dcf4054 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -1,50 +1,49 @@ [package] -name = "server" -edition = "2021" +name = "gauntlet-server" +edition.workspace = true [dependencies] -serde = { version = "1.0", features = ["derive"] } +# workspaces +gauntlet-common.workspace = true +gauntlet-utils.workspace = true +gauntlet-client.workspace = true +gauntlet-plugin-runtime.workspace = true +gauntlet-scenario-runner = { workspace = true, optional = true } -tokio = "1.28.1" -tokio-util = "0.7.11" -toml = "0.8.10" -tantivy = "0.22.0" -regex = "1.9.3" -once_cell = "1.18.0" -git2 = { version = "0.19.0", features = ["vendored-libgit2", "vendored-openssl"] } +# shared +anyhow.workspace = true +serde.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tracing.workspace = true +itertools.workspace = true +regex.workspace = true +futures.workspace = true +image.workspace = true +once_cell.workspace = true +tonic.workspace = true +bytes.workspace = true +walkdir.workspace = true +typed-path.workspace = true +interprocess.workspace = true + +# other +toml = "0.8" +tantivy = "0.22" +git2 = { version = "0.19", features = ["vendored-libgit2", "vendored-openssl"] } tempfile = "3" -async-stream = "0.3.5" -anyhow = { version = "1", features = ["backtrace"] } -thiserror = "1.0.48" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -sqlx = { version = "0.8.2", features = [ "runtime-tokio", "json", "sqlite" ] } -common = { path = "../common" } -utils = { path = "../utils" } -indexmap = { version = "2.1.0", features = ["serde"] } -tonic = "0.11.0" -client = { path = "../client" } -walkdir = "2.4.0" -include_dir = "0.7.3" +sqlx = { version = "0.8", features = [ "runtime-tokio", "json", "sqlite" ] } +include_dir = "0.7" open = "5" uuid = "1.8" -arboard = { version = "=3.2.1", features = ["wayland-data-control"] } # TODO update when dependency hell is solved -bytes = "1.6.0" -typed-path = "0.9" -plugin_runtime = { path = "../plugin_runtime" } -futures = "0.3.31" -url = "2.5.4" -image = "0.25" -interprocess = { version = "2.2.2", features = ["tokio"] } -ureq = "2.10.0" - -scenario_runner = { path = "../scenario_runner", optional = true } -itertools = "0.10.5" -vergen-pretty = "0.3.5" +arboard = { version = "3.4", features = ["wayland-data-control"] } +url = "2.5" +ureq = "2.10" +vergen-pretty = "0.3" [features] -release = ["common/release"] -scenario_runner = ["dep:scenario_runner", "common/scenario_runner", "plugin_runtime/scenario_runner"] +release = ["gauntlet-common/release"] +scenario_runner = ["dep:gauntlet-scenario-runner", "gauntlet-common/scenario_runner", "gauntlet-plugin-runtime/scenario_runner"] [build-dependencies] -vergen-gitcl = { version = "1.0.1", features = ["build", "cargo"] } +vergen-gitcl = { version = "1.0", features = ["build", "cargo"] } diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index 3e921e5..e9a3600 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -1,13 +1,13 @@ use std::rc::Rc; use std::sync::Arc; use vergen_pretty::vergen_pretty_env; -use client::{open_window, start_client}; -use common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; -use common::rpc::backend_api::BackendApi; -use common::rpc::backend_server::start_backend_server; -use common::{settings_env_data_from_string, settings_env_data_to_string, SettingsEnvData}; -use plugin_runtime::run_plugin_runtime; -use utils::channel::{channel, RequestReceiver, RequestSender}; +use gauntlet_client::{open_window, start_client}; +use gauntlet_common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; +use gauntlet_common::rpc::backend_api::BackendApi; +use gauntlet_common::rpc::backend_server::start_backend_server; +use gauntlet_common::{settings_env_data_from_string, settings_env_data_to_string, SettingsEnvData}; +use gauntlet_plugin_runtime::run_plugin_runtime; +use gauntlet_utils::channel::{channel, RequestReceiver, RequestSender}; use crate::plugins::ApplicationManager; use crate::rpc::BackendServerImpl; use crate::search::SearchIndex; @@ -123,7 +123,7 @@ fn start_frontend_mock( .build() .expect("unable to start frontend mock tokio runtime") .block_on(async { - scenario_runner::run_scenario_runner_frontend_mock(request_receiver, backend_sender).await + gauntlet_scenario_runner::run_scenario_runner_frontend_mock(request_receiver, backend_sender).await }) .unwrap(); } diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index db52beb..18f92d8 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -1,4 +1,4 @@ -use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, UiPropertyValue, UiWidgetId}; +use gauntlet_common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, UiPropertyValue, UiWidgetId}; #[derive(Debug)] diff --git a/rust/server/src/plugins/clipboard.rs b/rust/server/src/plugins/clipboard.rs index a350889..691ea55 100644 --- a/rust/server/src/plugins/clipboard.rs +++ b/rust/server/src/plugins/clipboard.rs @@ -3,7 +3,7 @@ use arboard::ImageData; use image::RgbaImage; use std::io::Cursor; use std::sync::{Arc, RwLock}; -use plugin_runtime::JsClipboardData; +use gauntlet_plugin_runtime::JsClipboardData; #[derive(Clone)] pub struct Clipboard { diff --git a/rust/server/src/plugins/config_reader.rs b/rust/server/src/plugins/config_reader.rs index 5823953..109dda2 100644 --- a/rust/server/src/plugins/config_reader.rs +++ b/rust/server/src/plugins/config_reader.rs @@ -1,6 +1,6 @@ use serde::Deserialize; -use common::dirs::Dirs; +use gauntlet_common::dirs::Dirs; use crate::plugins::data_db_repository::{DataDbRepository, DbWritePendingPlugin}; pub struct ConfigReader { diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index de61cc3..17f8c95 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -11,8 +11,8 @@ use sqlx::sqlite::SqliteConnectOptions; use sqlx::types::Json; use typed_path::TypedPathBuf; use uuid::Uuid; -use common::model::{PhysicalKey, PhysicalShortcut, PluginId}; -use common::dirs::Dirs; +use gauntlet_common::model::{PhysicalKey, PhysicalShortcut, PluginId}; +use gauntlet_common::dirs::Dirs; use crate::model::ActionShortcutKey; use crate::plugins::frecency::{FrecencyItemStats, FrecencyMetaParams}; use crate::plugins::loader::PluginManifestActionShortcutKey; diff --git a/rust/server/src/plugins/download_status.rs b/rust/server/src/plugins/download_status.rs index 6c59cbe..f73cf15 100644 --- a/rust/server/src/plugins/download_status.rs +++ b/rust/server/src/plugins/download_status.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::Duration; -use common::model::{DownloadStatus, PluginId}; +use gauntlet_common::model::{DownloadStatus, PluginId}; pub struct DownloadStatusHolder { running_downloads: Arc>> diff --git a/rust/server/src/plugins/icon_cache.rs b/rust/server/src/plugins/icon_cache.rs index 41007bc..e57f21b 100644 --- a/rust/server/src/plugins/icon_cache.rs +++ b/rust/server/src/plugins/icon_cache.rs @@ -1,5 +1,5 @@ use anyhow::anyhow; -use common::dirs::Dirs; +use gauntlet_common::dirs::Dirs; #[derive(Clone)] pub struct IconCache { diff --git a/rust/server/src/plugins/image_gatherer.rs b/rust/server/src/plugins/image_gatherer.rs index f3747b3..88917f1 100644 --- a/rust/server/src/plugins/image_gatherer.rs +++ b/rust/server/src/plugins/image_gatherer.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use common::model::{Image, ImageSource, ImageSourceAsset, ImageSourceUrl, RootWidget, UiWidgetId, WidgetVisitor}; -use plugin_runtime::BackendForPluginRuntimeApi; +use gauntlet_common::model::{Image, ImageSource, ImageSourceAsset, ImageSourceUrl, RootWidget, UiWidgetId, WidgetVisitor}; +use gauntlet_plugin_runtime::BackendForPluginRuntimeApi; use crate::plugins::js::BackendForPluginRuntimeApiImpl; use futures::StreamExt; use std::io::Read; diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index f492ffa..c2a7c55 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -14,24 +14,22 @@ use std::time::Duration; use anyhow::{anyhow, Context}; use bytes::Bytes; use futures::AsyncBufReadExt; -use indexmap::IndexMap; use interprocess::local_socket::{ListenerOptions, ToFsName, ToNsName}; use interprocess::local_socket::tokio::{RecvHalf, SendHalf}; use interprocess::local_socket::traits::tokio::{Listener, Stream}; use interprocess::TryClone; use once_cell::sync::Lazy; -use regex::Regex; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::net::TcpStream; use tokio::sync::Mutex; use tokio::task::spawn_blocking; use tokio_util::sync::CancellationToken; -use common::dirs::Dirs; -use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; -use common::rpc::frontend_api::FrontendApi; -use common::settings_env_data_to_string; -use plugin_runtime::{recv_message, send_message, BackendForPluginRuntimeApi, JsAdditionalSearchItem, JsClipboardData, JsInit, JsKeyboardEventOrigin, JsPluginCode, JsPluginPermissions, JsPreferenceUserData, JsEvent, JsUiPropertyValue, JsRequest, JsUiRenderLocation, JsResponse, JsMessage, JsPluginPermissionsFileSystem, JsPluginPermissionsExec, JsPluginPermissionsMainSearchBar, JsMessageSide}; +use gauntlet_common::dirs::Dirs; +use gauntlet_common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; +use gauntlet_common::rpc::frontend_api::FrontendApi; +use gauntlet_common::settings_env_data_to_string; +use gauntlet_plugin_runtime::{recv_message, send_message, BackendForPluginRuntimeApi, JsAdditionalSearchItem, JsClipboardData, JsInit, JsKeyboardEventOrigin, JsPluginCode, JsPluginPermissions, JsPreferenceUserData, JsEvent, JsUiPropertyValue, JsRequest, JsUiRenderLocation, JsResponse, JsMessage, JsPluginPermissionsFileSystem, JsPluginPermissionsExec, JsPluginPermissionsMainSearchBar, JsMessageSide}; use crate::model::{IntermediateUiEvent}; use crate::plugins::clipboard::Clipboard; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index d31e643..57e2158 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -12,11 +12,9 @@ use uuid::Uuid; use walkdir::WalkDir; use itertools::Itertools; use once_cell::sync::Lazy; -use regex::{Match, Regex}; -use tracing_subscriber::fmt::format; use typed_path::{TypedPathBuf, Utf8TypedPath, Utf8UnixComponent, Utf8WindowsComponent, Utf8WindowsPrefix, Utf8WindowsPrefixComponent}; -use common::model::{DownloadStatus, PluginId}; -use plugin_runtime::PERMISSIONS_VARIABLE_PATTERN; +use gauntlet_common::model::{DownloadStatus, PluginId}; +use gauntlet_plugin_runtime::PERMISSIONS_VARIABLE_PATTERN; use crate::model::ActionShortcutKey; use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_to_str, db_plugin_type_to_str, DbCode, DbPluginAction, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbPluginType, DbPreferenceEnumValue, DbWritePlugin, DbWritePluginAssetData, DbWritePluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions, DbPluginPermissionsFileSystem, DbPluginPermissionsExec}; use crate::plugins::download_status::DownloadStatusHolder; diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index f0c3b5e..13f1ea4 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -7,12 +7,12 @@ use anyhow::anyhow; use include_dir::{include_dir, Dir}; use tokio::runtime::Handle; -use common::model::{DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, UiPropertyValue, UiRequestData, UiResponseData, UiWidgetId}; -use common::rpc::frontend_api::FrontendApi; -use common::{settings_env_data_to_string, SettingsEnvData}; -use utils::channel::RequestSender; -use common::dirs::Dirs; -use plugin_runtime::{JsPluginCode, JsPluginPermissions, JsPluginPermissionsExec, JsPluginPermissionsFileSystem, JsPluginPermissionsMainSearchBar}; +use gauntlet_common::model::{DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, UiPropertyValue, UiRequestData, UiResponseData, UiWidgetId}; +use gauntlet_common::rpc::frontend_api::FrontendApi; +use gauntlet_common::{settings_env_data_to_string, SettingsEnvData}; +use gauntlet_utils::channel::RequestSender; +use gauntlet_common::dirs::Dirs; +use gauntlet_plugin_runtime::{JsPluginCode, JsPluginPermissions, JsPluginPermissionsExec, JsPluginPermissionsFileSystem, JsPluginPermissionsMainSearchBar}; use crate::model::{ActionShortcutKey}; use crate::plugins::clipboard::Clipboard; use crate::plugins::config_reader::ConfigReader; diff --git a/rust/server/src/plugins/run_status.rs b/rust/server/src/plugins/run_status.rs index 5c13702..46106f2 100644 --- a/rust/server/src/plugins/run_status.rs +++ b/rust/server/src/plugins/run_status.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex}; use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; -use common::model::PluginId; +use gauntlet_common::model::PluginId; pub struct RunStatusHolder { running_plugins: Arc>> diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index 289fed9..fa31ae1 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; -use common::{settings_env_data_to_string, SettingsEnvData}; -use common::model::{DownloadStatus, EntrypointId, PluginId, PluginPreferenceUserData, SettingsPlugin, UiPropertyValue, SearchResult, UiWidgetId, PhysicalKey, PhysicalShortcut, LocalSaveData}; -use common::rpc::backend_server::BackendServer; +use gauntlet_common::{settings_env_data_to_string, SettingsEnvData}; +use gauntlet_common::model::{DownloadStatus, EntrypointId, PluginId, PluginPreferenceUserData, SettingsPlugin, UiPropertyValue, SearchResult, UiWidgetId, PhysicalKey, PhysicalShortcut, LocalSaveData}; +use gauntlet_common::rpc::backend_server::BackendServer; use crate::plugins::ApplicationManager; use crate::search::SearchIndex; diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index 27ad33a..7d7a7b4 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -6,8 +6,8 @@ use tantivy::collector::TopDocs; use tantivy::query::{AllQuery, BooleanQuery, FuzzyTermQuery, Query, RegexQuery, TermQuery}; use tantivy::schema::*; use tantivy::tokenizer::TokenizerManager; -use common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType}; -use common::rpc::frontend_api::FrontendApi; +use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType}; +use gauntlet_common::rpc::frontend_api::FrontendApi; #[derive(Clone)] pub struct SearchIndex { diff --git a/rust/utils/Cargo.toml b/rust/utils/Cargo.toml index 0d0cbca..93673a3 100644 --- a/rust/utils/Cargo.toml +++ b/rust/utils/Cargo.toml @@ -1,8 +1,7 @@ [package] -name = "utils" -edition = "2021" +name = "gauntlet-utils" +edition.workspace = true [dependencies] -tokio = "1.28.1" -thiserror = "1.0.48" -log = "0.4.22" +tokio.workspace = true +thiserror.workspace = true diff --git a/src/main.rs b/src/main.rs index a6fe15b..37c1b1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ #![cfg_attr(feature = "release", windows_subsystem = "windows")] fn main() { - cli::init(); + gauntlet_cli::init(); } From bbfb17e4b18f0895e3bb3f65ec6469bd47b9b6fa Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:34:03 +0100 Subject: [PATCH 222/540] Fix scenarios not working --- js/scenario_runner_cli/src/main.ts | 22 +++------------------- rust/client/src/ui/mod.rs | 2 ++ rust/common/build.rs | 2 +- rust/common/src/model.rs | 15 +++++++++++++++ rust/server/build.rs | 2 +- rust/server/src/plugins/js.rs | 10 ++++++---- 6 files changed, 28 insertions(+), 25 deletions(-) diff --git a/js/scenario_runner_cli/src/main.ts b/js/scenario_runner_cli/src/main.ts index ccaa248..01923e2 100644 --- a/js/scenario_runner_cli/src/main.ts +++ b/js/scenario_runner_cli/src/main.ts @@ -36,7 +36,6 @@ async function runScenarios(expectedPlugin: string | undefined) { const scenariosRun = path.join(scenarios, "run"); console.log("Building server") - buildServer(projectRoot) console.log("Building scenario plugins") buildScenarioPlugins(projectRoot) @@ -50,7 +49,7 @@ async function runScenarios(expectedPlugin: string | undefined) { console.log("Starting runner") - const backendProcess = spawnSync('target/debug/gauntlet', { + const backendProcess = spawnSync('cargo', ['run', '--features', 'scenario_runner'], { stdio: "inherit", cwd: projectRoot, env: Object.assign(process.env, { @@ -82,8 +81,6 @@ async function runScreenshotGen(expectedPlugin: string | undefined, expectedEntr const scenarios = path.join(projectRoot, "scenarios"); const scenariosOut = path.join(scenarios, "out"); - buildServer(projectRoot) - for (const plugin of readdirSync(scenariosOut)) { if (expectedPlugin) { if (plugin != expectedPlugin) { @@ -116,10 +113,11 @@ async function runScreenshotGen(expectedPlugin: string | undefined, expectedEntr .map(x => (x.charAt(0).toUpperCase() + x.slice(1))) .join(" "); - const frontendReturn = spawnSync('target/debug/gauntlet', { + const frontendReturn = spawnSync('cargo', ['run', '--features', 'scenario_runner'], { stdio: "inherit", cwd: projectRoot, env: Object.assign(process.env, { + RUST_BACKTRACE: "1", RUST_LOG: "gauntlet-client=INFO", GAUNTLET_SCENARIO_RUNNER_TYPE: "screenshot_gen", GAUNTLET_SCREENSHOT_GEN_IN: scenarioFile, @@ -138,20 +136,6 @@ async function runScreenshotGen(expectedPlugin: string | undefined, expectedEntr } } -function buildServer(projectRoot: string) { - const serverBuildResult = spawnSync('cargo', ['build', '--features', 'scenario_runner'], { - stdio: "inherit", - cwd: projectRoot, - env: Object.assign(process.env, { - RUST_BACKTRACE: "1" - }) - }); - - if (serverBuildResult.status !== 0) { - throw new Error(`Unable to compile server, status: ${JSON.stringify(serverBuildResult)}`); - } -} - function buildScenarioPlugins(projectRoot: string) { const scenarioPluginBuildResult = spawnSync('npm', ['run', 'build-all'], { stdio: "inherit", diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 45b2422..d9879de 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -404,6 +404,8 @@ fn new( let gen_in = std::env::var("GAUNTLET_SCREENSHOT_GEN_IN") .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_IN"); + println!("Reading scenario file at: {}", gen_in); + let gen_in = fs::read_to_string(gen_in) .expect("Unable to read file at GAUNTLET_SCREENSHOT_GEN_IN"); diff --git a/rust/common/build.rs b/rust/common/build.rs index b187d16..5ab61c5 100644 --- a/rust/common/build.rs +++ b/rust/common/build.rs @@ -558,7 +558,7 @@ fn component_model_generator() -> Result<(), Box> { output.push_str("#[derive(Debug, Serialize, Deserialize, Encode, Decode)]\n"); output.push_str("pub struct RootWidget {\n"); - output.push_str(" #[serde(default, deserialize_with = \"array_to_option\")]\n"); + output.push_str(" #[serde(default, deserialize_with = \"array_to_option\", serialize_with = \"option_to_array\")]\n"); output.push_str(" pub content: Option\n"); output.push_str("}\n"); } diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 18fa0da..6430c5b 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -245,6 +245,21 @@ pub enum KeyboardEventOrigin { PluginView, } +fn option_to_array(value: &Option, serializer: S) -> Result +where + V: Serialize, + S: Serializer, +{ + let value = match value { + None => vec![], + Some(value) => vec![value] + }; + + let res = Vec::<&V>::serialize(&value, serializer)?; + + Ok(res) +} + fn array_to_option<'de, D, V>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, V: Deserialize<'de> { let res = Option::>::deserialize(deserializer)?; diff --git a/rust/server/build.rs b/rust/server/build.rs index 954e2a9..dc35c4f 100644 --- a/rust/server/build.rs +++ b/rust/server/build.rs @@ -1,7 +1,7 @@ use vergen_gitcl::{CargoBuilder, Emitter, GitclBuilder}; fn main() -> Result<(), Box> { - println!("cargo:rerun-if-changed=src/db_migrations"); + println!("cargo:rerun-if-changed=db_migrations"); let gitcl = GitclBuilder::all_git()?; let cargo = CargoBuilder::default() diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index c2a7c55..e1c082b 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -257,15 +257,17 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run let current_exe = std::env::current_exe() .context("unable to get current_exe")?; + #[cfg(not(feature = "scenario_runner"))] std::process::Command::new(current_exe) .env(PLUGIN_RUNTIME_ENV, name_str) .spawn() .context("start plugin runtime process")?; - // use only for debugging, only works if only one plugin is enabled - // std::thread::spawn(move || { - // plugin_runtime::run_plugin_runtime(name_str.to_str().unwrap().to_string()) - // }); + // use only for debugging and scenario_runner, only works if only one plugin is enabled + #[cfg(feature = "scenario_runner")] + std::thread::spawn(move || { + gauntlet_plugin_runtime::run_plugin_runtime(name_str.to_str().unwrap().to_string()) + }); let conn = listener.accept().await?; From 6209ff6272da8ec60c2533d785a51418fe485653 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:33:23 +0100 Subject: [PATCH 223/540] Update all js dependencies, react is still on v18 --- bundled_plugins/gauntlet/package.json | 6 +- dev_plugin/package.json | 4 +- js/api/package.json | 6 +- js/api_build/package.json | 2 +- js/bridge_build/package.json | 2 +- js/build/package.json | 20 +- js/core/package.json | 12 +- js/core/tsconfig.json | 2 +- js/react/package.json | 12 +- js/react/rollup.config.ts | 4 +- js/react_renderer/package.json | 18 +- js/scenario_runner_cli/package.json | 12 +- package-lock.json | 3049 ++++++----------- scenarios/plugins/docs_detail/package.json | 8 +- scenarios/plugins/docs_form/package.json | 8 +- scenarios/plugins/docs_grid/package.json | 8 +- .../docs_inline_separators/package.json | 8 +- .../docs_inline_three_sections/package.json | 8 +- .../docs_inline_two_sections/package.json | 8 +- scenarios/plugins/docs_list/package.json | 8 +- 20 files changed, 1037 insertions(+), 2168 deletions(-) diff --git a/bundled_plugins/gauntlet/package.json b/bundled_plugins/gauntlet/package.json index d507842..677f5dd 100644 --- a/bundled_plugins/gauntlet/package.json +++ b/bundled_plugins/gauntlet/package.json @@ -7,13 +7,13 @@ }, "dependencies": { "@project-gauntlet/api": "file:../../js/api", - "@std/async": "npm:@jsr/std__async@^1.0.8", - "@std/fs": "npm:@jsr/std__fs@^1.0.5" + "@std/async": "npm:@jsr/std__async@^1.0.9", + "@std/fs": "npm:@jsr/std__fs@^1.0.8" }, "devDependencies": { "@types/deno": "^2.0.0", "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "@types/react": "^18.2.14", + "@types/react": "^18.3.18", "typescript": "^5.7.2" } } diff --git a/dev_plugin/package.json b/dev_plugin/package.json index de33485..f5ad565 100644 --- a/dev_plugin/package.json +++ b/dev_plugin/package.json @@ -7,11 +7,11 @@ }, "dependencies": { "@project-gauntlet/api": "file:../js/api", - "@types/lodash": "^4.14.196", + "@types/lodash": "^4.17.13", "lodash": "^4.17.21" }, "devDependencies": { - "@types/react": "^18.2.14", + "@types/react": "^18.3.18", "@types/deno": "^2.0.0", "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", "typescript": "^5.7.2" diff --git a/js/api/package.json b/js/api/package.json index e0c76bb..b3c2413 100644 --- a/js/api/package.json +++ b/js/api/package.json @@ -27,9 +27,9 @@ "devDependencies": { "@project-gauntlet/typings": "*", "@rollup/plugin-alias": "^5.1.1", - "@types/react": "^18.2.35", - "rollup": "^4.27.4", - "tslib": "^2.6.2", + "@types/react": "^18.3.18", + "rollup": "^4.28.1", + "tslib": "^2.8.1", "typescript": "^5.7.2", "glob": "^11.0.0" }, diff --git a/js/api_build/package.json b/js/api_build/package.json index de4a532..6d732f8 100644 --- a/js/api_build/package.json +++ b/js/api_build/package.json @@ -9,7 +9,7 @@ "run-generator": "node dist/index.js" }, "devDependencies": { - "@types/node": "^18.19.67", + "@types/node": "^22.10.2", "typescript": "^5.7.2" } } diff --git a/js/bridge_build/package.json b/js/bridge_build/package.json index 6d01f59..dfbc31d 100644 --- a/js/bridge_build/package.json +++ b/js/bridge_build/package.json @@ -8,7 +8,7 @@ "run-generator": "node dist/index.js" }, "devDependencies": { - "@types/node": "^18.19.67", + "@types/node": "^22.10.2", "typescript": "^5.7.2" } } diff --git a/js/build/package.json b/js/build/package.json index 5f5362b..2b7b24e 100644 --- a/js/build/package.json +++ b/js/build/package.json @@ -13,19 +13,19 @@ }, "type": "module", "dependencies": { - "@actions/core": "^1.10.1", - "commander": "^11.1.0", - "octokit": "^3.1.2", - "simple-git": "^3.22.0", - "cross-spawn": "^7.0.3" + "@actions/core": "^1.11.1", + "commander": "^12.1.0", + "octokit": "^4.0.2", + "simple-git": "^3.27.0", + "cross-spawn": "^7.0.6" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.5", - "@types/node": "^18.19.67", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-typescript": "^12.1.2", + "@types/node": "^22.10.2", "@types/cross-spawn": "^6.0.6", - "tslib": "^2.6.2", + "tslib": "^2.8.1", "typescript": "^5.7.2" } } diff --git a/js/core/package.json b/js/core/package.json index 564b223..1398cae 100644 --- a/js/core/package.json +++ b/js/core/package.json @@ -8,13 +8,13 @@ "@project-gauntlet/api": "*", "@project-gauntlet/typings": "*", "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.5", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-typescript": "^12.1.2", "@types/deno": "^2.0.0", - "@types/react": "^18.2.35", - "rollup": "^4.27.4", - "tslib": "^2.6.2", + "@types/react": "^18.3.18", + "rollup": "^4.28.1", + "tslib": "^2.8.1", "typescript": "^5.7.2" } } diff --git a/js/core/tsconfig.json b/js/core/tsconfig.json index 79c25ee..7b10001 100644 --- a/js/core/tsconfig.json +++ b/js/core/tsconfig.json @@ -6,7 +6,7 @@ "target": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", - "types": ["@project-gauntlet/typings", "@types/deno", "@types/node"] + "types": ["@project-gauntlet/typings", "@types/deno"] }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/js/react/package.json b/js/react/package.json index 0851866..e744a6c 100644 --- a/js/react/package.json +++ b/js/react/package.json @@ -5,14 +5,14 @@ "build": "rollup --config rollup.config.ts --configPlugin typescript" }, "dependencies": { - "react": "^18.2.0" + "react": "^18.3.1" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-replace": "^6.0.1", - "@rollup/plugin-typescript": "^11.1.5", - "rollup": "^4.27.4", - "tslib": "^2.6.2", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-replace": "^6.0.2", + "@rollup/plugin-typescript": "^12.1.2", + "rollup": "^4.28.1", + "tslib": "^2.8.1", "typescript": "^5.7.2" } } diff --git a/js/react/rollup.config.ts b/js/react/rollup.config.ts index 5c2d6fa..a24467a 100644 --- a/js/react/rollup.config.ts +++ b/js/react/rollup.config.ts @@ -57,7 +57,9 @@ const config = (nodeEnv: string, reactBundle: string, outDir: string): RollupOpt }, external: [/^ext:.+/], plugins: [ - commonjs(), + commonjs({ + strictRequires: "auto" + }), replace({ delimiters: ['', ''], values: { diff --git a/js/react_renderer/package.json b/js/react_renderer/package.json index eb0c4e8..1649ffe 100644 --- a/js/react_renderer/package.json +++ b/js/react_renderer/package.json @@ -5,20 +5,20 @@ "build": "tsc --noEmit && rollup --config rollup.config.ts --configPlugin typescript" }, "dependencies": { - "react-reconciler": "^0.29.0" + "react-reconciler": "^0.29.2" }, "devDependencies": { "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-replace": "^6.0.1", - "@rollup/plugin-typescript": "^11.1.5", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-replace": "^6.0.2", + "@rollup/plugin-typescript": "^12.1.2", "@types/deno": "^2.0.0", - "@types/react": "^18.2.35", - "@types/react-reconciler": "^0.28.6", + "@types/react": "^18.3.18", + "@types/react-reconciler": "^0.28.9", "@project-gauntlet/typings": "*", - "rollup": "^4.27.4", - "tslib": "^2.6.2", + "rollup": "^4.28.1", + "tslib": "^2.8.1", "typescript": "^5.7.2" } } diff --git a/js/scenario_runner_cli/package.json b/js/scenario_runner_cli/package.json index 37b73bb..932a58c 100644 --- a/js/scenario_runner_cli/package.json +++ b/js/scenario_runner_cli/package.json @@ -7,14 +7,14 @@ }, "type": "module", "dependencies": { - "commander": "^11.1.0" + "commander": "^12.1.0" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.5", - "@types/node": "^18.19.67", - "tslib": "^2.6.2", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-typescript": "^12.1.2", + "@types/node": "^22.10.2", + "tslib": "^2.8.1", "typescript": "^5.7.2" } } diff --git a/package-lock.json b/package-lock.json index dbe81bd..ce8050a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,276 +24,40 @@ "name": "@project-gauntlet/bundled-plugin", "dependencies": { "@project-gauntlet/api": "file:../../js/api", - "@std/async": "npm:@jsr/std__async@^1.0.8", - "@std/fs": "npm:@jsr/std__fs@^1.0.5" + "@std/async": "npm:@jsr/std__async@^1.0.9", + "@std/fs": "npm:@jsr/std__fs@^1.0.8" }, "devDependencies": { "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", "@types/deno": "^2.0.0", - "@types/react": "^18.2.14", + "@types/react": "^18.3.18", "typescript": "^5.7.2" } }, - "bundled_plugins/gauntlet/node_modules/@project-gauntlet/tools": { - "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", - "dev": true, - "dependencies": { - "@grpc/grpc-js": "^1.12.5", - "@grpc/proto-loader": "^0.7.13", - "@rollup/plugin-commonjs": "^28.0.2", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "chalk": "^5.4.0", - "commander": "^12.1.0", - "rollup": "^4.28.1", - "rollup-plugin-cleandir": "^3.0.0", - "rollup-plugin-typescript2": "^0.36.0", - "simple-git": "^3.27.0", - "tail": "^2.2.6", - "toml": "^3.0.0", - "tslib": "^2.8.1", - "typescript": "^5.7.2", - "zod": "^3.24.1", - "zod-validation-error": "^3.4.0" - }, - "bin": { - "gauntlet": "bin/main.js" - } - }, - "bundled_plugins/gauntlet/node_modules/@rollup/plugin-commonjs": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", - "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "bundled_plugins/gauntlet/node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "bundled_plugins/gauntlet/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "bundled_plugins/gauntlet/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "bundled_plugins/gauntlet/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "dev_plugin": { "name": "@project-gauntlet/dev-plugin", "dependencies": { "@project-gauntlet/api": "file:../js/api", - "@types/lodash": "^4.14.196", + "@types/lodash": "^4.17.13", "lodash": "^4.17.21" }, "devDependencies": { "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", "@types/deno": "^2.0.0", - "@types/react": "^18.2.14", + "@types/react": "^18.3.18", "typescript": "^5.7.2" } }, - "dev_plugin/node_modules/@project-gauntlet/tools": { - "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", - "dev": true, - "dependencies": { - "@grpc/grpc-js": "^1.12.5", - "@grpc/proto-loader": "^0.7.13", - "@rollup/plugin-commonjs": "^28.0.2", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "chalk": "^5.4.0", - "commander": "^12.1.0", - "rollup": "^4.28.1", - "rollup-plugin-cleandir": "^3.0.0", - "rollup-plugin-typescript2": "^0.36.0", - "simple-git": "^3.27.0", - "tail": "^2.2.6", - "toml": "^3.0.0", - "tslib": "^2.8.1", - "typescript": "^5.7.2", - "zod": "^3.24.1", - "zod-validation-error": "^3.4.0" - }, - "bin": { - "gauntlet": "bin/main.js" - } - }, - "dev_plugin/node_modules/@rollup/plugin-commonjs": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", - "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "dev_plugin/node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "dev_plugin/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "dev_plugin/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "dev_plugin/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "js/api": { "name": "@project-gauntlet/api", "version": "0.11.0", "devDependencies": { "@project-gauntlet/typings": "*", "@rollup/plugin-alias": "^5.1.1", - "@types/react": "^18.2.35", + "@types/react": "^18.3.18", "glob": "^11.0.0", - "rollup": "^4.27.4", - "tslib": "^2.6.2", + "rollup": "^4.28.1", + "tslib": "^2.8.1", "typescript": "^5.7.2" } }, @@ -301,112 +65,34 @@ "name": "@project-gauntlet/api-build", "version": "0.1.0", "devDependencies": { - "@types/node": "^18.19.67", + "@types/node": "^22.10.2", "typescript": "^5.7.2" } }, - "js/api/node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "js/api/node_modules/jackspeak": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", - "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "js/api/node_modules/lru-cache": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", - "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", - "dev": true, - "engines": { - "node": "20 || >=22" - } - }, - "js/api/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "js/api/node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "js/bridge_build": { "name": "@project-gauntlet/bridge-build", "version": "0.1.0", "devDependencies": { - "@types/node": "^18.19.67", + "@types/node": "^22.10.2", "typescript": "^5.7.2" } }, "js/build": { "name": "@project-gauntlet/build", "dependencies": { - "@actions/core": "^1.10.1", - "commander": "^11.1.0", - "cross-spawn": "^7.0.3", - "octokit": "^3.1.2", - "simple-git": "^3.22.0" + "@actions/core": "^1.11.1", + "commander": "^12.1.0", + "cross-spawn": "^7.0.6", + "octokit": "^4.0.2", + "simple-git": "^3.27.0" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.5", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-typescript": "^12.1.2", "@types/cross-spawn": "^6.0.6", - "@types/node": "^18.19.67", - "tslib": "^2.6.2", + "@types/node": "^22.10.2", + "tslib": "^2.8.1", "typescript": "^5.7.2" } }, @@ -416,70 +102,61 @@ "@project-gauntlet/api": "*", "@project-gauntlet/typings": "*", "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.5", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-typescript": "^12.1.2", "@types/deno": "^2.0.0", - "@types/react": "^18.2.35", - "rollup": "^4.27.4", - "tslib": "^2.6.2", - "typescript": "^5.7.2" - } - }, - "js/deno": { - "name": "@project-gauntlet/deno", - "version": "0.11.0", - "extraneous": true, - "devDependencies": { - "@types/node": "^18.19.67", + "@types/react": "^18.3.18", + "rollup": "^4.28.1", + "tslib": "^2.8.1", "typescript": "^5.7.2" } }, "js/react": { "name": "@project-gauntlet/react", "dependencies": { - "react": "^18.2.0" + "react": "^18.3.1" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-replace": "^6.0.1", - "@rollup/plugin-typescript": "^11.1.5", - "rollup": "^4.27.4", - "tslib": "^2.6.2", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-replace": "^6.0.2", + "@rollup/plugin-typescript": "^12.1.2", + "rollup": "^4.28.1", + "tslib": "^2.8.1", "typescript": "^5.7.2" } }, "js/react_renderer": { "name": "@project-gauntlet/react-renderer", "dependencies": { - "react-reconciler": "^0.29.0" + "react-reconciler": "^0.29.2" }, "devDependencies": { "@project-gauntlet/typings": "*", "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-replace": "^6.0.1", - "@rollup/plugin-typescript": "^11.1.5", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-replace": "^6.0.2", + "@rollup/plugin-typescript": "^12.1.2", "@types/deno": "^2.0.0", - "@types/react": "^18.2.35", - "@types/react-reconciler": "^0.28.6", - "rollup": "^4.27.4", - "tslib": "^2.6.2", + "@types/react": "^18.3.18", + "@types/react-reconciler": "^0.28.9", + "rollup": "^4.28.1", + "tslib": "^2.8.1", "typescript": "^5.7.2" } }, "js/scenario_runner_cli": { "name": "@project-gauntlet/scenario-runner-cli", "dependencies": { - "commander": "^11.1.0" + "commander": "^12.1.0" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.5", - "@types/node": "^18.19.67", - "tslib": "^2.6.2", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-typescript": "^12.1.2", + "@types/node": "^22.10.2", + "tslib": "^2.8.1", "typescript": "^5.7.2" } }, @@ -487,27 +164,45 @@ "name": "@project-gauntlet/typings" }, "node_modules/@actions/core": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", - "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "license": "MIT", "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" } }, "node_modules/@actions/http-client": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.1.tgz", - "integrity": "sha512-KhC/cZsq7f8I4LfZSJKgCvEwfkE8o1538VoBeoGzokVLLnbFDEAdFD3UhoMklxo2un9NJVBdANOresx7vTHlHw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "license": "MIT", "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "license": "MIT" + }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", "engines": { "node": ">=14" } @@ -531,6 +226,7 @@ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", @@ -549,6 +245,7 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -561,96 +258,19 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" }, "node_modules/@js-sdsl/ordered-map": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", "dev": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/js-sdsl" @@ -665,6 +285,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", "dependencies": { "debug": "^4.1.1" } @@ -672,7 +293,8 @@ "node_modules/@kwsites/promise-deferred": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", - "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" }, "node_modules/@mstssk/cleandir": { "version": "2.0.0", @@ -685,405 +307,345 @@ } }, "node_modules/@octokit/app": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.1.0.tgz", - "integrity": "sha512-g3uEsGOQCBl1+W1rgfwoRFUIR6PtvB2T1E4RpygeUU5LrLvlOqcxrt5lfykIeRpUPpupreGJUYl70fqMDXdTpw==", + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-15.1.1.tgz", + "integrity": "sha512-fk8xrCSPTJGpyBdBNI+DcZ224dm0aApv4vi6X7/zTmANXlegKV2Td+dJ+fd7APPaPN7R+xttUsj2Fm+AFDSfMQ==", + "license": "MIT", "dependencies": { - "@octokit/auth-app": "^6.0.0", - "@octokit/auth-unauthenticated": "^5.0.0", - "@octokit/core": "^5.0.0", - "@octokit/oauth-app": "^6.0.0", - "@octokit/plugin-paginate-rest": "^9.0.0", - "@octokit/types": "^12.0.0", - "@octokit/webhooks": "^12.0.4" + "@octokit/auth-app": "^7.0.0", + "@octokit/auth-unauthenticated": "^6.0.0", + "@octokit/core": "^6.1.2", + "@octokit/oauth-app": "^7.0.0", + "@octokit/plugin-paginate-rest": "^11.0.0", + "@octokit/types": "^13.0.0", + "@octokit/webhooks": "^13.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/auth-app": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.1.tgz", - "integrity": "sha512-VrTtzRpyuT5nYGUWeGWQqH//hqEZDV+/yb6+w5wmWpmmUA1Tx950XsAc2mBBfvusfcdF2E7w8jZ1r1WwvfZ9pA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.1.3.tgz", + "integrity": "sha512-GZdkOp2kZTIy5dG9oXqvzUAZiPvDx4C/lMlN6yQjtG9d/+hYa7W8WXTJoOrXE8UdfL9A/sZMl206dmtkl9lwVQ==", + "license": "MIT", "dependencies": { - "@octokit/auth-oauth-app": "^7.1.0", - "@octokit/auth-oauth-user": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/request-error": "^5.1.0", - "@octokit/types": "^13.1.0", - "deprecation": "^2.3.1", - "lru-cache": "^10.0.0", - "universal-github-app-jwt": "^1.1.2", - "universal-user-agent": "^6.0.0" + "@octokit/auth-oauth-app": "^8.1.0", + "@octokit/auth-oauth-user": "^5.1.0", + "@octokit/request": "^9.1.1", + "@octokit/request-error": "^6.1.1", + "@octokit/types": "^13.4.1", + "toad-cache": "^3.7.0", + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" - }, - "node_modules/@octokit/auth-app/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" - } - }, "node_modules/@octokit/auth-oauth-app": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", - "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz", + "integrity": "sha512-5UtmxXAvU2wfcHIPPDWzVSAWXVJzG3NWsxb7zCFplCWEmMCArSZV0UQu5jw5goLQXbFyOr5onzEH37UJB3zQQg==", + "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^6.1.0", - "@octokit/auth-oauth-user": "^4.1.0", - "@octokit/request": "^8.3.1", + "@octokit/auth-oauth-device": "^7.0.0", + "@octokit/auth-oauth-user": "^5.0.1", + "@octokit/request": "^9.0.0", "@octokit/types": "^13.0.0", - "@types/btoa-lite": "^1.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" - } - }, "node_modules/@octokit/auth-oauth-device": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", - "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz", + "integrity": "sha512-HWl8lYueHonuyjrKKIup/1tiy0xcmQCdq5ikvMO1YwkNNkxb6DXfrPjrMYItNLyCP/o2H87WuijuE+SlBTT8eg==", + "license": "MIT", "dependencies": { - "@octokit/oauth-methods": "^4.1.0", - "@octokit/request": "^8.3.1", + "@octokit/oauth-methods": "^5.0.0", + "@octokit/request": "^9.0.0", "@octokit/types": "^13.0.0", - "universal-user-agent": "^6.0.0" + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" - } - }, "node_modules/@octokit/auth-oauth-user": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", - "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.1.tgz", + "integrity": "sha512-rRkMz0ErOppdvEfnemHJXgZ9vTPhBuC6yASeFaB7I2yLMd7QpjfrL1mnvRPlyKo+M6eeLxrKanXJ9Qte29SRsw==", + "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^6.1.0", - "@octokit/oauth-methods": "^4.1.0", - "@octokit/request": "^8.3.1", + "@octokit/auth-oauth-device": "^7.0.1", + "@octokit/oauth-methods": "^5.0.0", + "@octokit/request": "^9.0.1", "@octokit/types": "^13.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" - }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" - } - }, "node_modules/@octokit/auth-token": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", - "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", + "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", + "license": "MIT", "engines": { "node": ">= 18" } }, "node_modules/@octokit/auth-unauthenticated": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", - "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-6.1.0.tgz", + "integrity": "sha512-zPSmfrUAcspZH/lOFQnVnvjQZsIvmfApQH6GzJrkIunDooU1Su2qt2FfMTSVPRp7WLTQyC20Kd55lF+mIYaohQ==", + "license": "MIT", "dependencies": { - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0" + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz", - "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", + "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", + "license": "MIT", "dependencies": { - "@octokit/auth-token": "^4.0.0", - "@octokit/graphql": "^7.0.0", - "@octokit/request": "^8.0.2", - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.0.0", + "@octokit/request": "^9.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/endpoint": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", - "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "license": "MIT", "dependencies": { - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/graphql": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", - "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", + "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", + "license": "MIT", "dependencies": { - "@octokit/request": "^8.0.1", - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/oauth-app": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-6.1.0.tgz", - "integrity": "sha512-nIn/8eUJ/BKUVzxUXd5vpzl1rwaVxMyYbQkNZjHrF7Vk/yu98/YDF/N2KeWO7uZ0g3b5EyiFXFkZI8rJ+DH1/g==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-7.1.3.tgz", + "integrity": "sha512-EHXbOpBkSGVVGF1W+NLMmsnSsJRkcrnVmDKt0TQYRBb6xWfWzoi9sBD4DIqZ8jGhOWO/V8t4fqFyJ4vDQDn9bg==", + "license": "MIT", "dependencies": { - "@octokit/auth-oauth-app": "^7.0.0", - "@octokit/auth-oauth-user": "^4.0.0", - "@octokit/auth-unauthenticated": "^5.0.0", - "@octokit/core": "^5.0.0", - "@octokit/oauth-authorization-url": "^6.0.2", - "@octokit/oauth-methods": "^4.0.0", + "@octokit/auth-oauth-app": "^8.0.0", + "@octokit/auth-oauth-user": "^5.0.1", + "@octokit/auth-unauthenticated": "^6.0.0-beta.1", + "@octokit/core": "^6.0.0", + "@octokit/oauth-authorization-url": "^7.0.0", + "@octokit/oauth-methods": "^5.0.0", "@types/aws-lambda": "^8.10.83", - "universal-user-agent": "^6.0.0" + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/oauth-authorization-url": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", - "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", + "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==", + "license": "MIT", "engines": { "node": ">= 18" } }, "node_modules/@octokit/oauth-methods": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", - "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.2.tgz", + "integrity": "sha512-C5lglRD+sBlbrhCUTxgJAFjWgJlmTx5bQ7Ch0+2uqRjYv7Cfb5xpX4WuSC9UgQna3sqRGBL9EImX9PvTpMaQ7g==", + "license": "MIT", "dependencies": { - "@octokit/oauth-authorization-url": "^6.0.2", - "@octokit/request": "^8.3.1", - "@octokit/request-error": "^5.1.0", - "@octokit/types": "^13.0.0", - "btoa-lite": "^1.0.0" + "@octokit/oauth-authorization-url": "^7.0.0", + "@octokit/request": "^9.1.0", + "@octokit/request-error": "^6.1.0", + "@octokit/types": "^13.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" - } - }, "node_modules/@octokit/openapi-types": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", - "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==" + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", + "license": "MIT" + }, + "node_modules/@octokit/openapi-webhooks-types": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-8.5.1.tgz", + "integrity": "sha512-i3h1b5zpGSB39ffBbYdSGuAd0NhBAwPyA3QV3LYi/lx4lsbZiu7u2UHgXVUR6EpvOI8REOuVh1DZTRfHoJDvuQ==", + "license": "MIT" }, "node_modules/@octokit/plugin-paginate-graphql": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-4.0.1.tgz", - "integrity": "sha512-R8ZQNmrIKKpHWC6V2gum4x9LG2qF1RxRjo27gjQcG3j+vf2tLsEfE7I/wRWEPzYMaenr1M+qDAtNcwZve1ce1A==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-5.2.4.tgz", + "integrity": "sha512-pLZES1jWaOynXKHOqdnwZ5ULeVR6tVVCMm+AUbp0htdcyXDU95WbkYdU4R2ej1wKj5Tu94Mee2Ne0PjPO9cCyA==", + "license": "MIT", "engines": { "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=5" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz", - "integrity": "sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.6.tgz", + "integrity": "sha512-zcvqqf/+TicbTCa/Z+3w4eBJcAxCFymtc0UAIsR3dEVoNilWld4oXdscQ3laXamTszUZdusw97K8+DrbFiOwjw==", + "license": "MIT", "dependencies": { - "@octokit/types": "^12.4.0" + "@octokit/types": "^13.6.2" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=5" + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.6.tgz", + "integrity": "sha512-wMsdyHMjSfKjGINkdGKki06VEkgdEldIGstIEyGX0wbYHGByOwN/KiM+hAAlUwAtPkP3gvXtVQA9L3ITdV2tVw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.6.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-retry": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", - "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-7.1.2.tgz", + "integrity": "sha512-XOWnPpH2kJ5VTwozsxGurw+svB2e61aWlmk5EVIYZPwFK5F9h4cyPyj9CIKRyMXMHSwpIsI3mPOdpMmrRhe7UQ==", + "license": "MIT", "dependencies": { - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", + "@octokit/request-error": "^6.0.0", + "@octokit/types": "^13.0.0", "bottleneck": "^2.15.3" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=5" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-throttling": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", - "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-9.3.2.tgz", + "integrity": "sha512-FqpvcTpIWFpMMwIeSoypoJXysSAQ3R+ALJhXXSG1HTP3YZOIeLmcNcimKaXxTcws+Sh6yoRl13SJ5r8sXc1Fhw==", + "license": "MIT", "dependencies": { - "@octokit/types": "^12.2.0", + "@octokit/types": "^13.0.0", "bottleneck": "^2.15.3" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "@octokit/core": "^5.0.0" + "@octokit/core": "^6.0.0" } }, "node_modules/@octokit/request": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", - "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "license": "MIT", "dependencies": { - "@octokit/endpoint": "^9.0.1", - "@octokit/request-error": "^5.1.0", + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/request-error": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", - "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz", + "integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==", + "license": "MIT", "dependencies": { - "@octokit/types": "^13.1.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@octokit/types": "^13.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" - }, - "node_modules/@octokit/request-error/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" - } - }, - "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" - }, - "node_modules/@octokit/request/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" - } - }, "node_modules/@octokit/types": { - "version": "12.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.5.0.tgz", - "integrity": "sha512-YJEKcb0KkJlIUNU/zjnZwHEP8AoVh/OoIcP/1IyR4UHxExz7fzpe/a8IG4wBtQi7QDEqiomVLX88S6FpxxAJtg==", + "version": "13.6.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.2.tgz", + "integrity": "sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==", + "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^19.1.0" + "@octokit/openapi-types": "^22.2.0" } }, "node_modules/@octokit/webhooks": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.2.0.tgz", - "integrity": "sha512-CyuLJ0/P7bKZ+kIYw+fnkeVdhUzNuDKgNSI7pU/m7Nod0T7kP+s4s2f0pNmG9HL8/RZN1S0ZWTDll3VTMrFLAw==", + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-13.4.1.tgz", + "integrity": "sha512-I5YPUtfWidh+OzyrlDahJsUpkpGK0kCTmDRbuqGmlCUzOtxdEkX3R4d6Cd08ijQYwkVXQJanPdbKuZBeV2NMaA==", + "license": "MIT", "dependencies": { - "@octokit/request-error": "^5.0.0", - "@octokit/webhooks-methods": "^4.1.0", - "@octokit/webhooks-types": "7.4.0", - "aggregate-error": "^3.1.0" + "@octokit/openapi-webhooks-types": "8.5.1", + "@octokit/request-error": "^6.0.1", + "@octokit/webhooks-methods": "^5.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/webhooks-methods": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-4.1.0.tgz", - "integrity": "sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-5.1.0.tgz", + "integrity": "sha512-yFZa3UH11VIxYnnoOYCVoJ3q4ChuSOk2IVBBQ0O3xtKX4x9bmKb/1t+Mxixv2iUhzMdOl1qeWJqEhouXXzB3rQ==", + "license": "MIT", "engines": { "node": ">= 18" } }, - "node_modules/@octokit/webhooks-types": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.4.0.tgz", - "integrity": "sha512-FE2V+QZ2UYlh+9wWd5BPLNXG+J/XUD/PPq0ovS+nCcGX4+3qVbi3jYOmCTW48hg9SBBLtInx9+o7fFt4H5iP0Q==" - }, "node_modules/@project-gauntlet/api": { "resolved": "js/api", "link": true @@ -1152,6 +714,34 @@ "resolved": "js/scenario_runner_cli", "link": true }, + "node_modules/@project-gauntlet/tools": { + "version": "0.9.0", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", + "dev": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.5", + "@grpc/proto-loader": "^0.7.13", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "chalk": "^5.4.0", + "commander": "^12.1.0", + "rollup": "^4.28.1", + "rollup-plugin-cleandir": "^3.0.0", + "rollup-plugin-typescript2": "^0.36.0", + "simple-git": "^3.27.0", + "tail": "^2.2.6", + "toml": "^3.0.0", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0" + }, + "bin": { + "gauntlet": "bin/main.js" + } + }, "node_modules/@project-gauntlet/typings": { "resolved": "js/typings", "link": true @@ -1160,31 +750,36 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -1194,37 +789,43 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@rollup/plugin-alias": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz", "integrity": "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" }, @@ -1238,20 +839,22 @@ } }, "node_modules/@rollup/plugin-commonjs": { - "version": "25.0.7", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz", - "integrity": "sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==", + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", + "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", "dev": true, + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", - "glob": "^8.0.3", + "fdir": "^6.2.0", "is-reference": "1.2.1", - "magic-string": "^0.30.3" + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0 || 14 >= 14.17" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" @@ -1267,6 +870,7 @@ "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", "dev": true, + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.1.0" }, @@ -1283,15 +887,15 @@ } }, "node_modules/@rollup/plugin-node-resolve": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", - "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", "dev": true, + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.1", "is-module": "^1.0.0", "resolve": "^1.22.1" }, @@ -1308,10 +912,11 @@ } }, "node_modules/@rollup/plugin-replace": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", - "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.2.tgz", + "integrity": "sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ==", "dev": true, + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", "magic-string": "^0.30.3" @@ -1329,10 +934,11 @@ } }, "node_modules/@rollup/plugin-typescript": { - "version": "11.1.6", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz", - "integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==", + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.2.tgz", + "integrity": "sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg==", "dev": true, + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" @@ -1355,14 +961,15 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", - "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" + "picomatch": "^4.0.2" }, "engines": { "node": ">=14.0.0" @@ -1663,34 +1270,31 @@ }, "node_modules/@std/async": { "name": "@jsr/std__async", - "version": "1.0.8", - "resolved": "https://npm.jsr.io/~/11/@jsr/std__async/1.0.8.tgz", - "integrity": "sha512-veKSzQ5lY6uJL60IxPFD2P1uVssdOtb8LmApllO2HzwzgY3ZDsjD4ZIaDjUykXwBNfUY2tHAlE7d62OGsbzdAg==" + "version": "1.0.9", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__async/1.0.9.tgz", + "integrity": "sha512-gJgMcOx2DqpWeiFKnLIli4FcjjXlkJlMyPtHF+r7bGXI3FQPXEIpy8Z8A7/o3hjMyh8vSDdPijPpiLfPU91iUg==" }, "node_modules/@std/fs": { "name": "@jsr/std__fs", - "version": "1.0.5", - "resolved": "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.5.tgz", - "integrity": "sha512-2ihx5BjO2IxpJ1aHy+joER4l1tJSktyaNaoDb9HOVK5IRToUY5OwstLe3+yhZnSn2KXlCo5DBS1mfAgrtu10aw==", + "version": "1.0.8", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.8.tgz", + "integrity": "sha512-w2BNxizm0BLiNNYRM+flC1Y8r6aqaol1supvGvDtAPMI7x5CBBoHejpsVfWT9if+VGLNAMMq5g4syzWumd4JQg==", "dependencies": { - "@jsr/std__path": "^1.0.7" + "@jsr/std__path": "^1.0.8" } }, "node_modules/@types/aws-lambda": { - "version": "8.10.138", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.138.tgz", - "integrity": "sha512-71EHMl70TPWIAsFuHd85NHq6S6T2OOjiisPTrH7RgcjzpJpPh4RQJv7PvVvIxc6PIp8CLV7F9B+TdjcAES5vcA==" - }, - "node_modules/@types/btoa-lite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", - "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==" + "version": "8.10.146", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.146.tgz", + "integrity": "sha512-3BaDXYTh0e6UCJYL/jwV/3+GRslSc08toAiZSmleYtkAUyV5rtvdPYxrG/88uqvTuT6sb27WE9OS90ZNTIuQ0g==", + "license": "MIT" }, "node_modules/@types/cross-spawn": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -1706,52 +1310,50 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", - "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", - "dependencies": { - "@types/node": "*" - } + "dev": true, + "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.7", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", - "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==" + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.67", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", - "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/react": { - "version": "18.2.56", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.56.tgz", - "integrity": "sha512-NpwHDMkS/EFZF2dONFQHgkPRwhvgq/OAvIaGQzxGSBmaeR++kTg6njr15Vatz0/2VcCEwJQFi6Jf4Q0qBu0rLA==", + "version": "18.3.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", + "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-reconciler": { - "version": "0.28.8", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.8.tgz", - "integrity": "sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==", + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", "dev": true, - "dependencies": { + "license": "MIT", + "peerDependencies": { "@types/react": "*" } }, @@ -1759,45 +1361,30 @@ "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true - }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "dev": true, + "license": "MIT" }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -1807,49 +1394,31 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "license": "Apache-2.0" }, "node_modules/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", - "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "license": "MIT" }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/btoa-lite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", - "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==" - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/chalk": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.0.tgz", @@ -1863,19 +1432,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "engines": { - "node": ">=6" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1885,11 +1447,91 @@ "node": ">=12" } }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1901,26 +1543,30 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1934,14 +1580,16 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1957,40 +1605,31 @@ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } + "dev": true, + "license": "MIT" }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1999,13 +1638,30 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dev": true, + "license": "MIT", "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", @@ -2023,6 +1679,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -2032,10 +1689,11 @@ } }, "node_modules/foreground-child": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", - "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, + "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -2052,6 +1710,7 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2061,18 +1720,13 @@ "node": ">=12" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2087,6 +1741,7 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2096,24 +1751,30 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", "dev": true, + "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=12" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2123,13 +1784,15 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -2137,52 +1800,17 @@ "node": ">= 0.4" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", - "dev": true, - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", + "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2193,6 +1821,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2201,13 +1830,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "*" } @@ -2215,18 +1846,37 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -2234,51 +1884,12 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -2289,59 +1900,28 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + "dev": true, + "license": "MIT" }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -2350,23 +1930,23 @@ } }, "node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true, + "license": "ISC", "engines": { - "node": "14 || >=16.14" + "node": "20 || >=22" } }, "node_modules/magic-string": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", - "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/make-dir": { @@ -2374,6 +1954,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^6.0.0" }, @@ -2389,20 +1970,25 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { @@ -2410,89 +1996,44 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/octokit": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/octokit/-/octokit-3.2.1.tgz", - "integrity": "sha512-u+XuSejhe3NdIvty3Jod00JvTdAE/0/+XbhIDhefHbu+2OcTRHd80aCiH6TX19ZybJmwPQBKFQmHGxp0i9mJrg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-4.0.2.tgz", + "integrity": "sha512-wbqF4uc1YbcldtiBFfkSnquHtECEIpYD78YUXI6ri1Im5OO2NLo6ZVpRdbJpdnpZ05zMrVPssNiEo6JQtea+Qg==", + "license": "MIT", "dependencies": { - "@octokit/app": "^14.0.2", - "@octokit/core": "^5.0.0", - "@octokit/oauth-app": "^6.0.0", - "@octokit/plugin-paginate-graphql": "^4.0.0", - "@octokit/plugin-paginate-rest": "11.3.1", - "@octokit/plugin-rest-endpoint-methods": "13.2.2", - "@octokit/plugin-retry": "^6.0.0", - "@octokit/plugin-throttling": "^8.0.0", - "@octokit/request-error": "^5.0.0", + "@octokit/app": "^15.0.0", + "@octokit/core": "^6.0.0", + "@octokit/oauth-app": "^7.0.0", + "@octokit/plugin-paginate-graphql": "^5.0.0", + "@octokit/plugin-paginate-rest": "^11.0.0", + "@octokit/plugin-rest-endpoint-methods": "^13.0.0", + "@octokit/plugin-retry": "^7.0.0", + "@octokit/plugin-throttling": "^9.0.0", + "@octokit/request-error": "^6.0.0", "@octokit/types": "^13.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/octokit/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" - }, - "node_modules/octokit/node_modules/@octokit/plugin-paginate-rest": { - "version": "11.3.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.1.tgz", - "integrity": "sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==", - "dependencies": { - "@octokit/types": "^13.5.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": "5" - } - }, - "node_modules/octokit/node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.2.tgz", - "integrity": "sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==", - "dependencies": { - "@octokit/types": "^13.5.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": "^5" - } - }, - "node_modules/octokit/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -2508,6 +2049,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -2520,21 +2062,24 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2543,6 +2088,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", "engines": { "node": ">=8" } @@ -2551,15 +2097,34 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -2570,6 +2135,7 @@ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -2578,11 +2144,12 @@ } }, "node_modules/protobufjs": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", - "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", "dev": true, "hasInstallScript": true, + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -2602,9 +2169,10 @@ } }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -2613,18 +2181,19 @@ } }, "node_modules/react-reconciler": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.0.tgz", - "integrity": "sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==", + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "engines": { "node": ">=0.10.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/require-directory": { @@ -2632,23 +2201,28 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2710,6 +2284,7 @@ "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.36.0.tgz", "integrity": "sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw==", "dev": true, + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^4.1.2", "find-cache-dir": "^3.3.2", @@ -2727,6 +2302,7 @@ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", "dev": true, + "license": "MIT", "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" @@ -2735,37 +2311,34 @@ "node": ">= 8.0.0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "node_modules/rollup-plugin-typescript2/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2777,6 +2350,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2788,6 +2362,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", "engines": { "node": ">=8" } @@ -2797,6 +2372,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -2820,17 +2396,21 @@ } }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string-width-cjs": { @@ -2839,6 +2419,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2848,11 +2429,29 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2860,12 +2459,29 @@ "node": ">=8" } }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2873,11 +2489,22 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2890,15 +2517,26 @@ "resolved": "https://registry.npmjs.org/tail/-/tail-2.2.6.tgz", "integrity": "sha512-IQ6G4wK/t8VBauYiGPLx+d3fA5XjSVagjWV5SIYzvEvglbQjwEcukeYI68JOPpdydjxhZ9sIgzRlSmwSpphHyw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6.0.0" } }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/toml": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tslib": { "version": "2.8.1", @@ -2911,6 +2549,7 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", "engines": { "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } @@ -2920,6 +2559,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2932,6 +2572,7 @@ "version": "5.28.4", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -2940,45 +2581,39 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/universal-github-app-jwt": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.1.2.tgz", - "integrity": "sha512-t1iB2FmLFE+yyJY9+3wMx0ejB+MQpEVkH0gQv7dR6FZyltyq+ZZO0uDpbopxhrZ3SLEO4dCEkIujOMldEQ2iOA==", - "dependencies": { - "@types/jsonwebtoken": "^9.0.0", - "jsonwebtoken": "^9.0.2" - } + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.0.tgz", + "integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==", + "license": "MIT" }, "node_modules/universal-user-agent": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", + "license": "ISC" }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -2990,17 +2625,18 @@ } }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -3012,6 +2648,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -3024,16 +2661,73 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -3043,6 +2737,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -3061,10 +2756,56 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zod": { "version": "3.24.1", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", @@ -3089,991 +2830,117 @@ } }, "scenarios/js/api": {}, - "scenarios/js/deno": { - "extraneous": true - }, "scenarios/plugins/docs_detail": { "name": "@project-gauntlet/docs-detailt", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "@types/deno": "^2.0.0", - "@types/react": "^18.2.14", - "typescript": "^5.7.2" + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" } }, "scenarios/plugins/docs_detail/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_detail/node_modules/@project-gauntlet/tools": { - "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", - "dev": true, - "dependencies": { - "@grpc/grpc-js": "^1.12.5", - "@grpc/proto-loader": "^0.7.13", - "@rollup/plugin-commonjs": "^28.0.2", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "chalk": "^5.4.0", - "commander": "^12.1.0", - "rollup": "^4.28.1", - "rollup-plugin-cleandir": "^3.0.0", - "rollup-plugin-typescript2": "^0.36.0", - "simple-git": "^3.27.0", - "tail": "^2.2.6", - "toml": "^3.0.0", - "tslib": "^2.8.1", - "typescript": "^5.7.2", - "zod": "^3.24.1", - "zod-validation-error": "^3.4.0" - }, - "bin": { - "gauntlet": "bin/main.js" - } - }, - "scenarios/plugins/docs_detail/node_modules/@rollup/plugin-commonjs": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", - "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_detail/node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_detail/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "scenarios/plugins/docs_detail/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "scenarios/plugins/docs_detail/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "scenarios/plugins/docs_form": { "name": "@project-gauntlet/docs-form", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "@types/deno": "^2.0.0", - "@types/react": "^18.2.14", - "typescript": "^5.7.2" + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" } }, "scenarios/plugins/docs_form/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_form/node_modules/@project-gauntlet/tools": { - "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", - "dev": true, - "dependencies": { - "@grpc/grpc-js": "^1.12.5", - "@grpc/proto-loader": "^0.7.13", - "@rollup/plugin-commonjs": "^28.0.2", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "chalk": "^5.4.0", - "commander": "^12.1.0", - "rollup": "^4.28.1", - "rollup-plugin-cleandir": "^3.0.0", - "rollup-plugin-typescript2": "^0.36.0", - "simple-git": "^3.27.0", - "tail": "^2.2.6", - "toml": "^3.0.0", - "tslib": "^2.8.1", - "typescript": "^5.7.2", - "zod": "^3.24.1", - "zod-validation-error": "^3.4.0" - }, - "bin": { - "gauntlet": "bin/main.js" - } - }, - "scenarios/plugins/docs_form/node_modules/@rollup/plugin-commonjs": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", - "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_form/node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_form/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "scenarios/plugins/docs_form/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "scenarios/plugins/docs_form/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "scenarios/plugins/docs_grid": { "name": "@project-gauntlet/docs-grid", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "@types/deno": "^2.0.0", - "@types/react": "^18.2.14", - "typescript": "^5.7.2" + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" } }, "scenarios/plugins/docs_grid/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_grid/node_modules/@project-gauntlet/tools": { - "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", - "dev": true, - "dependencies": { - "@grpc/grpc-js": "^1.12.5", - "@grpc/proto-loader": "^0.7.13", - "@rollup/plugin-commonjs": "^28.0.2", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "chalk": "^5.4.0", - "commander": "^12.1.0", - "rollup": "^4.28.1", - "rollup-plugin-cleandir": "^3.0.0", - "rollup-plugin-typescript2": "^0.36.0", - "simple-git": "^3.27.0", - "tail": "^2.2.6", - "toml": "^3.0.0", - "tslib": "^2.8.1", - "typescript": "^5.7.2", - "zod": "^3.24.1", - "zod-validation-error": "^3.4.0" - }, - "bin": { - "gauntlet": "bin/main.js" - } - }, - "scenarios/plugins/docs_grid/node_modules/@rollup/plugin-commonjs": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", - "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_grid/node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_grid/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "scenarios/plugins/docs_grid/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "scenarios/plugins/docs_grid/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "scenarios/plugins/docs_inline": { - "name": "@project-gauntlet/docs-inline", - "extraneous": true, - "dependencies": { - "@project-gauntlet/api": "file:../../js/api" - }, - "devDependencies": { - "@project-gauntlet/tools": "file:../../../tools", - "@types/deno": "^2.0.0", - "@types/react": "^18.2.14", - "typescript": "^5.3.3" - } - }, "scenarios/plugins/docs_inline_separators": { "name": "@project-gauntlet/docs-inline-separators", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "@types/deno": "^2.0.0", - "@types/react": "^18.2.14", - "typescript": "^5.7.2" + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" } }, "scenarios/plugins/docs_inline_separators/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_inline_separators/node_modules/@project-gauntlet/tools": { - "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", - "dev": true, - "dependencies": { - "@grpc/grpc-js": "^1.12.5", - "@grpc/proto-loader": "^0.7.13", - "@rollup/plugin-commonjs": "^28.0.2", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "chalk": "^5.4.0", - "commander": "^12.1.0", - "rollup": "^4.28.1", - "rollup-plugin-cleandir": "^3.0.0", - "rollup-plugin-typescript2": "^0.36.0", - "simple-git": "^3.27.0", - "tail": "^2.2.6", - "toml": "^3.0.0", - "tslib": "^2.8.1", - "typescript": "^5.7.2", - "zod": "^3.24.1", - "zod-validation-error": "^3.4.0" - }, - "bin": { - "gauntlet": "bin/main.js" - } - }, - "scenarios/plugins/docs_inline_separators/node_modules/@rollup/plugin-commonjs": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", - "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_inline_separators/node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_inline_separators/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "scenarios/plugins/docs_inline_separators/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "scenarios/plugins/docs_inline_separators/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "scenarios/plugins/docs_inline_three_sections": { "name": "@project-gauntlet/docs-inline-three-sections", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "@types/deno": "^2.0.0", - "@types/react": "^18.2.14", - "typescript": "^5.7.2" + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" } }, "scenarios/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/tools": { - "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", - "dev": true, - "dependencies": { - "@grpc/grpc-js": "^1.12.5", - "@grpc/proto-loader": "^0.7.13", - "@rollup/plugin-commonjs": "^28.0.2", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "chalk": "^5.4.0", - "commander": "^12.1.0", - "rollup": "^4.28.1", - "rollup-plugin-cleandir": "^3.0.0", - "rollup-plugin-typescript2": "^0.36.0", - "simple-git": "^3.27.0", - "tail": "^2.2.6", - "toml": "^3.0.0", - "tslib": "^2.8.1", - "typescript": "^5.7.2", - "zod": "^3.24.1", - "zod-validation-error": "^3.4.0" - }, - "bin": { - "gauntlet": "bin/main.js" - } - }, - "scenarios/plugins/docs_inline_three_sections/node_modules/@rollup/plugin-commonjs": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", - "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_inline_three_sections/node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_inline_three_sections/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "scenarios/plugins/docs_inline_three_sections/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "scenarios/plugins/docs_inline_three_sections/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "scenarios/plugins/docs_inline_two_sections": { "name": "@project-gauntlet/docs-inline-two-sections", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "@types/deno": "^2.0.0", - "@types/react": "^18.2.14", - "typescript": "^5.7.2" + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" } }, "scenarios/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true }, - "scenarios/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/tools": { - "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", - "dev": true, - "dependencies": { - "@grpc/grpc-js": "^1.12.5", - "@grpc/proto-loader": "^0.7.13", - "@rollup/plugin-commonjs": "^28.0.2", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "chalk": "^5.4.0", - "commander": "^12.1.0", - "rollup": "^4.28.1", - "rollup-plugin-cleandir": "^3.0.0", - "rollup-plugin-typescript2": "^0.36.0", - "simple-git": "^3.27.0", - "tail": "^2.2.6", - "toml": "^3.0.0", - "tslib": "^2.8.1", - "typescript": "^5.7.2", - "zod": "^3.24.1", - "zod-validation-error": "^3.4.0" - }, - "bin": { - "gauntlet": "bin/main.js" - } - }, - "scenarios/plugins/docs_inline_two_sections/node_modules/@rollup/plugin-commonjs": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", - "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_inline_two_sections/node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_inline_two_sections/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "scenarios/plugins/docs_inline_two_sections/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "scenarios/plugins/docs_inline_two_sections/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "scenarios/plugins/docs_list": { "name": "@project-gauntlet/docs-list", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "@types/deno": "^2.0.0", - "@types/react": "^18.2.14", - "typescript": "^5.7.2" + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" } }, "scenarios/plugins/docs_list/node_modules/@project-gauntlet/api": { "resolved": "scenarios/js/api", "link": true - }, - "scenarios/plugins/docs_list/node_modules/@project-gauntlet/tools": { - "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", - "dev": true, - "dependencies": { - "@grpc/grpc-js": "^1.12.5", - "@grpc/proto-loader": "^0.7.13", - "@rollup/plugin-commonjs": "^28.0.2", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "chalk": "^5.4.0", - "commander": "^12.1.0", - "rollup": "^4.28.1", - "rollup-plugin-cleandir": "^3.0.0", - "rollup-plugin-typescript2": "^0.36.0", - "simple-git": "^3.27.0", - "tail": "^2.2.6", - "toml": "^3.0.0", - "tslib": "^2.8.1", - "typescript": "^5.7.2", - "zod": "^3.24.1", - "zod-validation-error": "^3.4.0" - }, - "bin": { - "gauntlet": "bin/main.js" - } - }, - "scenarios/plugins/docs_list/node_modules/@rollup/plugin-commonjs": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", - "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_list/node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "scenarios/plugins/docs_list/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "scenarios/plugins/docs_list/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "scenarios/plugins/docs_list/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "tools": { - "name": "@project-gauntlet/tools", - "version": "0.9.0", - "extraneous": true, - "dependencies": { - "@grpc/grpc-js": "^1.11.1", - "@grpc/proto-loader": "^0.7.13", - "@rollup/plugin-commonjs": "^26.0.1", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^15.2.3", - "chalk": "^5.3.0", - "commander": "^12.1.0", - "rollup": "^4.27.4", - "rollup-plugin-cleandir": "^2.0.0", - "rollup-plugin-typescript2": "^0.36.0", - "simple-git": "^3.25.0", - "tail": "^2.2.6", - "toml": "^3.0.0", - "tslib": "^2.6.3", - "typescript": "^5.2.2", - "zod": "^3.23.8", - "zod-validation-error": "^3.3.0" - }, - "bin": { - "gauntlet": "bin/main.js" - }, - "devDependencies": { - "@types/node": "^18.19.67", - "@types/tail": "^2.2.3", - "workspace-root": "^3.2.0" - } } } } diff --git a/scenarios/plugins/docs_detail/package.json b/scenarios/plugins/docs_detail/package.json index 1ebfe64..694c863 100644 --- a/scenarios/plugins/docs_detail/package.json +++ b/scenarios/plugins/docs_detail/package.json @@ -9,9 +9,9 @@ "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@types/react": "^18.2.14", - "@types/deno": "^2.0.0", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "typescript": "^5.7.2" + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" } } diff --git a/scenarios/plugins/docs_form/package.json b/scenarios/plugins/docs_form/package.json index 0972129..f4e8854 100644 --- a/scenarios/plugins/docs_form/package.json +++ b/scenarios/plugins/docs_form/package.json @@ -9,9 +9,9 @@ "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@types/react": "^18.2.14", - "@types/deno": "^2.0.0", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "typescript": "^5.7.2" + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" } } diff --git a/scenarios/plugins/docs_grid/package.json b/scenarios/plugins/docs_grid/package.json index 7d631b3..0602364 100644 --- a/scenarios/plugins/docs_grid/package.json +++ b/scenarios/plugins/docs_grid/package.json @@ -9,9 +9,9 @@ "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@types/react": "^18.2.14", - "@types/deno": "^2.0.0", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "typescript": "^5.7.2" + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" } } diff --git a/scenarios/plugins/docs_inline_separators/package.json b/scenarios/plugins/docs_inline_separators/package.json index 9715fcb..7049d90 100644 --- a/scenarios/plugins/docs_inline_separators/package.json +++ b/scenarios/plugins/docs_inline_separators/package.json @@ -9,9 +9,9 @@ "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@types/react": "^18.2.14", - "@types/deno": "^2.0.0", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "typescript": "^5.7.2" + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" } } diff --git a/scenarios/plugins/docs_inline_three_sections/package.json b/scenarios/plugins/docs_inline_three_sections/package.json index 29a5f6d..c1d6262 100644 --- a/scenarios/plugins/docs_inline_three_sections/package.json +++ b/scenarios/plugins/docs_inline_three_sections/package.json @@ -9,9 +9,9 @@ "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@types/react": "^18.2.14", - "@types/deno": "^2.0.0", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "typescript": "^5.7.2" + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" } } diff --git a/scenarios/plugins/docs_inline_two_sections/package.json b/scenarios/plugins/docs_inline_two_sections/package.json index bf55c1c..4ba2946 100644 --- a/scenarios/plugins/docs_inline_two_sections/package.json +++ b/scenarios/plugins/docs_inline_two_sections/package.json @@ -9,9 +9,9 @@ "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@types/react": "^18.2.14", - "@types/deno": "^2.0.0", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "typescript": "^5.7.2" + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" } } diff --git a/scenarios/plugins/docs_list/package.json b/scenarios/plugins/docs_list/package.json index cc87c1f..688f432 100644 --- a/scenarios/plugins/docs_list/package.json +++ b/scenarios/plugins/docs_list/package.json @@ -9,9 +9,9 @@ "@project-gauntlet/api": "file:../../js/api" }, "devDependencies": { - "@types/react": "^18.2.14", - "@types/deno": "^2.0.0", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", - "typescript": "^5.7.2" + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" } } From b68536103336d3411b76f8c15cab05fa9b6d9c6c Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:49:19 +0100 Subject: [PATCH 224/540] Hide window when plugin action is executed --- rust/client/src/ui/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index d9879de..ee99dad 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -570,7 +570,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { widget_id, }; - Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + Task::batch([ + state.hide_window(), + Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + ]) } AppMsg::RunSearchItemAction(search_result, action_index) => { match search_result.entrypoint_type { From c233b3ca0087dc2f87c56875a23e61cdf1b91371 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:22:42 +0100 Subject: [PATCH 225/540] Fix separator in inline view not being horizontally centered --- rust/client/src/ui/widget.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index c578a99..faa6b21 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1381,14 +1381,13 @@ impl<'b> ComponentWidgets<'b> { .iter() .map(|members| { match members { - InlineWidgetOrderedMembers::Content(widget) => self.render_content_widget(widget, true), - InlineWidgetOrderedMembers::InlineSeparator(widget) => { - let element = self.render_inline_separator_widget(widget); + InlineWidgetOrderedMembers::Content(widget) => { + let element = self.render_content_widget(widget, true); container(element) - .width(Length::Fill) .into() - } + }, + InlineWidgetOrderedMembers::InlineSeparator(widget) => self.render_inline_separator_widget(widget) } }) .collect(); From 5bb92eea8ab60649917a200caf53640dff16051e Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:55:41 +0100 Subject: [PATCH 226/540] Fix hud window not appearing on wayland --- rust/client/src/ui/hud/mod.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rust/client/src/ui/hud/mod.rs b/rust/client/src/ui/hud/mod.rs index edb27e4..520b225 100644 --- a/rust/client/src/ui/hud/mod.rs +++ b/rust/client/src/ui/hud/mod.rs @@ -53,9 +53,11 @@ fn open_wayland() -> Task { let id = window::Id::unique(); let settings = layer_shell_settings(); - Task::done(AppMsg::LayerShell(crate::ui::layer_shell::LayerShellAppMsg::NewLayerShell { id, settings })) - .then(move |_| sleep_for_2_seconds(id)) - .then(|id| Task::done(AppMsg::LayerShell(crate::ui::layer_shell::LayerShellAppMsg::RemoveWindow(id)))) + Task::batch([ + Task::done(AppMsg::LayerShell(crate::ui::layer_shell::LayerShellAppMsg::NewLayerShell { id, settings })), + sleep_for_2_seconds(id) + .then(|id| Task::done(AppMsg::LayerShell(crate::ui::layer_shell::LayerShellAppMsg::RemoveWindow(id)))) + ]) } #[cfg(target_os = "linux")] From 42f3bb4d7d4d476c90a5ff0f1c4f6b9b4ae51ead Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 20 Dec 2024 21:18:16 +0100 Subject: [PATCH 227/540] Use ArrayBuffer in Clipboard Api instead of Blob --- dev_plugin/src/detail-view.tsx | 5 +++-- js/api/src/helpers.ts | 28 ++++++++++++++++++---------- js/typings/index.d.ts | 2 +- rust/plugin_runtime/src/clipboard.rs | 2 +- rust/server/src/plugins/clipboard.rs | 6 +----- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/dev_plugin/src/detail-view.tsx b/dev_plugin/src/detail-view.tsx index 97d63a4..fc483f9 100644 --- a/dev_plugin/src/detail-view.tsx +++ b/dev_plugin/src/detail-view.tsx @@ -4,9 +4,10 @@ import { Action, ActionPanel, Detail, Icons } from "@project-gauntlet/api/compon import { useNavigation } from "@project-gauntlet/api/hooks"; import { Clipboard, entrypointPreferences, pluginPreferences } from "@project-gauntlet/api/helpers"; -async function readFile(url: string): Promise { +async function readFile(url: string): Promise { const res = await fetch(url); - return await res.blob(); + const blob = await res.blob(); + return await blob.arrayBuffer(); } interface DetailViewEntrypointConfig { diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index 3e34339..4db3d2b 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -51,17 +51,17 @@ export type GeneratorProps = { }; export const Clipboard: Clipboard = { - read: async function (): Promise<{ "text/plain"?: string | undefined; "image/png"?: Blob | undefined; }> { + read: async function (): Promise<{ "text/plain"?: string | undefined; "image/png"?: ArrayBuffer | undefined; }> { const data = await clipboard_read(); - const result: { "text/plain"?: string; "image/png"?: Blob; } = {}; + const result: { "text/plain"?: string; "image/png"?: ArrayBuffer; } = {}; if (data.text_data) { result["text/plain"] = data.text_data; } if (data.png_data) { - result["image/png"] = data.png_data; // TODO arraybuffer? fix when migrating to deno's op2 + result["image/png"] = new Uint8Array(data.png_data).buffer; } return result @@ -69,13 +69,21 @@ export const Clipboard: Clipboard = { readText: async function (): Promise { return await clipboard_read_text() }, - write: async function (data: { "text/plain"?: string | undefined; "image/png"?: Blob | undefined; }): Promise { + write: async function (data: { "text/plain"?: string | undefined; "image/png"?: ArrayBuffer | undefined; }): Promise { const text_data = data["text/plain"]; const png_data = data["image/png"]; - return await clipboard_write({ - text_data: text_data, - png_data: png_data != undefined ? Array.from(new Uint8Array(png_data as any)) : undefined, // TODO arraybuffer? fix when migrating to deno's op2 - }) + + const write_data: { text_data?: string, png_data?: number[] } = {}; + + if (text_data) { + write_data.text_data = text_data; + } + + if (png_data) { + write_data.png_data = Array.from(new Uint8Array(png_data)); + } + + return await clipboard_write(write_data) }, writeText: async function (data: string): Promise { return await clipboard_write_text(data) @@ -86,9 +94,9 @@ export const Clipboard: Clipboard = { } export interface Clipboard { - read(): Promise<{ ["text/plain"]?: string, ["image/png"]?: Blob }>; + read(): Promise<{ ["text/plain"]?: string, ["image/png"]?: ArrayBuffer }>; readText(): Promise; - write(data: { ["text/plain"]?: string, ["image/png"]?: Blob }): Promise; + write(data: { ["text/plain"]?: string, ["image/png"]?: ArrayBuffer }): Promise; writeText(data: string): Promise; clear(): Promise; } diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 12db725..7b664b0 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -206,7 +206,7 @@ declare module "ext:core/ops" { function fetch_action_id_for_shortcut(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean): Promise; - function clipboard_read(): Promise<{ text_data?: string, png_data?: Blob }>; + function clipboard_read(): Promise<{ text_data?: string, png_data?: number[] }>; function clipboard_read_text(): Promise; function clipboard_write(data: { text_data?: string, png_data?: number[] }): Promise; function clipboard_write_text(data: string): Promise; diff --git a/rust/plugin_runtime/src/clipboard.rs b/rust/plugin_runtime/src/clipboard.rs index 574236e..25995c4 100644 --- a/rust/plugin_runtime/src/clipboard.rs +++ b/rust/plugin_runtime/src/clipboard.rs @@ -50,7 +50,7 @@ pub async fn clipboard_read_text(state: Rc>) -> anyhow::Result< } #[op2(async)] -pub async fn clipboard_write(state: Rc>, #[serde] data: JSClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2 +pub async fn clipboard_write(state: Rc>, #[serde] data: JSClipboardData) -> anyhow::Result<()> { let api = { let state = state.borrow(); diff --git a/rust/server/src/plugins/clipboard.rs b/rust/server/src/plugins/clipboard.rs index 691ea55..e8ec76a 100644 --- a/rust/server/src/plugins/clipboard.rs +++ b/rust/server/src/plugins/clipboard.rs @@ -135,11 +135,7 @@ impl Clipboard { } fn unknown_err_clipboard(err: arboard::Error) -> Error { - anyhow!("UNKNOWN_ERROR: {:?}", err) -} - -fn unknown_err_image(err: image::ImageError) -> Error { - anyhow!("UNKNOWN_ERROR: {:?}", err) + anyhow!("UNKNOWN_ERROR: {}", err) } fn unable_to_convert_image_err() -> Error { From f15e7c61cea898f33d408c8ec116e199068fc8b1 Mon Sep 17 00:00:00 2001 From: Tristan Schrader Date: Fri, 20 Dec 2024 14:47:14 -0800 Subject: [PATCH 228/540] feat: Nix package + devShell & NixOS + Home-Manager modules (#27) We use crane to provide incremental caching to avoid unnecessary rebuilds. Because @project-gauntlet/tools is a git:// dependency in NPM, we must use fetchNpmDeps with a hash unique to package-lock.json. librusty_v8 builds quite slowly, so we fetch the binary bindings from the project's GitHub release and provide a .#fetch-rusty-v8-hashes package to get the correct hashes when v8 updates. The modules are quite basic for now, but once declarative configuration is supported in Gauntlet, these can be built out more. Also, we had to include a patch in Cargo.toml for libffi-sys because its crates.io release is outdated and breaks the nix sandbox. --- .envrc | 1 + .gitignore | 2 + Cargo.lock | 3 +- Cargo.toml | 4 ++ README.md | 10 +++- default.nix | 13 +++++ flake.lock | 107 ++++++++++++++++++++++++++++++++++++ flake.nix | 12 +++++ nix/README.md | 64 ++++++++++++++++++++++ nix/default.nix | 31 +++++++++++ nix/modules/common.nix | 12 +++++ nix/modules/home.nix | 35 ++++++++++++ nix/modules/nixos.nix | 15 ++++++ nix/overlay.nix | 119 +++++++++++++++++++++++++++++++++++++++++ 14 files changed, 424 insertions(+), 4 deletions(-) create mode 100644 .envrc create mode 100644 default.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/README.md create mode 100644 nix/default.nix create mode 100644 nix/modules/common.nix create mode 100644 nix/modules/home.nix create mode 100644 nix/modules/nixos.nix create mode 100644 nix/overlay.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 668d4bf..ebe541e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea /target /node_modules +/.direnv +result diff --git a/Cargo.lock b/Cargo.lock index e3d6917..219432d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6020,8 +6020,7 @@ dependencies = [ [[package]] name = "libffi-sys" version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36115160c57e8529781b4183c2bb51fdc1f6d6d1ed345591d84be7703befb3c" +source = "git+https://github.com/tov/libffi-rs?rev=d0704d634b6f3ffef5b6fc7e07fe965a1cff5c7b#d0704d634b6f3ffef5b6fc7e07fe965a1cff5c7b" dependencies = [ "cc", ] diff --git a/Cargo.toml b/Cargo.toml index 4d116b7..6083862 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,3 +73,7 @@ gauntlet-cli = { path = "rust/cli" } [features] release = ["gauntlet-cli/release"] scenario_runner = ["gauntlet-cli/scenario_runner"] + +[patch.crates-io] +# NOTE https://github.com/ipetkov/crane/issues/336 +libffi-sys = { git = "https://github.com/tov/libffi-rs", rev = "d0704d634b6f3ffef5b6fc7e07fe965a1cff5c7b" } diff --git a/README.md b/README.md index a7bd5bb..50b4503 100644 --- a/README.md +++ b/README.md @@ -185,9 +185,15 @@ To start `systemd` service run: systemctl --user enable --now gauntlet.service ``` +#### Nix + +The nix flake in this repository is community maintained. If you face a problem, please create an issue and hopefully somebody will work on it. + +To install, you either know what to do, or you can read more [here](nix/README.md). + #### Other Linux Distributions -At the moment application is only available for Arch Linux. If you want to create a package for other distributions see [Application packaging for Linux](#application-packaging-for-Linux) +At the moment application is only available for Arch Linux and Nix. If you want to create a package for other distributions see [Application packaging for Linux](#application-packaging-for-Linux) ### Global Shortcut Main window can be opened using global shortcut or CLI command: @@ -367,7 +373,7 @@ This section contains a list of things that could be useful for someone who wants to package application for Linux distribution. If something is missing, please [create an issue](https://github.com/project-gauntlet/gauntlet/issues). -Application is already packaged for Arch Linux so you can use it as example, see [Arch Linux](#arch-linux) +Application is already packaged for [Arch Linux](#arch-linux) and [Nix](#nix) so you can use them as examples. Relevant CLI commands: diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..d6bf574 --- /dev/null +++ b/default.nix @@ -0,0 +1,13 @@ +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + nodeName = lock.nodes.root.inputs.flake-compat; + in + fetchTarball { + url = + lock.nodes.${nodeName}.locked.url + or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz"; + sha256 = lock.nodes.${nodeName}.locked.narHash; + } +) {src = ./.;}) +.defaultNix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e355443 --- /dev/null +++ b/flake.lock @@ -0,0 +1,107 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1733688869, + "narHash": "sha256-KrhxxFj1CjESDrL5+u/zsVH0K+Ik9tvoac/oFPoxSB8=", + "owner": "ipetkov", + "repo": "crane", + "rev": "604637106e420ad99907cae401e13ab6b452e7d9", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1733392399, + "narHash": "sha256-kEsTJTUQfQFIJOcLYFt/RvNxIK653ZkTBIs4DG+cBns=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "d0797a04b81caeae77bcff10a9dde78bc17f5661", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1733096140, + "narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-compat": "flake-compat", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a871c7b --- /dev/null +++ b/flake.nix @@ -0,0 +1,12 @@ +{ + description = "https://github.com/project-gauntlet/gauntlet"; + outputs = inputs: inputs.flake-parts.lib.mkFlake {inherit inputs;} {imports = [./nix];}; + inputs = { + nixpkgs.url = github:nixos/nixpkgs/nixos-unstable; + systems.url = github:nix-systems/default; + flake-parts.url = github:hercules-ci/flake-parts; + flake-compat.url = github:edolstra/flake-compat; + flake-compat.flake = false; + crane.url = github:ipetkov/crane; + }; +} diff --git a/nix/README.md b/nix/README.md new file mode 100644 index 0000000..0c78963 --- /dev/null +++ b/nix/README.md @@ -0,0 +1,64 @@ +# Nix + +The Nix package derivation is currently defined for all [default systems](https://github.com/nix-systems/default), and it can be integrated into NixOS and Home-Manager configurations as below. + +## Installation + +Here's how to reference the package derivation (and explicitly pin it) in your `flake.nix`: + +``` nix +{ + inputs.gauntlet.url = github:project-gauntlet/gauntlet/; + inputs.gauntlet.inputs.nixpkgs.follows = "nixpkgs"; +} +``` + +The package can then be referenced directly with `gauntlet.packages.${system}.default` or integrated as an overlay with `gauntlet.overlays.default`. + +## Configuration + +Under `programs.gauntlet`, the options provide the following: + +1. `enable`: adds executable to system path +2. `service.enable`: runs daemon with systemd (MacOS launchd not yet supported) + +The examples below assume flake inputs are passed to `nixpkgs.lib.nixosSystem` and `home-manager.lib.mkHomeManagerConfiguration` respectively as `inputs` parameter. + +### NixOS + +``` nix +{inputs, ...}: { + imports = [inputs.gauntlet.nixosModules.default]; + programs.gauntlet = { + enable = true; + service.enable = true; + }; +} +``` + +### Home-Manager + +Once `config.toml` is [supported](../README.md#application-config), Home-Manager can populate its contents with `programs.gauntlet.config`. + +``` nix +{inputs, ...}: { + imports = [inputs.gauntlet.homeManagerModules.default]; + programs.gauntlet = { + enable = true; + service.enable = true; + config = {}; + }; +} +``` + +## Development + +When updating dependencies or bumping the project version, please follow these steps to adjust the relevant values at the top of `./nix/overlay.nix`: + +1. If there is a new package release, set `version` to that upcoming version tag. +2. If `package-lock.json` has changed, set `npmDepsHash` to `""` and rebuild with `nix build`, copying the actual value back into `npmDepsHash`. This is necessary for `fetchNpmDeps` because `importNpmLock` doesn't work with `git://` dependencies like for `@project-gauntlet/tools`. +3. If `Cargo.lock` has changed, run `nix run .#fetch-rusty-v8-hashes` and replace `RUSTY_V8_ARCHIVE` as instructed if different. Because building `librusty_v8` takes forever, we follow nixpkgs precedent and fetch binaries in a fixed-output-derivation. + +When making any changes to nix code, please format with `nix fmt` when done. + +When running the project in development, `.#devShells.default` will provide access to all repository tooling. You can access this by running `nix develop`, or `direnv allow` if you have `direnv` + `nix-direnv`. diff --git a/nix/default.nix b/nix/default.nix new file mode 100644 index 0000000..3d3114e --- /dev/null +++ b/nix/default.nix @@ -0,0 +1,31 @@ +{inputs, ...}: { + imports = [ + ./modules/home.nix + ./modules/nixos.nix + ./overlay.nix + ]; + systems = import inputs.systems; + perSystem = { + pkgs, + lib, + system, + ... + }: let + inherit (pkgs) alejandra cargo cmake deno gauntlet gtk3 libxkbcommon libGL mkShell nodejs protobuf stdenv xorg wayland; + in { + _module.args.pkgs = import inputs.nixpkgs { + inherit system; + overlays = [inputs.self.overlays.default]; + }; + devShells.default = mkShell { + packages = [cargo cmake deno nodejs protobuf]; + shellHook = lib.optionalString stdenv.hostPlatform.isLinux '' + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${lib.makeLibraryPath [libxkbcommon libGL xorg.libX11 wayland]}" + export PATH="$PATH:${lib.makeBinPath [gtk3]}" + ''; + }; + formatter = alejandra; + packages.default = gauntlet; + packages.fetch-rusty-v8-hashes = gauntlet.fetchRustyV8Hashes; + }; +} diff --git a/nix/modules/common.nix b/nix/modules/common.nix new file mode 100644 index 0000000..043fb33 --- /dev/null +++ b/nix/modules/common.nix @@ -0,0 +1,12 @@ +{self, ...}: { + lib, + pkgs, + ... +}: { + options.programs.gauntlet = { + enable = lib.mkEnableOption "Gauntlet application launcher"; + package = lib.mkPackageOption pkgs "gauntlet" {}; + service.enable = lib.mkEnableOption "running Gauntlet as a service"; + }; + config.nixpkgs.overlays = [self.overlays.default]; +} diff --git a/nix/modules/home.nix b/nix/modules/home.nix new file mode 100644 index 0000000..aee1492 --- /dev/null +++ b/nix/modules/home.nix @@ -0,0 +1,35 @@ +flake: { + flake.homeManagerModules.default = { + config, + lib, + pkgs, + ... + }: let + inherit (lib) elem getExe mkIf mkMerge mkOption platforms toList types; + inherit (pkgs.stdenv.hostPlatform) isLinux isDarwin; + cfg = config.programs.gauntlet; + toml = pkgs.formats.toml {}; + in { + imports = [(import ./common.nix flake)]; + options.programs.gauntlet.config = mkOption { + inherit (toml) type; + default = {}; + description = "Application configuration in config.toml"; + }; + config = mkIf cfg.enable { + home.packages = [cfg.package]; + launchd.agents = mkIf (cfg.service.enable && isDarwin) { + gauntlet.enable = true; + gauntlet.config = { + RunAtLoad = true; + KeepAlive.Crashed = true; + ProgramArguments = [(getExe cfg.package) "--minimized"]; + }; + }; + xdg.configFile = mkMerge [ + (mkIf (cfg.service.enable && isLinux) {"systemd/user/gauntlet.service".source = "${cfg.package}/lib/systemd/user/gauntlet.service";}) + (mkIf (cfg.config != {}) {"gauntlet/config.toml".source = toml.generate "gauntlet.config.toml" config.programs.gauntlet.config;}) + ]; + }; + }; +} diff --git a/nix/modules/nixos.nix b/nix/modules/nixos.nix new file mode 100644 index 0000000..734d870 --- /dev/null +++ b/nix/modules/nixos.nix @@ -0,0 +1,15 @@ +flake: { + flake.nixosModules.default = { + config, + lib, + ... + }: let + cfg = config.programs.gauntlet; + in { + imports = [(import ./common.nix flake)]; + config = lib.mkIf cfg.enable { + environment.systemPackages = [cfg.package]; + systemd.packages = lib.mkIf cfg.service.enable [cfg.package]; + }; + }; +} diff --git a/nix/overlay.nix b/nix/overlay.nix new file mode 100644 index 0000000..108dd22 --- /dev/null +++ b/nix/overlay.nix @@ -0,0 +1,119 @@ +{ + config, + inputs, + ... +}: { + flake.overlays.default = final: _: let + # These must be updated following the instructions in ./nix/README.md when dependencies are updated or the version bumped + version = "v11"; + npmDepsHash = "sha256-zcqn+EsxWfdxkIKUnF8kiuURtPiE6XQOaDAspuKEe64="; + RUSTY_V8_ARCHIVE = fetchRustyV8 "130.0.2" { + aarch64-darwin = "sha256-aWZ/4Q4Wttx37xOdBmTCPGP+eYGhr4CM1UkYq8pC7Qs="; + aarch64-linux = "sha256-p9+tHmKIM5wBABubHIAstpwfzO19ypPzOuaV4b6loCU="; + x86_64-darwin = "sha256-zNC0DAkMbbFM1M+t6rgKtN0QAm4ONEbCi6Sxivhf8dk="; + x86_64-linux = "sha256-ew2WZhdsHfffRQtif076AWAlFohwPo/RbmW/6D3LzkU="; + }; + + inherit + (final) + # Libraries + lib + stdenv + # Builders + buildPackages + fetchurl + fetchNpmDeps + makeWrapper + writeShellScriptBin + # Packages + cmake + deno + gtk3 + libxkbcommon + libGL + xorg + nodejs + openssl + pkg-config + protobuf + wayland + yq + ; + inherit (buildPackages.npmHooks.override {inherit nodejs;}) npmConfigHook; + inherit (lib) concatStringsSep getExe' makeBinPath makeLibraryPath optional; + inherit (stdenv.hostPlatform) isLinux rust system; + # Borrowed from other packages in nixpkgs https://github.com/search?q=repo%3ANixOS%2Fnixpkgs%20RUSTY_V8_ARCHIVE&type=code + buildRustyV8Url = version: target: "https://github.com/denoland/rusty_v8/releases/download/v${version}/librusty_v8_release_${target}.a.gz"; + fetchRustyV8 = version: hashes: + fetchurl { + name = "librusty_v8-${version}"; + url = buildRustyV8Url version rust.rustcTarget; + hash = hashes.${system}; + meta.version = version; + meta.sourceProvenance = [lib.sourceTypes.binaryNativeCode]; + }; + fetchRustyV8Hashes = writeShellScriptBin "fetch-librusty_v8-hashes.sh" '' + version=$(${getExe' yq "tomlq"} '.package | map(select(.name == "v8")) | .[0].version' --raw-output ${inputs.self}/Cargo.lock) + echo "Update ./nix/overlay.nix as follows:" + echo "RUSTY_V8_ARCHIVE = fetchRustyV8 \"$version\" {" + for system in ${concatStringsSep " " config.systems}; do + target=$(nix eval --raw nixpkgs#legacyPackages.$system.stdenv.hostPlatform.rust.rustcTarget) + hash=$(nix-prefetch-url --print-path ${buildRustyV8Url "$version" "$target"} | tail -n 1 | xargs nix hash file) + echo " $system = \"$hash\";" + done + echo "};" + ''; + pname = "gauntlet"; + src = inputs.self; + cargoArtifactsArgs = { + inherit pname src version RUSTY_V8_ARCHIVE; + cargoExtraArgs = "--features release"; + nativeBuildInputs = [cmake pkg-config protobuf]; + buildInputs = [openssl]; + # OPENSSL_CONFIG_DIR didn't work for vendored dependencies + OPENSSL_NO_VENDOR = true; + }; + craneLib = inputs.crane.mkLib final; + cargoArtifacts = craneLib.buildDepsOnly cargoArtifactsArgs; + in { + gauntlet = craneLib.buildPackage { + inherit (cargoArtifactsArgs) cargoExtraArgs OPENSSL_NO_VENDOR RUSTY_V8_ARCHIVE; + inherit cargoArtifacts pname src version; + # fetchNpmDeps + makeCacheWritable is necessary with npm git:// dependencies + npmDeps = fetchNpmDeps { + inherit src; + name = "${pname}-${version}-npm-deps"; + hash = npmDepsHash; + }; + makeCacheWritable = true; + nativeBuildInputs = cargoArtifactsArgs.nativeBuildInputs ++ [nodejs npmConfigHook] ++ optional isLinux makeWrapper; + buildInputs = cargoArtifactsArgs.buildInputs ++ [deno]; + preBuild = "npm run build"; + postInstall = + if isLinux + then '' + install -Dm644 assets/linux/gauntlet.desktop $out/share/applications/gauntlet.desktop + install -Dm644 assets/linux/gauntlet.service $out/lib/systemd/user/gauntlet.service + install -Dm644 assets/linux/icon_256.png $out/share/icons/hicolor/256x256/apps/gauntlet.png + '' + else '' + contentsDir=$out/Applications/Gauntlet.app/Contents + install -Dm755 $out/bin/gauntlet $contentsDir/MacOS/Gauntlet + install -Dm644 assets/macos/AppIcon.icns $contentsDir/Resources/AppIcon.icns + install -Dm644 assets/macos/Info.plist $contentsDir/Info.plist + ''; + postFixup = + if isLinux + then '' + patchelf --add-rpath ${makeLibraryPath [libxkbcommon libGL xorg.libX11 wayland]} $out/bin/gauntlet + wrapProgram $out/bin/gauntlet --suffix PATH : ${makeBinPath [gtk3]} + substituteInPlace $out/lib/systemd/user/gauntlet.service --replace /usr/bin/gauntlet $out/bin/gauntlet + '' + else '' + substituteInPlace $out/Applications/Gauntlet.app/Contents/Info.plist --replace __VERSION__ ${version} + ''; + passthru = {inherit fetchRustyV8Hashes;}; + meta.mainProgram = "gauntlet"; + }; + }; +} From ba497bf14d72fe94d551a48aa4807643a8a3490e Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 21 Dec 2024 19:34:19 +0100 Subject: [PATCH 229/540] Fix non-closable ghost window after pressing shortcut if main window is already open --- rust/client/src/ui/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index ee99dad..2ed6411 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -350,7 +350,7 @@ fn run_wayland( start_mode: iced_layershell::settings::StartMode::Background, events_transparent: true, keyboard_interactivity: iced_layershell::reexport::KeyboardInteractivity::None, - size: Some((0, 0)), + size: None, ..Default::default() }) .subscription(subscription) @@ -1372,8 +1372,8 @@ fn view_hud(state: &AppModel) -> Element<'_, AppMsg> { hud } None => { - horizontal_space() - .into() + container(horizontal_space()) + .themed(ContainerStyle::Hud) } } } @@ -1886,7 +1886,7 @@ impl AppModel { } fn hide_window(&mut self) -> Task { - let Some(main_window_id) = self.main_window_id else { + let Some(main_window_id) = self.main_window_id.take() else { return Task::none() }; @@ -1922,6 +1922,10 @@ impl AppModel { } fn show_window(&mut self) -> Task { + if let Some(_) = self.main_window_id { + return Task::none() + }; + #[cfg(target_os = "linux")] let (main_window_id, open_task) = if self.wayland { open_main_window_wayland() From 68be9a1a2f724b53953f4657deeede09cad7db23 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 21 Dec 2024 20:57:15 +0100 Subject: [PATCH 230/540] Rework plugin shutdown to be more reliable --- rust/client/src/ui/mod.rs | 1 + rust/plugin_runtime/src/api.rs | 52 ++++++++++++++++---------- rust/plugin_runtime/src/lib.rs | 14 ++++--- rust/plugin_runtime/src/model.rs | 6 +++ rust/server/src/plugins/js.rs | 63 +++++++++++++++++++------------- 5 files changed, 86 insertions(+), 50 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 2ed6411..1d0bd3f 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -1372,6 +1372,7 @@ fn view_hud(state: &AppModel) -> Element<'_, AppMsg> { hud } None => { + // this should never be shown, but in case it does, do not make it fully transparent container(horizontal_space()) .themed(ContainerStyle::Hud) } diff --git a/rust/plugin_runtime/src/api.rs b/rust/plugin_runtime/src/api.rs index 4e48422..226b4b4 100644 --- a/rust/plugin_runtime/src/api.rs +++ b/rust/plugin_runtime/src/api.rs @@ -3,7 +3,7 @@ use crate::{JsRequest, JsResponse, JsUiRenderLocation}; use gauntlet_common::model::{EntrypointId, RootWidget, UiRenderLocation}; use std::collections::HashMap; use anyhow::anyhow; -use gauntlet_utils::channel::RequestSender; +use gauntlet_utils::channel::{RequestError, RequestSender}; #[allow(async_fn_in_trait)] pub trait BackendForPluginRuntimeApi { @@ -62,6 +62,18 @@ impl BackendForPluginRuntimeApiProxy { request_sender } } + + async fn request(&self, request: JsRequest) -> anyhow::Result { + match self.request_sender.send_receive(request).await { + Ok(ok) => Ok(ok.map_err(|e| anyhow!(e))?), + Err(err) => { + match err { + RequestError::TimeoutError => Err(anyhow!("Backend was unable to process message in a timely manner")), + RequestError::OtherSideWasDropped => Err(anyhow!("Plugin runtime is being stopped")) + } + } + } + } } impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { @@ -71,7 +83,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { refresh_search_list, }; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::Nothing => Ok(()), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -82,7 +94,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { path: path.to_string(), }; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::AssetData { data } => Ok(data), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -91,7 +103,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result> { let request = JsRequest::GetCommandGeneratorEntrypointIds; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::CommandGeneratorEntrypointIds { data } => Ok(data), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -100,7 +112,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { async fn get_plugin_preferences(&self) -> anyhow::Result> { let request = JsRequest::GetPluginPreferences; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::PluginPreferences { data } => Ok(data), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -111,7 +123,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { entrypoint_id, }; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::EntrypointPreferences { data } => Ok(data), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -120,7 +132,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { async fn plugin_preferences_required(&self) -> anyhow::Result { let request = JsRequest::PluginPreferencesRequired; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::PluginPreferencesRequired { data } => Ok(data), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -131,7 +143,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { entrypoint_id, }; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::EntrypointPreferencesRequired { data } => Ok(data), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -140,7 +152,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { async fn clipboard_read(&self) -> anyhow::Result { let request = JsRequest::ClipboardRead; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::ClipboardRead { data } => Ok(data), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -149,7 +161,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { async fn clipboard_read_text(&self) -> anyhow::Result> { let request = JsRequest::ClipboardReadText; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::ClipboardReadText { data } => Ok(data), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -160,7 +172,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { data }; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::Nothing => Ok(()), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -171,7 +183,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { data, }; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::Nothing => Ok(()), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -180,7 +192,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { async fn clipboard_clear(&self) -> anyhow::Result<()> { let request = JsRequest::ClipboardClear; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::Nothing => Ok(()), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -192,7 +204,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { show, }; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::Nothing => Ok(()), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -203,7 +215,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { display, }; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::Nothing => Ok(()), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -219,7 +231,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { modifier_meta, }; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::ActionIdForShortcut { data } => Ok(data), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -242,7 +254,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { container, }; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::Nothing => Ok(()), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -257,7 +269,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { }, }; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::Nothing => Ok(()), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -270,7 +282,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { entrypoint_preferences_required, }; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::Nothing => Ok(()), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } @@ -279,7 +291,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { async fn ui_clear_inline_view(&self) -> anyhow::Result<()> { let request = JsRequest::ClearInlineView; - match self.request_sender.send_receive(request).await?.map_err(|e| anyhow!(e))? { + match self.request(request).await? { JsResponse::Nothing => Ok(()), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } diff --git a/rust/plugin_runtime/src/lib.rs b/rust/plugin_runtime/src/lib.rs index 4741d94..ca2549d 100644 --- a/rust/plugin_runtime/src/lib.rs +++ b/rust/plugin_runtime/src/lib.rs @@ -87,7 +87,7 @@ async fn run_outer(socket_name: String) -> anyhow::Result<()> { tokio::select! { _ = stop_token.cancelled() => { - tracing::info!("Plugin runtime outer loop has been stopped {:?}", plugin_id) + tracing::debug!("Plugin runtime outer loop will be stopped {:?}", plugin_id) } result @ _ = { tokio::task::unconstrained(async { @@ -120,6 +120,10 @@ async fn run_outer(socket_name: String) -> anyhow::Result<()> { } } + send_message(JsMessageSide::PluginRuntime, &mut sender, JsPluginRuntimeMessage::Stopped).await?; + + tracing::info!("Plugin runtime outer loop has been stopped {:?}", plugin_id); + drop((recver, sender)); Ok(()) @@ -142,7 +146,7 @@ async fn run(outer_handle: Handle, stop_token: CancellationToken, init: JsInit, tokio::select! { _ = stop_token.cancelled() => { - tracing::info!("Plugin runtime inner loop has been stopped {:?}", plugin_id) + tracing::debug!("Plugin runtime inner loop will be stopped {:?}", plugin_id) } result @ _ = { tokio::task::unconstrained(async { @@ -151,12 +155,12 @@ async fn run(outer_handle: Handle, stop_token: CancellationToken, init: JsInit, } => { if let Err(err) = result { tracing::error!("Plugin runtime inner loop has failed {:?} - {:?}", plugin_id, err) - } else { - tracing::error!("Plugin runtime inner loop has stopped unexpectedly {:?}", plugin_id) } } } + tracing::info!("Plugin runtime inner loop has been stopped {:?}", plugin_id); + Ok(()) } @@ -183,7 +187,7 @@ async fn request_loop( rx }; - send_message(JsMessageSide::PluginRuntime, send, request).await?; + send_message(JsMessageSide::PluginRuntime, send, JsPluginRuntimeMessage::Request(request)).await?; tracing::trace!("Waiting for oneshot response..."); diff --git a/rust/plugin_runtime/src/model.rs b/rust/plugin_runtime/src/model.rs index 6dc001f..5acdde4 100644 --- a/rust/plugin_runtime/src/model.rs +++ b/rust/plugin_runtime/src/model.rs @@ -66,6 +66,12 @@ pub enum JsPluginPermissionsMainSearchBar { Read, } +#[derive(Debug, Encode, Decode)] +pub enum JsPluginRuntimeMessage { + Stopped, + Request(JsRequest), +} + #[derive(Debug, Encode, Decode)] pub enum JsResponse { Nothing, diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index e1c082b..175ab52 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -29,7 +29,7 @@ use gauntlet_common::dirs::Dirs; use gauntlet_common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; use gauntlet_common::rpc::frontend_api::FrontendApi; use gauntlet_common::settings_env_data_to_string; -use gauntlet_plugin_runtime::{recv_message, send_message, BackendForPluginRuntimeApi, JsAdditionalSearchItem, JsClipboardData, JsInit, JsKeyboardEventOrigin, JsPluginCode, JsPluginPermissions, JsPreferenceUserData, JsEvent, JsUiPropertyValue, JsRequest, JsUiRenderLocation, JsResponse, JsMessage, JsPluginPermissionsFileSystem, JsPluginPermissionsExec, JsPluginPermissionsMainSearchBar, JsMessageSide}; +use gauntlet_plugin_runtime::{recv_message, send_message, BackendForPluginRuntimeApi, JsAdditionalSearchItem, JsClipboardData, JsInit, JsKeyboardEventOrigin, JsPluginCode, JsPluginPermissions, JsPreferenceUserData, JsEvent, JsUiPropertyValue, JsRequest, JsUiRenderLocation, JsResponse, JsMessage, JsPluginPermissionsFileSystem, JsPluginPermissionsExec, JsPluginPermissionsMainSearchBar, JsMessageSide, JsPluginRuntimeMessage}; use crate::model::{IntermediateUiEvent}; use crate::plugins::clipboard::Clipboard; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; @@ -288,12 +288,12 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run send_message(JsMessageSide::Backend, &mut sender, JsMessage::Stop).await?; - tokio::time::sleep(Duration::from_secs(1)).await; + // select should be stopped by accepting stopped plugin runtime message in request loop + std::future::pending::<()>().await; } - result @ _ = { + result = { tokio::task::unconstrained(async { loop { - if let Err(err) = event_loop(&mut command_receiver, &sender, plugin_id.clone()).await { tracing::error!("Event loop faced an error {:?}", err); break; @@ -301,19 +301,27 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run } }) } => { - tracing::error!("Event loop has unexpectedly stopped {:?}", plugin_id) + tracing::error!("Event loop has been stopped {:?}", plugin_id) } - result @ _ = { + result = { tokio::task::unconstrained(async { loop { - if let Err(err) = request_loop(&mut recver, &sender, &api).await { - tracing::error!("Request loop faced an error {:?}", err); - break; + match request_loop(&mut recver, &sender, &api).await { + Ok(stop) => { + if stop { + tracing::info!("Stopping request loop as requested by plugin runtime"); + break; + } + } + Err(err) => { + tracing::error!("Request loop faced an error {:?}", err); + break; + } } } }) } => { - tracing::error!("Request loop has unexpectedly stopped {:?}", plugin_id) + tracing::error!("Request loop has been stopped {:?}", plugin_id) } } @@ -403,32 +411,37 @@ async fn event_loop(command_receiver: &mut tokio::sync::broadcast::Receiver, api: &BackendForPluginRuntimeApiImpl) -> anyhow::Result<()> { - match recv_message::(JsMessageSide::Backend, recv).await { +async fn request_loop(recv: &mut RecvHalf, send: &Mutex, api: &BackendForPluginRuntimeApiImpl) -> anyhow::Result { + match recv_message::(JsMessageSide::Backend, recv).await { Err(e) => { Err(anyhow!("Unable to handle message: {:?}", e)) } Ok(message) => { - tracing::trace!("Handling request message: {:?}", message); + tracing::trace!("Handling js runtime message: {:?}", message); - match handle_message(message, api).await { - Ok(response) => { - let mut send = send.lock().await; + match message { + JsPluginRuntimeMessage::Stopped => Ok(true), + JsPluginRuntimeMessage::Request(message) => { + match handle_message(message, api).await { + Ok(response) => { + let mut send = send.lock().await; - tracing::trace!("Sending request response: {:?}", response); + tracing::trace!("Sending request response: {:?}", response); - send_message(JsMessageSide::Backend, &mut send, JsMessage::Response(Ok(response))).await?; + send_message(JsMessageSide::Backend, &mut send, JsMessage::Response(Ok(response))).await?; - Ok(()) - } - Err(err) => { - let mut send = send.lock().await; + Ok(false) + } + Err(err) => { + let mut send = send.lock().await; - let err = format!("{:?}", err); + let err = format!("{:?}", err); - send_message(JsMessageSide::Backend, &mut send, JsMessage::Response(Err(err))).await?; + send_message(JsMessageSide::Backend, &mut send, JsMessage::Response(Err(err))).await?; - Ok(()) + Ok(false) + } + } } } } From f2caf8a60bbc00550c9c9341b4c729d7c05d793a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:47:35 +0100 Subject: [PATCH 231/540] Update CHANGELOG.md and README.md --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ README.md | 3 +-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bd4d16..b8a876a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,33 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +### General +- Each plugin Deno runtime now runs in separate OS process +- Nix package, dev shell and home-manager modules are now available (contributed by @schradert) +- Replaced wayland layer-shell implementation, fixing several long-standing issues + - Fixed icons not being rendered properly + - Fixed Ctrl + A shortcut not working in text inputs + - Fixed Backspace only removing single character at a time +- Improved window handling on macOS + - Main window is now non-activating, so it doesn't take away focus from front-most application + - Main window no longer shows up in macOS Dock panel + - Fixed application not receiving keyboard input if front-most application is using Secure Event Input (e.g. Terminal) +- Main window is now closed automatically when plugin action is executed +- A lot of internal dependency updates + +### Plugin API +- Internal JS functions are no longer accessible from plugins +- **BREAKING CHANGE**: Deno updated from `v1.37.0` to `v2.1.1` +- **BREAKING CHANGE**: Clipboard api now uses `ArrayBuffer` instead of `Blob` +- `@project-gauntlet/deno` is deprecated in favor of `@types/deno` + +### UI/UX Improvements +- Hud window was moved lower (below main window) on non-wayland platforms + +### Fixes +- Fixed possible freeze when spamming keys or buttons in plugin view +- Fixed separator in `` view not being horizontally centered + ## [11] - 2024-11-16 ### General diff --git a/README.md b/README.md index 50b4503..d349ee1 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ https://github.com/user-attachments/assets/19964ed6-9cd9-48d4-9835-6be04de14b66 - Linux - Both X11 and Wayland (via LayerShell protocol) are supported + - Application plugin depends on `gtk-launch` - macOS - Windows - Bundled "Applications" plugin is not yet implemented. See [#9](https://github.com/project-gauntlet/gauntlet/issues/9) @@ -421,7 +422,6 @@ You will need: To build dev run: ```bash -git submodule update --init npm ci npm run build npm run build-dev-plugin @@ -431,7 +431,6 @@ In dev (without "release" feature) application will use only directories inside To build release run: ```bash -git submodule update --init npm ci npm run build cargo build --release --features release From 8fb6b5becb19242a421635a7cd08806ba8564e91 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 22 Dec 2024 13:12:46 +0100 Subject: [PATCH 232/540] Fix frontmost application not getting a focus on macOS after main window is closed --- Cargo.lock | 1 + rust/client/Cargo.toml | 3 +++ rust/client/src/ui/mod.rs | 12 ++++++++++++ 3 files changed, 16 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 219432d..e30da9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3957,6 +3957,7 @@ dependencies = [ "iced_layershell", "image 0.25.5", "itertools 0.13.0", + "objc2-app-kit", "once_cell", "serde", "serde_json", diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index a153a6b..a20338c 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -31,6 +31,9 @@ tray-icon = { version = "0.19.2", default-features = false } [target.'cfg(target_os = "linux")'.dependencies] iced_layershell.workspace = true +[target.'cfg(target_os = "macos")'.dependencies] +objc2-app-kit = { version = "0.2.2", features = ["NSWorkspace"] } + [build-dependencies] gauntlet-component-model.workspace = true anyhow.workspace = true diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 1d0bd3f..a09c493 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -1911,6 +1911,18 @@ impl AppModel { window::close(main_window_id) ); + #[cfg(target_os = "macos")] + unsafe { + // when closing NSPanel current active application doesn't automatically become key window + // is there a proper way? without doing this manually + let app = objc2_app_kit::NSWorkspace::sharedWorkspace() + .menuBarOwningApplication(); + + if let Some(app) = app { + app.activateWithOptions(objc2_app_kit::NSApplicationActivationOptions::empty()); + } + } + match &self.global_state { GlobalState::PluginView { plugin_view_data: PluginViewData { plugin_id, .. }, .. } => { commands.push(self.close_plugin_view(plugin_id.clone())); From a00b78e719eefdbbdfc492e8f7754609ba7fd15e Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 22 Dec 2024 13:17:09 +0100 Subject: [PATCH 233/540] Update iced-layershell to fix click transparency not working on hud window --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e30da9c..3616fc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5167,7 +5167,7 @@ dependencies = [ [[package]] name = "iced_layershell" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#7df6d555dd018b0df400dfc814f664da8f01958e" +source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#de88e8d3be5e7ebef6d8456ea62eed26e503bf92" dependencies = [ "futures", "iced", @@ -5186,7 +5186,7 @@ dependencies = [ [[package]] name = "iced_layershell_macros" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#7df6d555dd018b0df400dfc814f664da8f01958e" +source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#de88e8d3be5e7ebef6d8456ea62eed26e503bf92" dependencies = [ "darling", "manyhow", @@ -5916,7 +5916,7 @@ dependencies = [ [[package]] name = "layershellev" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#7df6d555dd018b0df400dfc814f664da8f01958e" +source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#de88e8d3be5e7ebef6d8456ea62eed26e503bf92" dependencies = [ "bitflags 2.6.0", "calloop 0.14.2", @@ -11646,7 +11646,7 @@ dependencies = [ [[package]] name = "waycrate_xkbkeycode" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#7df6d555dd018b0df400dfc814f664da8f01958e" +source = "git+https://github.com/project-gauntlet/exwlshelleventloop.git?branch=gauntlet-0.13#de88e8d3be5e7ebef6d8456ea62eed26e503bf92" dependencies = [ "bitflags 2.6.0", "calloop 0.14.2", From 95b0243d5b2fb8bae0de44d7c39712f77cca1860 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 22 Dec 2024 13:18:10 +0100 Subject: [PATCH 234/540] Bump version in nix overlay --- nix/overlay.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/overlay.nix b/nix/overlay.nix index 108dd22..6c0e5f7 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -5,7 +5,7 @@ }: { flake.overlays.default = final: _: let # These must be updated following the instructions in ./nix/README.md when dependencies are updated or the version bumped - version = "v11"; + version = "v12"; npmDepsHash = "sha256-zcqn+EsxWfdxkIKUnF8kiuURtPiE6XQOaDAspuKEe64="; RUSTY_V8_ARCHIVE = fetchRustyV8 "130.0.2" { aarch64-darwin = "sha256-aWZ/4Q4Wttx37xOdBmTCPGP+eYGhr4CM1UkYq8pC7Qs="; From 00163031e0bd4ea2ecbc15201fad873f2166e950 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:36:45 +0100 Subject: [PATCH 235/540] Remove publishing of deprecated @project-gauntlet/deno --- .github/workflows/release.yaml | 2 +- .github/workflows/setup-linux.yaml | 2 +- .github/workflows/setup-windows.yaml | 2 +- js/build/src/main.ts | 8 -------- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 20de2e8..8cb295b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -34,7 +34,7 @@ jobs: submodules: true - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - run: npm ci diff --git a/.github/workflows/setup-linux.yaml b/.github/workflows/setup-linux.yaml index 98a92e0..ba86a02 100644 --- a/.github/workflows/setup-linux.yaml +++ b/.github/workflows/setup-linux.yaml @@ -26,7 +26,7 @@ jobs: - run: git pull - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: '@project-gauntlet' - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/setup-windows.yaml b/.github/workflows/setup-windows.yaml index e409a6a..dd7eb99 100644 --- a/.github/workflows/setup-windows.yaml +++ b/.github/workflows/setup-windows.yaml @@ -20,7 +20,7 @@ jobs: - run: git pull - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 registry-url: "https://registry.npmjs.org" scope: '@project-gauntlet' - uses: dtolnay/rust-toolchain@stable diff --git a/js/build/src/main.ts b/js/build/src/main.ts index 9569bba..33e8a04 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -238,10 +238,6 @@ async function makeRepoChanges(projectRoot: string): Promise<{ releaseNotes: str spawnWithErrors('npm', ['version', `0.${newVersion}.0`], { cwd: packageDir }) } - console.log("Bump version for deno subproject...") - const denoProjectPath = path.join(projectRoot, "js", "deno"); - bumpNpmPackage(denoProjectPath) - console.log("Bump version for api subproject...") const apiProjectPath = path.join(projectRoot, "js", "api"); bumpNpmPackage(apiProjectPath) @@ -461,10 +457,6 @@ async function packageForWindows(projectRoot: string, arch: string): Promise<{ f function publishNpmPackage(projectRoot: string) { - console.log("Publishing npm deno package...") - const denoProjectPath = path.join(projectRoot, "js", "deno"); - spawnWithErrors('npm', ['publish'], { cwd: denoProjectPath }) - console.log("Publishing npm api package...") const apiProjectPath = path.join(projectRoot, "js", "api"); spawnWithErrors('npm', ['publish'], { cwd: apiProjectPath }) From ff05d7c6735b7ce0b36f37e66fbf69966fc95d86 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 22 Dec 2024 13:51:26 +0000 Subject: [PATCH 236/540] Prepare for v12 release --- CHANGELOG.md | 2 ++ VERSION | 2 +- js/api/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8a876a..2eeefbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +## [12] - 2024-12-22 + ### General - Each plugin Deno runtime now runs in separate OS process - Nix package, dev shell and home-manager modules are now available (contributed by @schradert) diff --git a/VERSION b/VERSION index 9d60796..3cacc0b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -11 \ No newline at end of file +12 \ No newline at end of file diff --git a/js/api/package.json b/js/api/package.json index b3c2413..9663918 100644 --- a/js/api/package.json +++ b/js/api/package.json @@ -1,6 +1,6 @@ { "name": "@project-gauntlet/api", - "version": "0.11.0", + "version": "0.12.0", "type": "module", "exports": { "./components": { diff --git a/package-lock.json b/package-lock.json index ce8050a..b1719ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ }, "js/api": { "name": "@project-gauntlet/api", - "version": "0.11.0", + "version": "0.12.0", "devDependencies": { "@project-gauntlet/typings": "*", "@rollup/plugin-alias": "^5.1.1", From dbc22aa81b1afce91efb869f0df9ccbff7b6cd6a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 22 Dec 2024 16:53:07 +0100 Subject: [PATCH 237/540] Update mismatched nix hash --- nix/README.md | 2 ++ nix/overlay.nix | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nix/README.md b/nix/README.md index 0c78963..9839d57 100644 --- a/nix/README.md +++ b/nix/README.md @@ -6,6 +6,8 @@ The Nix package derivation is currently defined for all [default systems](https: Here's how to reference the package derivation (and explicitly pin it) in your `flake.nix`: +**NOTE**: Currently there is an issue with release process causing commit with version tag to have mismatched hash. Please use next commit which updates the hash in following format: `github:project-gauntlet/gauntlet/` + ``` nix { inputs.gauntlet.url = github:project-gauntlet/gauntlet/; diff --git a/nix/overlay.nix b/nix/overlay.nix index 6c0e5f7..a84b768 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -6,7 +6,7 @@ flake.overlays.default = final: _: let # These must be updated following the instructions in ./nix/README.md when dependencies are updated or the version bumped version = "v12"; - npmDepsHash = "sha256-zcqn+EsxWfdxkIKUnF8kiuURtPiE6XQOaDAspuKEe64="; + npmDepsHash = "sha256-TlfUwNsmyN4dzqBh3CW33pGXxBZHLhSDyAqS4fJCmPU="; RUSTY_V8_ARCHIVE = fetchRustyV8 "130.0.2" { aarch64-darwin = "sha256-aWZ/4Q4Wttx37xOdBmTCPGP+eYGhr4CM1UkYq8pC7Qs="; aarch64-linux = "sha256-p9+tHmKIM5wBABubHIAstpwfzO19ypPzOuaV4b6loCU="; From c555579f2dcf9888a0707118748dc0041d9c3a33 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 23 Dec 2024 16:26:10 +0100 Subject: [PATCH 238/540] Fix zombie processes being left over after plugin runtime was stopped --- rust/plugin_runtime/src/lib.rs | 4 +- rust/server/src/plugins/js.rs | 79 ++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/rust/plugin_runtime/src/lib.rs b/rust/plugin_runtime/src/lib.rs index ca2549d..870103a 100644 --- a/rust/plugin_runtime/src/lib.rs +++ b/rust/plugin_runtime/src/lib.rs @@ -122,7 +122,7 @@ async fn run_outer(socket_name: String) -> anyhow::Result<()> { send_message(JsMessageSide::PluginRuntime, &mut sender, JsPluginRuntimeMessage::Stopped).await?; - tracing::info!("Plugin runtime outer loop has been stopped {:?}", plugin_id); + tracing::debug!("Plugin runtime outer loop has been stopped {:?}", plugin_id); drop((recver, sender)); @@ -159,7 +159,7 @@ async fn run(outer_handle: Handle, stop_token: CancellationToken, init: JsInit, } } - tracing::info!("Plugin runtime inner loop has been stopped {:?}", plugin_id); + tracing::debug!("Plugin runtime inner loop has been stopped {:?}", plugin_id); Ok(()) } diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 175ab52..92acfa8 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -258,7 +258,7 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run .context("unable to get current_exe")?; #[cfg(not(feature = "scenario_runner"))] - std::process::Command::new(current_exe) + let mut runtime_process = std::process::Command::new(current_exe) .env(PLUGIN_RUNTIME_ENV, name_str) .spawn() .context("start plugin runtime process")?; @@ -278,61 +278,84 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run send_message(JsMessageSide::Backend, &mut sender, init).await?; - let sender = Mutex::new(sender); + let sender = Arc::new(Mutex::new(sender)); - tokio::select! { - _ = run_status_guard.stopped() => { - let mut sender = sender.lock().await; + tokio::task::spawn({ + let sender = sender.clone(); + async move { + run_status_guard.stopped().await; tracing::info!("Requesting plugin runtime to stop..."); - send_message(JsMessageSide::Backend, &mut sender, JsMessage::Stop).await?; - - // select should be stopped by accepting stopped plugin runtime message in request loop - std::future::pending::<()>().await; + let mut sender = sender.lock().await; + if let Err(err) = send_message(JsMessageSide::Backend, &mut sender, JsMessage::Stop).await { + tracing::error!("Error when sending stop request to plugin runtime {:?}", err); + } } + }); + + tokio::select! { result = { - tokio::task::unconstrained(async { + let sender = sender.clone(); + let plugin_id = plugin_id.clone(); + tokio::task::unconstrained(async move { loop { if let Err(err) = event_loop(&mut command_receiver, &sender, plugin_id.clone()).await { tracing::error!("Event loop faced an error {:?}", err); break; } } - }) + }) } => { tracing::error!("Event loop has been stopped {:?}", plugin_id) } result = { tokio::task::unconstrained(async { - loop { - match request_loop(&mut recver, &sender, &api).await { - Ok(stop) => { - if stop { - tracing::info!("Stopping request loop as requested by plugin runtime"); - break; - } - } - Err(err) => { - tracing::error!("Request loop faced an error {:?}", err); - break; - } - } - } + let sender = sender.clone(); + loop { + match request_loop(&mut recver, &sender, &api).await { + Ok(stop) => { + if stop { + tracing::debug!("Stopping request loop as requested by plugin runtime"); + break; + } + } + Err(err) => { + tracing::error!("Request loop faced an error {:?}", err); + break; + } + } + } }) } => { - tracing::error!("Request loop has been stopped {:?}", plugin_id) + tracing::debug!("Request loop has been stopped {:?}", plugin_id) } } drop((recver, sender)); - drop(run_status_guard); - if let Err(err) = cache.clear_plugin_icon_cache_dir(&plugin_uuid) { tracing::error!(target = "plugin", "plugin {:?} unable to cleanup icon cache {:?}", plugin_id, err) } + #[cfg(not(feature = "scenario_runner"))] + { + let code = runtime_process.wait() + .context("Error while waiting for JS runtime process to finish")? + .code(); + + match code { + Some(code) => { + if code == 0 { + tracing::info!("Plugin Runtime was stopped successfully") + } else { + tracing::error!("Runtime process finished with status code: {code}") + } + }, + None => tracing::error!("Process terminated by signal") + } + } + Ok(()) } From 45af858f13e7ec2e494188cf66309237739dd554 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 24 Dec 2024 14:25:37 +0100 Subject: [PATCH 239/540] Fix npm run dev sometimes not working --- rust/server/src/plugins/js.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 92acfa8..045dd51 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -201,9 +201,10 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run uds_socket_file.to_fs_name::()? }; - let opts = ListenerOptions::new().name(name); - - let listener = opts.create_tokio()?; + let listener = ListenerOptions::new() + .name(name) + .reclaim_name(false) + .create_tokio()?; let home_dir = home_dir .to_str() From dd12028e15a48e39a514a23fddc5cb6902e83210 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 24 Dec 2024 14:49:15 +0100 Subject: [PATCH 240/540] Fix values in `permissions.exec.command` not being resolved properly --- Cargo.lock | 19 +++++++++++++++++++ dev_plugin/gauntlet.toml | 4 ++-- dev_plugin/src/command-a.ts | 18 +++++++++++++++++- rust/plugin_runtime/Cargo.toml | 1 + rust/plugin_runtime/src/permissions.rs | 2 +- rust/server/src/plugins/loader.rs | 14 +++++++++++++- 6 files changed, 53 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3616fc9..c19535b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3334,6 +3334,12 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "equivalent" version = "1.0.1" @@ -4061,6 +4067,7 @@ dependencies = [ "tracing", "typed-path", "walkdir", + "which 7.0.1", ] [[package]] @@ -12062,6 +12069,18 @@ dependencies = [ "winsafe", ] +[[package]] +name = "which" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028" +dependencies = [ + "either", + "env_home", + "rustix", + "winsafe", +] + [[package]] name = "whoami" version = "1.5.2" diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 8ebd256..1656ae3 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -192,7 +192,7 @@ os = 'macos' os = 'windows' [permissions] -environment = ["RUST_LOG"] +environment = ["RUST_LOG", "LD_LIBRARY_PATH"] system = ["systemMemoryInfo"] network = ["upload.wikimedia.org", "api.github.com"] clipboard = ["read", "write", "clear"] @@ -210,5 +210,5 @@ read = [ write = ["/home/exidex/.test"] [permissions.exec] -command = ["ls"] +command = ["echo"] executable = ["/usr/bin/ls"] \ No newline at end of file diff --git a/dev_plugin/src/command-a.ts b/dev_plugin/src/command-a.ts index e52b8fb..fb6381d 100644 --- a/dev_plugin/src/command-a.ts +++ b/dev_plugin/src/command-a.ts @@ -1,5 +1,21 @@ - export default function Command() { + const env = Deno.env.get("LD_LIBRARY_PATH"); + + console.log("LD_LIBRARY_PATH:", env); + + const command = new Deno.Command("echo", { + args: ["test"], + env: { + LD_LIBRARY_PATH: "" + } + }); + + const child = command.outputSync(); + + const stdout = new TextDecoder().decode(child.stdout); + + console.dir(stdout) + const systemMemoryInfo = Deno.systemMemoryInfo(); console.dir(systemMemoryInfo) diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml index 0812dbe..89eb7c1 100644 --- a/rust/plugin_runtime/Cargo.toml +++ b/rust/plugin_runtime/Cargo.toml @@ -31,6 +31,7 @@ deno_core = { version = "0.321.0" } # deno 2.1.1 deno_runtime = { version = "0.188.0" } resvg = { version = "0.44.0", default-features = false} numbat = "1.14.0" +which = "7.0.1" [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] libc = "0.2" diff --git a/rust/plugin_runtime/src/permissions.rs b/rust/plugin_runtime/src/permissions.rs index 7edcdd1..3b10793 100644 --- a/rust/plugin_runtime/src/permissions.rs +++ b/rust/plugin_runtime/src/permissions.rs @@ -131,7 +131,7 @@ fn run_permission( let granted_command = permissions.command .iter() - .map(|cmd| AllowRunDescriptor(PathBuf::from(cmd))) + .flat_map(|cmd| anyhow::Ok(AllowRunDescriptor(which::which_global(cmd)?))) .collect::>(); let mut granted = HashSet::new(); diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index 57e2158..3ae8bcd 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -364,7 +364,7 @@ impl PluginLoader { Self::validate_network_permissions(&permissions.network)?; Self::validate_path_permissions(&permissions.filesystem.read, supports_linux, supports_macos, supports_windows)?; Self::validate_path_permissions(&permissions.filesystem.write, supports_linux, supports_macos, supports_windows)?; - Self::validate_string_permissions(&permissions.exec.command)?; + Self::validate_command_permissions(&permissions.exec.command)?; Self::validate_path_permissions(&permissions.exec.executable, supports_linux, supports_macos, supports_windows)?; // even though system accepts a list of predefined values @@ -546,6 +546,18 @@ impl PluginLoader { Ok(()) } + fn validate_command_permissions(values: &[String]) -> anyhow::Result<()> { + Self::validate_string_permissions(values)?; + + for value in values { + if value.contains("/") || value.contains("\\") { + Err(anyhow!("Command permissions value cannot be a path"))? + } + } + + Ok(()) + } + fn validate_network_permissions(values: &[String]) -> anyhow::Result<()> { for value in values { if value.is_empty() { From 9604cb85f17cc4da7a295f06f8fb9b4054918136 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 24 Dec 2024 17:32:51 +0100 Subject: [PATCH 241/540] Improve rejected promise error log --- js/core/src/init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/core/src/init.ts b/js/core/src/init.ts index 4f6cfd4..4dfbad5 100644 --- a/js/core/src/init.ts +++ b/js/core/src/init.ts @@ -2,7 +2,7 @@ import { runPluginLoop } from "gauntlet:core"; globalThis.addEventListener("unhandledrejection", (event) => { event.preventDefault() - console.error("Rejected promise", event); + console.error("Rejected promise, reason:", event.reason); }); (async () => { From d01ab6bedc8e9865c2535f7929c680ee22ff349a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 24 Dec 2024 19:01:45 +0100 Subject: [PATCH 242/540] Rename Image type to ImageLike to avoid conflict with Image component --- js/api/src/gen/components.tsx | 22 +++++++++++----------- rust/component_model/src/lib.rs | 12 ++++++------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index 20ddebd..d8ef3b6 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -42,7 +42,7 @@ declare global { children?: ElementComponent; }; ["gauntlet:image"]: { - source: Image; + source: ImageLike; }; ["gauntlet:h1"]: { children?: StringComponent; @@ -121,15 +121,15 @@ declare global { ["gauntlet:empty_view"]: { title: string; description?: string; - image?: Image; + image?: ImageLike; }; ["gauntlet:accessory_icon"]: { - icon: Image; + icon: ImageLike; tooltip?: string; }; ["gauntlet:accessory_text"]: { text: string; - icon?: Image; + icon?: ImageLike; tooltip?: string; }; ["gauntlet:search_bar"]: { @@ -141,7 +141,7 @@ declare global { children?: ElementComponent; title: string; subtitle?: string; - icon?: Image; + icon?: ImageLike; onClick?: () => void; }; ["gauntlet:list_section"]: { @@ -360,7 +360,7 @@ export type ImageSourceAsset = { asset: string; }; export type ImageSource = ImageSourceUrl | ImageSourceAsset; -export type Image = ImageSource | Icons; +export type ImageLike = ImageSource | Icons; export interface ActionProps { id?: string; label: string; @@ -451,7 +451,7 @@ Metadata.Value = MetadataValue; Metadata.Icon = MetadataIcon; Metadata.Separator = MetadataSeparator; export interface ImageProps { - source: Image; + source: ImageLike; } export const Image: FC = (props: ImageProps): ReactNode => { return ; @@ -648,13 +648,13 @@ Inline.Center = Content; export interface EmptyViewProps { title: string; description?: string; - image?: Image; + image?: ImageLike; } export const EmptyView: FC = (props: EmptyViewProps): ReactNode => { return ; }; export interface IconAccessoryProps { - icon: Image; + icon: ImageLike; tooltip?: string; } export const IconAccessory: FC = (props: IconAccessoryProps): ReactNode => { @@ -662,7 +662,7 @@ export const IconAccessory: FC = (props: IconAccessoryProps) }; export interface TextAccessoryProps { text: string; - icon?: Image; + icon?: ImageLike; tooltip?: string; } export const TextAccessory: FC = (props: TextAccessoryProps): ReactNode => { @@ -679,7 +679,7 @@ export const SearchBar: FC = (props: SearchBarProps): ReactNode export interface ListItemProps { title: string; subtitle?: string; - icon?: Image; + icon?: ImageLike; accessories?: (ElementComponent | ElementComponent)[]; onClick?: () => void; } diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index 44c5f3c..53e14e1 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -558,7 +558,7 @@ fn root(children: &[&Component]) -> Component { }, ] }), - ("Image".to_owned(), SharedType::Union { + ("ImageLike".to_owned(), SharedType::Union { items: vec![ PropertyType::SharedTypeRef { name: "ImageSource".to_owned() @@ -738,7 +738,7 @@ pub fn create_component_model() -> Vec { mark_doc!("/image/description.md"), "Image", [ - property("source", mark_doc!("/image/props/source.md"), false, PropertyType::SharedTypeRef { name: "Image".to_owned() }) + property("source", mark_doc!("/image/props/source.md"), false, PropertyType::SharedTypeRef { name: "ImageLike".to_owned() }) ], children_none(), ); @@ -1035,7 +1035,7 @@ pub fn create_component_model() -> Vec { [ property("title", mark_doc!("/empty_view/props/title.md"),false, PropertyType::String), property("description", mark_doc!("/empty_view/props/description.md"),true, PropertyType::String), - property("image", mark_doc!("/empty_view/props/image.md"),true, PropertyType::SharedTypeRef { name: "Image".to_owned() }), + property("image", mark_doc!("/empty_view/props/image.md"),true, PropertyType::SharedTypeRef { name: "ImageLike".to_owned() }), ], children_none(), ); @@ -1046,7 +1046,7 @@ pub fn create_component_model() -> Vec { "TextAccessory", [ property("text", mark_doc!("/accessory_text/props/text.md"),false, PropertyType::String), - property("icon", mark_doc!("/accessory_text/props/icon.md"),true, PropertyType::SharedTypeRef { name: "Image".to_owned() }), + property("icon", mark_doc!("/accessory_text/props/icon.md"),true, PropertyType::SharedTypeRef { name: "ImageLike".to_owned() }), property("tooltip", mark_doc!("/accessory_text/props/tooltip.md"),true, PropertyType::String), ], children_none(), @@ -1057,7 +1057,7 @@ pub fn create_component_model() -> Vec { mark_doc!("/accessory_icon/description.md"), "IconAccessory", [ - property("icon", mark_doc!("/accessory_icon/props/icon.md"),false, PropertyType::SharedTypeRef { name: "Image".to_owned() }), + property("icon", mark_doc!("/accessory_icon/props/icon.md"),false, PropertyType::SharedTypeRef { name: "ImageLike".to_owned() }), property("tooltip", mark_doc!("/accessory_icon/props/tooltip.md"),true, PropertyType::String), ], children_none(), @@ -1084,7 +1084,7 @@ pub fn create_component_model() -> Vec { [ property("title", mark_doc!("/list_item/props/title.md"),false, PropertyType::String), property("subtitle", mark_doc!("/list_item/props/subtitle.md"),true, PropertyType::String), - property("icon", mark_doc!("/list_item/props/icon.md"),true, PropertyType::SharedTypeRef { name: "Image".to_owned() }), + property("icon", mark_doc!("/list_item/props/icon.md"),true, PropertyType::SharedTypeRef { name: "ImageLike".to_owned() }), property("accessories", mark_doc!("/list_item/props/accessories.md"),true, PropertyType::Array { item: Box::new(PropertyType::Union { items: vec![component_ref(&accessory_text_component, Arity::ZeroOrMore), component_ref(&accessory_icon_component, Arity::ZeroOrMore)]}) }), event("onClick", mark_doc!("/list_item/props/onClick.md"), true, []) ], From 63a54d1d18569780cf340c403dc861c54055e05b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 24 Dec 2024 19:14:50 +0100 Subject: [PATCH 243/540] Fix build after renaming Image into ImageLike --- rust/client/src/ui/widget.rs | 8 ++++---- rust/common/src/model.rs | 2 +- rust/plugin_runtime/src/ui.rs | 2 +- rust/server/src/plugins/image_gatherer.rs | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index faa6b21..92b947a 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -15,7 +15,7 @@ use crate::ui::theme::text_input::TextInputStyle; use crate::ui::theme::tooltip::TooltipStyle; use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::AppMsg; -use gauntlet_common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Icons, Image, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiWidgetId}; +use gauntlet_common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Icons, ImageLike, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiWidgetId}; use gauntlet_common_ui::shortcut_to_text; use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; @@ -2061,9 +2061,9 @@ impl<'b> ComponentWidgets<'b> { } } - fn render_image<'a>(&self, widget_id: UiWidgetId, image_data: &Image, icon_style: Option) -> Element<'a, ComponentWidgetEvent> { + fn render_image<'a>(&self, widget_id: UiWidgetId, image_data: &ImageLike, icon_style: Option) -> Element<'a, ComponentWidgetEvent> { match image_data { - Image::ImageSource(_) => { + ImageLike::ImageSource(_) => { match self.images.get(&widget_id) { Some(bytes) => { image(Handle::from_bytes(bytes.clone())) @@ -2075,7 +2075,7 @@ impl<'b> ComponentWidgets<'b> { } } } - Image::Icons(icon) => { + ImageLike::Icons(icon) => { match icon_style { None => { value(icon_to_bootstrap(icon)) diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 6430c5b..a123cc6 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -323,7 +323,7 @@ pub trait WidgetVisitor { } } - async fn image(&mut self, _widget_id: UiWidgetId, _widget: &Image) { + async fn image(&mut self, _widget_id: UiWidgetId, _widget: &ImageLike) { } diff --git a/rust/plugin_runtime/src/ui.rs b/rust/plugin_runtime/src/ui.rs index f720b1c..8a42f91 100644 --- a/rust/plugin_runtime/src/ui.rs +++ b/rust/plugin_runtime/src/ui.rs @@ -9,7 +9,7 @@ use indexmap::IndexMap; use serde::{de, Deserialize, Deserializer, Serialize}; use serde::de::Error; use tokio::runtime::Handle; -use gauntlet_common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, EntrypointId, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Image, ImageSource, ImageSourceAsset, ImageSourceUrl, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectItemWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiPropertyValue, UiRenderLocation, UiWidgetId, WidgetVisitor}; +use gauntlet_common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, EntrypointId, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, ImageLike, ImageSource, ImageSourceAsset, ImageSourceUrl, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectItemWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiPropertyValue, UiRenderLocation, UiWidgetId, WidgetVisitor}; use gauntlet_component_model::{Component, Property, PropertyType, SharedType}; use gauntlet_component_model::Component::Root; use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; diff --git a/rust/server/src/plugins/image_gatherer.rs b/rust/server/src/plugins/image_gatherer.rs index 88917f1..c8eaceb 100644 --- a/rust/server/src/plugins/image_gatherer.rs +++ b/rust/server/src/plugins/image_gatherer.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use gauntlet_common::model::{Image, ImageSource, ImageSourceAsset, ImageSourceUrl, RootWidget, UiWidgetId, WidgetVisitor}; +use gauntlet_common::model::{ImageLike, ImageSource, ImageSourceAsset, ImageSourceUrl, RootWidget, UiWidgetId, WidgetVisitor}; use gauntlet_plugin_runtime::BackendForPluginRuntimeApi; use crate::plugins::js::BackendForPluginRuntimeApiImpl; use futures::StreamExt; @@ -11,8 +11,8 @@ pub struct ImageGatherer<'a> { } impl<'a> WidgetVisitor for ImageGatherer<'a> { - async fn image(&mut self, widget_id: UiWidgetId, widget: &Image) { - if let Image::ImageSource(image_source) = &widget { + async fn image(&mut self, widget_id: UiWidgetId, widget: &ImageLike) { + if let ImageLike::ImageSource(image_source) = &widget { self.image_sources.insert(widget_id, get_image_date(&self.api, image_source).await); } } From 7fb6ca4074a1b2dd66e4d64213e672c703f92902 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 24 Dec 2024 21:56:38 +0100 Subject: [PATCH 244/540] Implement generated command accessories --- dev_plugin/src/command-generator.ts | 11 +- dev_plugin/src/list-view.tsx | 1 + js/api/src/helpers.ts | 14 ++ js/core/src/command-generator.ts | 22 ++- js/typings/index.d.ts | 16 ++- rust/client/src/ui/mod.rs | 7 +- rust/client/src/ui/search_list.rs | 207 +++++++++++++--------------- rust/client/src/ui/widget.rs | 201 +++++++++++++-------------- rust/common/build.rs | 2 +- rust/common/src/model.rs | 14 ++ rust/plugin_runtime/src/model.rs | 17 ++- rust/server/src/plugins/js.rs | 26 +++- rust/server/src/search.rs | 22 ++- 13 files changed, 322 insertions(+), 238 deletions(-) diff --git a/dev_plugin/src/command-generator.ts b/dev_plugin/src/command-generator.ts index 91579d1..73395e2 100644 --- a/dev_plugin/src/command-generator.ts +++ b/dev_plugin/src/command-generator.ts @@ -1,6 +1,7 @@ import { GeneratorProps, showHud } from "@project-gauntlet/api/helpers"; +import { Icons } from "@project-gauntlet/api/components"; -export default function CommandGenerator({ add, remove: _ }: GeneratorProps): void { +export default function CommandGenerator({ add, remove: _, updateAccessories }: GeneratorProps): void { add('generated-test-1', { name: 'Generated Item 1', fn: () => { @@ -52,6 +53,12 @@ export default function CommandGenerator({ add, remove: _ }: GeneratorProps): vo name: 'Generated Item 4', fn: () => { console.log('generated-test-4') - } + }, }) + + updateAccessories('generated-test-4', [ + { + text: "1 window open" + }, + ]) } diff --git a/dev_plugin/src/list-view.tsx b/dev_plugin/src/list-view.tsx index 91173c5..9259710 100644 --- a/dev_plugin/src/list-view.tsx +++ b/dev_plugin/src/list-view.tsx @@ -20,6 +20,7 @@ export default function ListView(): ReactElement { value={searchText} onChange={setSearchText} /> + { numbers.map(value => { const title = "Title " + value; diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index 4db3d2b..b89ac21 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -45,9 +45,23 @@ export interface GeneratedCommandAction { fn: () => void } +export type GeneratedCommandAccessory = GeneratedCommandTextAccessory | GeneratedCommandIconAccessory; + +export interface GeneratedCommandTextAccessory { + text: string + icon?: string + tooltip?: string +} + +export interface GeneratedCommandIconAccessory { + icon: string + tooltip?: string +} + export type GeneratorProps = { add: (id: string, data: GeneratedCommand) => void, remove: (id: string) => void, + updateAccessories: (id: string, accessories: GeneratedCommandAccessory[] | undefined) => void, }; export const Clipboard: Clipboard = { diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index 239292e..93e6468 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -22,11 +22,12 @@ interface GeneratedCommandAction { type GeneratorProps = { add: (id: string, data: GeneratedCommand) => void, remove: (id: string) => void, + updateAccessories: (id: string, accessories: GeneratedCommandAccessory[] | undefined) => void, }; type Generator = (props: GeneratorProps) => void | (() => (void | Promise)) | Promise (void | Promise))> -type ProcessedGeneratedCommand = { generatorEntrypointId: string, uuid: string, command: GeneratedCommand }; +type ProcessedGeneratedCommand = { generatorEntrypointId: string, uuid: string, command: GeneratedCommand, accessories?: GeneratedCommandAccessory[] }; type ProcessedGeneratedCommands = { [lookupEntrypointId: string]: ProcessedGeneratedCommand }; type GeneratorCleanups = { [generatorEntrypointId: string]: () => (void | Promise) }; @@ -63,7 +64,8 @@ export async function runCommandGenerators(): Promise { storedGeneratedCommands[lookupId] = { generatorEntrypointId: generatorEntrypointId, uuid: crypto.randomUUID(), - command: data + command: data, + accessories: [], } reloadSearchIndex(true) @@ -77,11 +79,24 @@ export async function runCommandGenerators(): Promise { reloadSearchIndex(true) } + const updateAccessories = (id: string, accessories: GeneratedCommandAccessory[] | undefined): void => { + op_log_info("command_generator", `Updating accessories for entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) + const lookupId = generatorEntrypointId + ":" + id; + + const generatedCommand = storedGeneratedCommands[lookupId]; + if (generatedCommand) { + generatedCommand.accessories = accessories + reloadSearchIndex(true) + } else { + console.error(`Unable to update accessories for entry '${id}' because it doesn't exist`) + } + } + // noinspection ES6MissingAwait (async () => { try { update_loading_bar(generatorEntrypointId, true) - let cleanup = await generator({ add, remove }) + let cleanup = await generator({ add, remove, updateAccessories }) update_loading_bar(generatorEntrypointId, false) if (typeof cleanup === "function") { generatorCleanups[generatorEntrypointId] = cleanup @@ -108,6 +123,7 @@ export function generatedCommandSearchIndex(): AdditionalSearchItem[] { id: action.ref, label: action.label })), + entrypoint_accessories: value.accessories || [] })) } diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 7b664b0..20013b6 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -37,8 +37,6 @@ type MacOSDesktopSettings13AndPostData = { icon: ArrayBuffer | undefined, } -type PromiseRejectCallback = (type: number, promise: Promise, reason: any) => void; - type PluginEvent = ViewEvent | NotReactsKeyboardEvent | RunCommand | RunGeneratedCommand | OpenView | CloseView | OpenInlineView | ReloadSearchIndex | RefreshSearchIndex type RenderLocation = "InlineView" | "View" @@ -112,12 +110,26 @@ type UiWidget = { type Props = { [key: string]: any }; type PropsWithChildren = { children?: UiWidget[] } & Props; +type GeneratedCommandAccessory = GeneratedCommandTextAccessory | GeneratedCommandIconAccessory; + +interface GeneratedCommandTextAccessory { + text: string + icon?: string + tooltip?: string +} + +interface GeneratedCommandIconAccessory { + icon: string + tooltip?: string +} + type AdditionalSearchItem = { entrypoint_name: string, entrypoint_id: string, entrypoint_uuid: string, entrypoint_icon: ArrayBuffer | undefined, entrypoint_actions: AdditionalSearchItemAction[], + entrypoint_accessories: GeneratedCommandAccessory[], } type AdditionalSearchItemAction = { diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index a09c493..de573d5 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -1599,11 +1599,8 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { .width(Length::Fill) .themed(TextInputStyle::MainSearch); - let search_list = search_list( - &state.search_results, - &focused_search_result, - |search_result| AppMsg::RunSearchItemAction(search_result, None), - ); + let search_list = search_list(&state.search_results, &focused_search_result) + .map(|search_result| AppMsg::RunSearchItemAction(search_result, None)); let search_list = container(search_list) .width(Length::Fill) diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index cc33100..ad0b4c6 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -1,139 +1,122 @@ -use iced::{Alignment, Length}; -use iced::advanced::image::Handle; -use iced::widget::{column, Component, container, horizontal_space}; -use iced::widget::button; -use iced::widget::component; -use iced::widget::row; -use iced::widget::text; -use iced::widget::text::Shaping; -use gauntlet_common::model::SearchResult; use crate::ui::scroll_handle::ScrollHandle; -use crate::ui::theme::{Element, GauntletComplexTheme, ThemableWidget}; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; use crate::ui::theme::image::ImageStyle; use crate::ui::theme::space::ThemeKindSpace; use crate::ui::theme::text::TextStyle; +use crate::ui::theme::{Element, ThemableWidget}; +use crate::ui::widget::{render_icon_accessory, render_text_accessory}; +use std::collections::HashMap; -pub struct SearchList<'a, Message> { - on_select: Box Message>, - focused_search_result: Option, - search_results: &'a[SearchResult], -} +use gauntlet_common::model::{IconAccessoryWidget, ImageLike, SearchResult, SearchResultAccessory, TextAccessoryWidget}; +use iced::advanced::image::Handle; +use iced::widget::button; +use iced::widget::row; +use iced::widget::text; +use iced::widget::text::Shaping; +use iced::widget::{column, container, horizontal_space}; +use iced::{Alignment, Length}; -pub fn search_list<'a, Message>( - search_results: &'a[SearchResult], +pub fn search_list<'a>( + search_results: &'a [SearchResult], focused_search_result: &ScrollHandle, - on_select: impl Fn(SearchResult) -> Message + 'static, -) -> SearchList<'a, Message> { - SearchList::new(search_results, focused_search_result.index, on_select) -} +) -> Element<'a, SearchResult> { + let items: Vec> = search_results + .iter() + .enumerate() + .map(|(index, search_result)| { + let main_text: Element<_> = text(&search_result.entrypoint_name) + .shaping(Shaping::Advanced) + .into(); + let main_text: Element<_> = container(main_text) + .themed(ContainerStyle::MainListItemText); -#[derive(Debug, Clone)] -pub struct SelectItemEvent(SearchResult); + let spacer: Element<_> = horizontal_space() + .width(Length::Fill) + .into(); -impl<'a, Message> SearchList<'a, Message> { - pub fn new( - search_results: &'a[SearchResult], - focused_search_result: Option, - on_open_view: impl Fn(SearchResult) -> Message + 'static, - ) -> Self { - Self { - search_results, - focused_search_result, - on_select: Box::new(on_open_view), - } - } -} - -impl<'a, Message> Component for SearchList<'a, Message> { - type State = (); - type Event = SelectItemEvent; - - fn update( - &mut self, - _state: &mut Self::State, - SelectItemEvent(event): SelectItemEvent, - ) -> Option { - Some((self.on_select)(event)) - } - - fn view(&self, _state: &Self::State) -> Element { - let items: Vec> = self.search_results - .iter() - .enumerate() - .map(|(index, search_result)| { - let main_text: Element<_> = text(&search_result.entrypoint_name) - .shaping(Shaping::Advanced) - .into(); - let main_text: Element<_> = container(main_text) - .themed(ContainerStyle::MainListItemText); - - let spacer: Element<_> = horizontal_space() - .width(Length::Fill) - .into(); - - let sub_text: Element<_> = text(&search_result.plugin_name) - .shaping(Shaping::Advanced) - .themed(TextStyle::MainListItemSubtext); - let sub_text: Element<_> = container(sub_text) + let sub_text: Element<_> = text(&search_result.plugin_name) + .shaping(Shaping::Advanced) + .themed(TextStyle::MainListItemSubtext); + let sub_text: Element<_> = container(sub_text) .themed(ContainerStyle::MainListItemSubText); // FIXME find a way to set padding based on whether the scroll bar is visible - let mut button_content = vec![]; + let mut button_content = vec![]; - if let Some(path) = &search_result.entrypoint_icon { - let image: Element<_> = iced::widget::image(Handle::from_path(path)) - .themed(ImageStyle::MainListItemIcon); + if let Some(path) = &search_result.entrypoint_icon { + let image: Element<_> = iced::widget::image(Handle::from_path(path)) + .themed(ImageStyle::MainListItemIcon); - let image: Element<_> = container(image) - .themed(ContainerStyle::MainListItemIcon); + let image: Element<_> = container(image) + .themed(ContainerStyle::MainListItemIcon); - button_content.push(image); - } else { - let spacer: Element<_> = horizontal_space() // TODO replace with grayed out gauntlet icon + button_content.push(image); + } else { + let spacer: Element<_> = horizontal_space() // TODO replace with grayed out gauntlet icon .themed(ThemeKindSpace::MainListItemIcon); - let spacer: Element<_> = container(spacer) - .themed(ContainerStyle::MainListItemIcon); + let spacer: Element<_> = container(spacer) + .themed(ContainerStyle::MainListItemIcon); - button_content.push(spacer); - } - - button_content.push(main_text); button_content.push(spacer); - button_content.push(sub_text); + } - let button_content: Element<_> = row(button_content) - .align_y(Alignment::Center) + button_content.push(main_text); + button_content.push(spacer); + + if search_result.entrypoint_accessories.len() > 0 { + let accessories: Vec> = search_result.entrypoint_accessories + .iter() + .map(|accessory| { + match accessory { + SearchResultAccessory::TextAccessory { text, icon, tooltip } => { + render_text_accessory(&HashMap::new(), &TextAccessoryWidget { + __id__: 0, + text: text.clone(), + icon: icon.as_ref().map(|icon| ImageLike::Icons(icon.clone())), + tooltip: tooltip.clone(), + }) + }, + SearchResultAccessory::IconAccessory { icon, tooltip } => { + render_icon_accessory(&HashMap::new(), &IconAccessoryWidget { + __id__: 0, + icon: ImageLike::Icons(icon.clone()), + tooltip: tooltip.clone(), + }) + } + } + }) + .collect(); + + let accessories: Element<_> = row(accessories) .into(); - let style = match self.focused_search_result { - None => ButtonStyle::MainListItem, - Some(focused_index) => { - if focused_index == index { - ButtonStyle::MainListItemFocused - } else { - ButtonStyle::MainListItem - } + button_content.push(accessories); + } + + button_content.push(sub_text); + + let button_content: Element<_> = row(button_content) + .align_y(Alignment::Center) + .into(); + + let style = match focused_search_result.index { + None => ButtonStyle::MainListItem, + Some(focused_index) => { + if focused_index == index { + ButtonStyle::MainListItemFocused + } else { + ButtonStyle::MainListItem } - }; + } + }; - button(button_content) - .width(Length::Fill) - .on_press(SelectItemEvent(search_result.clone())) - .themed(style) - }) - .collect(); + button(button_content) + .width(Length::Fill) + .on_press(search_result.clone()) + .themed(style) + }) + .collect(); - column(items).into() - } + column(items).into() } - -impl<'a, Message> From> for Element<'a, Message> - where - Message: 'a, -{ - fn from(search_list: SearchList<'a, Message>) -> Self { - component(search_list) - } -} \ No newline at end of file diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 92b947a..5f26629 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1000,7 +1000,7 @@ impl<'b> ComponentWidgets<'b> { fn render_image_widget<'a>(&self, widget: &ImageWidget, centered: bool) -> Element<'a, ComponentWidgetEvent> { // TODO image size, height and width - let content: Element<_> = self.render_image(widget.__id__, &widget.source, None); + let content: Element<_> = render_image(self.images, widget.__id__, &widget.source, None); let mut content = container(content) .width(Length::Fill); @@ -1412,7 +1412,7 @@ impl<'b> ComponentWidgets<'b> { fn render_empty_view_widget<'a>(&self, widget: &EmptyViewWidget) -> Element<'a, ComponentWidgetEvent> { let image: Option> = widget.image .as_ref() - .map(|image| self.render_image(widget.__id__, image, Some(TextStyle::EmptyViewSubtitle))); + .map(|image| render_image(self.images, widget.__id__, image, Some(TextStyle::EmptyViewSubtitle))); let title: Element<_> = text(widget.title.to_string()) .shaping(Shaping::Advanced) @@ -1459,69 +1459,6 @@ impl<'b> ComponentWidgets<'b> { .themed(TextInputStyle::PluginSearchBar) } - fn render_icon_accessory<'a>(&self, widget: &IconAccessoryWidget) -> Element<'a, ComponentWidgetEvent> { - let icon = self.render_image(widget.__id__, &widget.icon, Some(TextStyle::IconAccessory)); - - let content = container(icon) - .align_x(Horizontal::Center) - .align_y(Vertical::Center) - .themed(ContainerStyle::IconAccessory); - - match widget.tooltip.as_ref() { - None => content, - Some(tooltip_text) => { - let tooltip_text: Element<_> = text(tooltip_text.to_string()) - .shaping(Shaping::Advanced) - .into(); - - tooltip(content, tooltip_text, Position::Top) - .themed(TooltipStyle::Tooltip) - } - } - } - - fn render_text_accessory<'a>(&self, widget: &TextAccessoryWidget) -> Element<'a, ComponentWidgetEvent> { - let icon: Option> = widget.icon - .as_ref() - .map(|icon| self.render_image(widget.__id__, icon, Some(TextStyle::TextAccessory))); - - let text_content: Element<_> = text(widget.text.to_string()) - .shaping(Shaping::Advanced) - .themed(TextStyle::TextAccessory); - - let mut content: Vec> = vec![]; - - if let Some(icon) = icon { - let icon: Element<_> = container(icon) - .themed(ContainerStyle::TextAccessoryIcon); - - content.push(icon) - } - - content.push(text_content); - - let content: Element<_> = row(content) - .align_y(Alignment::Center) - .into(); - - let content = container(content) - .align_x(Horizontal::Center) - .align_y(Vertical::Center) - .themed(ContainerStyle::TextAccessory); - - match widget.tooltip.as_ref() { - None => content, - Some(tooltip_text) => { - let tooltip_text: Element<_> = text(tooltip_text.to_string()) - .shaping(Shaping::Advanced) - .into(); - - tooltip(content, tooltip_text, Position::Top) - .themed(TooltipStyle::Tooltip) - } - } - } - fn render_list_widget<'a>( &self, list_widget: &ListWidget, @@ -1669,7 +1606,7 @@ impl<'b> ComponentWidgets<'b> { ) -> Element<'a, ComponentWidgetEvent> { let icon: Option> = widget.icon .as_ref() - .map(|icon| self.render_image(widget.__id__, icon, None)); + .map(|icon| render_image(self.images, widget.__id__, icon, None)); let title: Element<_> = text(widget.title.to_string()) .shaping(Shaping::Advanced) @@ -1701,8 +1638,8 @@ impl<'b> ComponentWidgets<'b> { .iter() .map(|accessory| { match accessory { - ListItemAccessories::_0(widget) => self.render_text_accessory(widget), - ListItemAccessories::_1(widget) => self.render_icon_accessory(widget) + ListItemAccessories::_0(widget) => render_text_accessory(self.images, widget), + ListItemAccessories::_1(widget) => render_icon_accessory(self.images, widget) } }) .collect(); @@ -1894,7 +1831,7 @@ impl<'b> ComponentWidgets<'b> { let mut sub_content_right = vec![]; if let Some(widget) = &widget.content.accessory { - sub_content_right.push(self.render_icon_accessory(widget)); + sub_content_right.push(render_icon_accessory(self.images, widget)); } let sub_content_left: Element<_> = column(sub_content_left) @@ -2060,37 +1997,6 @@ impl<'b> ComponentWidgets<'b> { } } } - - fn render_image<'a>(&self, widget_id: UiWidgetId, image_data: &ImageLike, icon_style: Option) -> Element<'a, ComponentWidgetEvent> { - match image_data { - ImageLike::ImageSource(_) => { - match self.images.get(&widget_id) { - Some(bytes) => { - image(Handle::from_bytes(bytes.clone())) - .into() - } - None => { - horizontal_space() - .into() - } - } - } - ImageLike::Icons(icon) => { - match icon_style { - None => { - value(icon_to_bootstrap(icon)) - .font(BOOTSTRAP_FONT) - .into() - } - Some(icon_style) => { - value(icon_to_bootstrap(icon)) - .font(BOOTSTRAP_FONT) - .themed(icon_style) - } - } - } - } - } } @@ -2620,6 +2526,101 @@ fn render_shortcut<'a, T: 'a>(shortcut: &PhysicalShortcut) -> Element<'a, T> { .themed(RowStyle::ActionShortcut) } +fn render_image<'a, T: 'a + Clone>(images: &HashMap>, widget_id: UiWidgetId, image_data: &ImageLike, icon_style: Option) -> Element<'a, T> { + match image_data { + ImageLike::ImageSource(_) => { + match images.get(&widget_id) { + Some(bytes) => { + image(Handle::from_bytes(bytes.clone())) + .into() + } + None => { + horizontal_space() + .into() + } + } + } + ImageLike::Icons(icon) => { + match icon_style { + None => { + value(icon_to_bootstrap(icon)) + .font(BOOTSTRAP_FONT) + .into() + } + Some(icon_style) => { + value(icon_to_bootstrap(icon)) + .font(BOOTSTRAP_FONT) + .themed(icon_style) + } + } + } + } +} + +pub fn render_icon_accessory<'a, T: 'a + Clone>(images: &HashMap>, widget: &IconAccessoryWidget) -> Element<'a, T> { + let icon = render_image(images, widget.__id__, &widget.icon, Some(TextStyle::IconAccessory)); + + let content = container(icon) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .themed(ContainerStyle::IconAccessory); + + match widget.tooltip.as_ref() { + None => content, + Some(tooltip_text) => { + let tooltip_text: Element<_> = text(tooltip_text.to_string()) + .shaping(Shaping::Advanced) + .into(); + + tooltip(content, tooltip_text, Position::Top) + .themed(TooltipStyle::Tooltip) + } + } +} + +pub fn render_text_accessory<'a, T: 'a + Clone>(images: &HashMap>, widget: &TextAccessoryWidget) -> Element<'a, T> { + let icon: Option> = widget.icon + .as_ref() + .map(|icon| render_image(images, widget.__id__, icon, Some(TextStyle::TextAccessory))); + + let text_content: Element<_> = text(widget.text.to_string()) + .shaping(Shaping::Advanced) + .themed(TextStyle::TextAccessory); + + let mut content: Vec> = vec![]; + + if let Some(icon) = icon { + let icon: Element<_> = container(icon) + .themed(ContainerStyle::TextAccessoryIcon); + + content.push(icon) + } + + content.push(text_content); + + let content: Element<_> = row(content) + .align_y(Alignment::Center) + .into(); + + let content = container(content) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .themed(ContainerStyle::TextAccessory); + + match widget.tooltip.as_ref() { + None => content, + Some(tooltip_text) => { + let tooltip_text: Element<_> = text(tooltip_text.to_string()) + .shaping(Shaping::Advanced) + .into(); + + tooltip(content, tooltip_text, Position::Top) + .themed(TooltipStyle::Tooltip) + } + } +} + + #[derive(Clone, Debug)] pub enum ComponentWidgetEvent { LinkClick { diff --git a/rust/common/build.rs b/rust/common/build.rs index 5ab61c5..eb94ff8 100644 --- a/rust/common/build.rs +++ b/rust/common/build.rs @@ -501,7 +501,7 @@ fn component_model_generator() -> Result<(), Box> { for (type_name, shared_type) in shared_types { match shared_type { SharedType::Enum { items } => { - output.push_str("#[derive(Debug, Serialize, Deserialize, Encode, Decode)]\n"); + output.push_str("#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]\n"); output.push_str(&format!("pub enum {} {{\n", type_name)); for item in items { diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index a123cc6..c90b725 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -113,6 +113,20 @@ pub struct SearchResult { pub entrypoint_icon: Option, pub entrypoint_type: SearchResultEntrypointType, pub entrypoint_actions: Vec, + pub entrypoint_accessories: Vec, +} + +#[derive(Debug, Clone)] +pub enum SearchResultAccessory { + TextAccessory { + text: String, + icon: Option, + tooltip: Option + }, + IconAccessory { + icon: Icons, + tooltip: Option + }, } #[derive(Debug, Clone)] diff --git a/rust/plugin_runtime/src/model.rs b/rust/plugin_runtime/src/model.rs index 5acdde4..816e257 100644 --- a/rust/plugin_runtime/src/model.rs +++ b/rust/plugin_runtime/src/model.rs @@ -1,5 +1,5 @@ use crate::JsEvent; -use gauntlet_common::model::{EntrypointId, PluginId, RootWidget}; +use gauntlet_common::model::{EntrypointId, Icons, PluginId, RootWidget}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt; @@ -172,6 +172,7 @@ pub struct JsAdditionalSearchItem { pub entrypoint_uuid: String, pub entrypoint_icon: Option>, pub entrypoint_actions: Vec, + pub entrypoint_accessories: Vec, } impl fmt::Debug for JsAdditionalSearchItem { @@ -203,6 +204,20 @@ pub enum JsPreferenceUserData { ListOfNumbers(Vec), } +#[derive(Debug, Deserialize, Serialize, Encode, Decode)] +#[serde(untagged)] +pub enum JsAdditionalSearchItemAccessory { + TextAccessory { + text: String, + icon: Option, + tooltip: Option + }, + IconAccessory { + icon: Icons, + tooltip: Option + }, +} + #[derive(Debug, Serialize, Deserialize, Encode, Decode)] pub struct JsClipboardData { pub text_data: Option, diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 045dd51..c3a42f6 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -26,10 +26,10 @@ use tokio::sync::Mutex; use tokio::task::spawn_blocking; use tokio_util::sync::CancellationToken; use gauntlet_common::dirs::Dirs; -use gauntlet_common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; +use gauntlet_common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, SearchResultAccessory, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; use gauntlet_common::rpc::frontend_api::FrontendApi; use gauntlet_common::settings_env_data_to_string; -use gauntlet_plugin_runtime::{recv_message, send_message, BackendForPluginRuntimeApi, JsAdditionalSearchItem, JsClipboardData, JsInit, JsKeyboardEventOrigin, JsPluginCode, JsPluginPermissions, JsPreferenceUserData, JsEvent, JsUiPropertyValue, JsRequest, JsUiRenderLocation, JsResponse, JsMessage, JsPluginPermissionsFileSystem, JsPluginPermissionsExec, JsPluginPermissionsMainSearchBar, JsMessageSide, JsPluginRuntimeMessage}; +use gauntlet_plugin_runtime::{recv_message, send_message, BackendForPluginRuntimeApi, JsAdditionalSearchItem, JsClipboardData, JsInit, JsKeyboardEventOrigin, JsPluginCode, JsPluginPermissions, JsPreferenceUserData, JsEvent, JsUiPropertyValue, JsRequest, JsUiRenderLocation, JsResponse, JsMessage, JsPluginPermissionsFileSystem, JsPluginPermissionsExec, JsPluginPermissionsMainSearchBar, JsMessageSide, JsPluginRuntimeMessage, JsAdditionalSearchItemAccessory}; use crate::model::{IntermediateUiEvent}; use crate::plugins::clipboard::Clipboard; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; @@ -725,7 +725,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { shortcuts.insert(id.clone(), entrypoint_shortcuts); } - let mut plugins_search_items = generated_commands.into_iter() + let mut generated_search_items = generated_commands.into_iter() .map(|item| { let entrypoint_icon_path = match item.entrypoint_icon { None => None, @@ -753,6 +753,19 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { }) .collect(); + let entrypoint_accessories = item.entrypoint_accessories.into_iter() + .map(|accessory| { + match accessory { + JsAdditionalSearchItemAccessory::TextAccessory { text, icon, tooltip } => { + SearchResultAccessory::TextAccessory { text, icon, tooltip } + } + JsAdditionalSearchItemAccessory::IconAccessory { icon, tooltip } => { + SearchResultAccessory::IconAccessory { icon, tooltip } + } + } + }) + .collect(); + Ok(SearchIndexItem { entrypoint_type: SearchResultEntrypointType::GeneratedCommand, entrypoint_id: EntrypointId::from_string(item.entrypoint_id), @@ -760,6 +773,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { entrypoint_icon_path, entrypoint_frecency, entrypoint_actions, + entrypoint_accessories, }) }) .collect::>>()?; @@ -806,6 +820,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { entrypoint_icon_path, entrypoint_frecency, entrypoint_actions: vec![], + entrypoint_accessories: vec![], })) }, DbPluginEntrypointType::View => { @@ -816,6 +831,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { entrypoint_icon_path, entrypoint_frecency, entrypoint_actions: vec![], + entrypoint_accessories: vec![], })) }, DbPluginEntrypointType::CommandGenerator | DbPluginEntrypointType::InlineView => { @@ -828,9 +844,9 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { .flat_map(|item| item) .collect::>(); - plugins_search_items.append(&mut builtin_search_items); + generated_search_items.append(&mut builtin_search_items); - self.search_index.save_for_plugin(self.plugin_id.clone(), name, plugins_search_items, refresh_search_list) + self.search_index.save_for_plugin(self.plugin_id.clone(), name, generated_search_items, refresh_search_list) .context("error when updating search index")?; Ok(()) diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index 7d7a7b4..c5edb03 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -6,7 +6,7 @@ use tantivy::collector::TopDocs; use tantivy::query::{AllQuery, BooleanQuery, FuzzyTermQuery, Query, RegexQuery, TermQuery}; use tantivy::schema::*; use tantivy::tokenizer::TokenizerManager; -use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType}; +use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult, SearchResultAccessory, SearchResultEntrypointAction, SearchResultEntrypointType}; use gauntlet_common::rpc::frontend_api::FrontendApi; #[derive(Clone)] @@ -29,6 +29,7 @@ struct EntrypointData { icon_path: Option, frecency: f64, actions: Vec, + accessories: Vec, } struct EntrypointActionData { @@ -44,6 +45,7 @@ pub struct SearchIndexItem { pub entrypoint_icon_path: Option, pub entrypoint_frecency: f64, pub entrypoint_actions: Vec, + pub entrypoint_accessories: Vec, } #[derive(Clone, Debug)] @@ -133,20 +135,21 @@ impl SearchIndex { index_writer.commit()?; self.index_reader.reload()?; - let data = search_items.iter() + let data = search_items.into_iter() .map(|item| { - let actions = item.entrypoint_actions.iter() + let actions = item.entrypoint_actions.into_iter() .map(|action| EntrypointActionData { - label: action.label.clone(), - shortcut: action.shortcut.clone(), + label: action.label, + shortcut: action.shortcut, }) .collect(); let data = EntrypointData { - entrypoint_type: item.entrypoint_type.clone(), - icon_path: item.entrypoint_icon_path.clone(), + entrypoint_type: item.entrypoint_type, + icon_path: item.entrypoint_icon_path, frecency: item.entrypoint_frecency, actions, + accessories: item.entrypoint_accessories, }; (item.entrypoint_id.clone(), data) @@ -256,6 +259,10 @@ impl SearchIndex { }) .collect(); + let entrypoint_accessories = entrypoint_data.accessories.iter() + .cloned() + .collect(); + let result_item = SearchResult { entrypoint_type: entrypoint_data.entrypoint_type.clone(), entrypoint_name, @@ -264,6 +271,7 @@ impl SearchIndex { plugin_name, plugin_id, entrypoint_actions, + entrypoint_accessories, }; (result_item, entrypoint_data.frecency) From ba4fd06f3780e9830cfe3b410e773572ccd061b1 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 25 Dec 2024 11:33:37 +0100 Subject: [PATCH 245/540] Merge updateAccessories into add, introduce get by id and getAll functions for command generator --- dev_plugin/src/command-generator.ts | 14 ++++++-------- js/api/src/helpers.ts | 4 +++- js/core/src/command-generator.ts | 28 ++++++++++++++++++---------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/dev_plugin/src/command-generator.ts b/dev_plugin/src/command-generator.ts index 73395e2..c8ee630 100644 --- a/dev_plugin/src/command-generator.ts +++ b/dev_plugin/src/command-generator.ts @@ -1,7 +1,6 @@ import { GeneratorProps, showHud } from "@project-gauntlet/api/helpers"; -import { Icons } from "@project-gauntlet/api/components"; -export default function CommandGenerator({ add, remove: _, updateAccessories }: GeneratorProps): void { +export default function CommandGenerator({ add, remove: _ }: GeneratorProps): void { add('generated-test-1', { name: 'Generated Item 1', fn: () => { @@ -54,11 +53,10 @@ export default function CommandGenerator({ add, remove: _, updateAccessories }: fn: () => { console.log('generated-test-4') }, + accessories: [ + { + text: "1 window open" + } + ], }) - - updateAccessories('generated-test-4', [ - { - text: "1 window open" - }, - ]) } diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index b89ac21..2d8dab6 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -37,6 +37,7 @@ export interface GeneratedCommand { icon?: ArrayBuffer fn: () => void actions?: GeneratedCommandAction[] + accessories?: GeneratedCommandAccessory[] } export interface GeneratedCommandAction { @@ -61,7 +62,8 @@ export interface GeneratedCommandIconAccessory { export type GeneratorProps = { add: (id: string, data: GeneratedCommand) => void, remove: (id: string) => void, - updateAccessories: (id: string, accessories: GeneratedCommandAccessory[] | undefined) => void, + get: (id: string) => GeneratedCommand | undefined + getAll: () => GeneratedCommand[] }; export const Clipboard: Clipboard = { diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index 93e6468..b7fae35 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -2,6 +2,7 @@ import { fetch_action_id_for_shortcut, get_command_generator_entrypoint_ids, op_log_info, + op_log_debug, update_loading_bar } from "ext:core/ops"; import { reloadSearchIndex } from "./search-index"; @@ -11,6 +12,7 @@ interface GeneratedCommand { // TODO is it possible to import api here icon?: ArrayBuffer fn: () => void actions?: GeneratedCommandAction[] + accessories?: GeneratedCommandAccessory[] } interface GeneratedCommandAction { @@ -22,12 +24,13 @@ interface GeneratedCommandAction { type GeneratorProps = { add: (id: string, data: GeneratedCommand) => void, remove: (id: string) => void, - updateAccessories: (id: string, accessories: GeneratedCommandAccessory[] | undefined) => void, + get: (id: string) => GeneratedCommand | undefined + getAll: () => GeneratedCommand[] }; type Generator = (props: GeneratorProps) => void | (() => (void | Promise)) | Promise (void | Promise))> -type ProcessedGeneratedCommand = { generatorEntrypointId: string, uuid: string, command: GeneratedCommand, accessories?: GeneratedCommandAccessory[] }; +type ProcessedGeneratedCommand = { generatorEntrypointId: string, uuid: string, command: GeneratedCommand }; type ProcessedGeneratedCommands = { [lookupEntrypointId: string]: ProcessedGeneratedCommand }; type GeneratorCleanups = { [generatorEntrypointId: string]: () => (void | Promise) }; @@ -65,7 +68,6 @@ export async function runCommandGenerators(): Promise { generatorEntrypointId: generatorEntrypointId, uuid: crypto.randomUUID(), command: data, - accessories: [], } reloadSearchIndex(true) @@ -79,24 +81,30 @@ export async function runCommandGenerators(): Promise { reloadSearchIndex(true) } - const updateAccessories = (id: string, accessories: GeneratedCommandAccessory[] | undefined): void => { - op_log_info("command_generator", `Updating accessories for entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) + const get = (id: string) => { + op_log_debug("command_generator", `Getting entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) const lookupId = generatorEntrypointId + ":" + id; const generatedCommand = storedGeneratedCommands[lookupId]; if (generatedCommand) { - generatedCommand.accessories = accessories - reloadSearchIndex(true) + return generatedCommand.command } else { - console.error(`Unable to update accessories for entry '${id}' because it doesn't exist`) + return undefined } } + const getAll = (): GeneratedCommand[] => { + op_log_debug("command_generator", `Getting all entries by command generator entrypoint '${generatorEntrypointId}'`) + + return Object.entries(storedGeneratedCommands) + .map(([_, value]) => value.command) + } + // noinspection ES6MissingAwait (async () => { try { update_loading_bar(generatorEntrypointId, true) - let cleanup = await generator({ add, remove, updateAccessories }) + let cleanup = await generator({ add, remove, get, getAll }) update_loading_bar(generatorEntrypointId, false) if (typeof cleanup === "function") { generatorCleanups[generatorEntrypointId] = cleanup @@ -123,7 +131,7 @@ export function generatedCommandSearchIndex(): AdditionalSearchItem[] { id: action.ref, label: action.label })), - entrypoint_accessories: value.accessories || [] + entrypoint_accessories: value.command.accessories || [] })) } From 0b48acdf776f893fc8f2b39e92ae3a703098e933 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 25 Dec 2024 13:37:46 +0100 Subject: [PATCH 246/540] Move command generator primary action into actions field which allows to specify custom default label --- bundled_plugins/gauntlet/src/applications.ts | 55 ++++-- dev_plugin/src/command-generator.ts | 62 ++++--- js/api/src/helpers.ts | 3 +- js/core/src/command-generator.ts | 23 ++- js/typings/index.d.ts | 2 +- rust/client/src/ui/mod.rs | 184 +++++++++++-------- rust/client/src/ui/state/mod.rs | 2 +- rust/common/src/model.rs | 2 +- rust/common/src/rpc/backend_api.rs | 2 +- rust/plugin_runtime/src/events.rs | 2 +- rust/server/src/model.rs | 2 +- rust/server/src/plugins/js.rs | 2 +- rust/server/src/plugins/mod.rs | 2 +- 13 files changed, 208 insertions(+), 135 deletions(-) diff --git a/bundled_plugins/gauntlet/src/applications.ts b/bundled_plugins/gauntlet/src/applications.ts index f4b6f03..2ed9bd4 100644 --- a/bundled_plugins/gauntlet/src/applications.ts +++ b/bundled_plugins/gauntlet/src/applications.ts @@ -28,9 +28,14 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro path => linux_app_from_path(path), (id, data) => ({ name: data.name, - fn: () => { - linux_open_application(id) - }, + actions: [ + { + label: "Open application", + fn: () => { + linux_open_application(id) + }, + } + ], icon: data.icon, // TODO lazy icons }), add, @@ -44,9 +49,14 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro for (const setting of macos_settings_13_and_post()) { add(`settings:${setting.preferences_id}`, { name: setting.name, - fn: () => { - macos_open_setting_13_and_post(setting.preferences_id) - }, + actions: [ + { + label: "Open settings", + fn: () => { + macos_open_setting_13_and_post(setting.preferences_id) + }, + } + ], icon: setting.icon, }) } @@ -54,9 +64,14 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro for (const setting of macos_settings_pre_13()) { add(`settings:${setting.path}`, { name: setting.name, - fn: () => { - macos_open_setting_pre_13(setting.path) - }, + actions: [ + { + label: "Open settings", + fn: () => { + macos_open_setting_pre_13(setting.path) + }, + } + ], icon: setting.icon, }) } @@ -70,9 +85,14 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro let data = app.data; add(data.path, { name: data.name, - fn: () => { - macos_open_application(data.path) - }, + actions: [ + { + label: "Open application", + fn: () => { + macos_open_application(data.path) + }, + } + ], icon: data.icon, }) break; @@ -88,9 +108,14 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro path => macos_app_from_arbitrary_path(path), (_id, data) => ({ name: data.name, - fn: () => { - macos_open_application(data.path) - }, + actions: [ + { + label: "Open application", + fn: () => { + macos_open_application(data.path) + }, + } + ], icon: data.icon, }), add, diff --git a/dev_plugin/src/command-generator.ts b/dev_plugin/src/command-generator.ts index c8ee630..bd80aa0 100644 --- a/dev_plugin/src/command-generator.ts +++ b/dev_plugin/src/command-generator.ts @@ -3,27 +3,35 @@ import { GeneratorProps, showHud } from "@project-gauntlet/api/helpers"; export default function CommandGenerator({ add, remove: _ }: GeneratorProps): void { add('generated-test-1', { name: 'Generated Item 1', - fn: () => { - new Promise(() => { - throw new Error("gen") - }) + actions: [ + { + label: "Run Generated Item 1", + fn: () => { + new Promise(() => { + throw new Error("gen") + }) - console.log('generated-test-1') - } + console.log('generated-test-1') + } + } + ] }) add('generated-test-2', { name: 'Generated Item 2', - fn: () => { - console.log('generated-test-2') - - sessionStorage.setItem("test", "test") - console.dir(sessionStorage.getItem("test")) - - localStorage.setItem("test", "test") - console.dir(localStorage.getItem("test")) - }, actions: [ + { + label: "Run Generated Item 2", + fn: () => { + console.log('generated-test-2') + + sessionStorage.setItem("test", "test") + console.dir(sessionStorage.getItem("test")) + + localStorage.setItem("test", "test") + console.dir(localStorage.getItem("test")) + } + }, { label: "Test 1", fn: () => { @@ -42,17 +50,27 @@ export default function CommandGenerator({ add, remove: _ }: GeneratorProps): vo add('generated-test-3', { name: 'Generated Item 3', - fn: () => { - showHud("HUD test display") - console.log('generated-test-3') - } + actions: [ + { + label: "Run Generated Item 3", + fn: () => { + showHud("HUD test display") + console.log('generated-test-3') + }, + } + ] }) add('generated-test-4', { name: 'Generated Item 4', - fn: () => { - console.log('generated-test-4') - }, + actions: [ + { + label: "Run Generated Item 4", + fn: () => { + console.log('generated-test-4') + }, + } + ], accessories: [ { text: "1 window open" diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index 2d8dab6..d09f397 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -34,9 +34,8 @@ export function showHud(display: string): void { export interface GeneratedCommand { name: string + actions: GeneratedCommandAction[] icon?: ArrayBuffer - fn: () => void - actions?: GeneratedCommandAction[] accessories?: GeneratedCommandAccessory[] } diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index b7fae35..d0559f4 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -9,9 +9,8 @@ import { reloadSearchIndex } from "./search-index"; interface GeneratedCommand { // TODO is it possible to import api here name: string + actions: GeneratedCommandAction[] icon?: ArrayBuffer - fn: () => void - actions?: GeneratedCommandAction[] accessories?: GeneratedCommandAccessory[] } @@ -62,6 +61,10 @@ export async function runCommandGenerators(): Promise { const add = (id: string, data: GeneratedCommand) => { op_log_info("command_generator", `Adding entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) + if (data.actions.length < 1) { + throw new Error(`Error when adding entry '${id}': at least one action should be provided`) + } + const lookupId = generatorEntrypointId + ":" + id; storedGeneratedCommands[lookupId] = { @@ -126,7 +129,7 @@ export function generatedCommandSearchIndex(): AdditionalSearchItem[] { entrypoint_uuid: value.uuid, entrypoint_name: value.command.name, entrypoint_icon: value.command.icon, - entrypoint_actions: (value.command.actions || []) + entrypoint_actions: value.command.actions .map(action => ({ id: action.ref, label: action.label @@ -149,19 +152,15 @@ export async function runGeneratedCommandAction(entrypointId: string, key: strin } } -export function runGeneratedCommand(entrypointId: string, action_index: number | undefined) { +export function runGeneratedCommand(entrypointId: string, action_index: number) { const generatedCommand = storedGeneratedCommands[entrypointId]; if (generatedCommand) { - if (typeof action_index == "number") { - const actions = generatedCommand.command.actions; - if (actions) { - actions[action_index].fn() - } else { - throw new Error("Generated command with entrypoint id '" + entrypointId + "' doesn't have actions, action index: " + action_index) - } + const actions = generatedCommand.command.actions; + if (actions) { + actions[action_index].fn() } else { - generatedCommand.command.fn() + throw new Error("Generated command with entrypoint id '" + entrypointId + "' doesn't have actions, action index: " + action_index) } } else { throw new Error("Generated command with entrypoint id '" + entrypointId + "' not found") diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 20013b6..774cac1 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -78,7 +78,7 @@ type RunCommand = { type RunGeneratedCommand = { type: "RunGeneratedCommand" entrypointId: string - actionIndex: number | undefined + actionIndex: number } type OpenInlineView = { diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index de573d5..42e45f6 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -104,9 +104,9 @@ pub enum AppMsg { RunGeneratedCommandEvent { plugin_id: PluginId, entrypoint_id: EntrypointId, - action_index: Option + action_index: usize }, - RunSearchItemAction(SearchResult, Option), + RunSearchItemAction(SearchResult, usize), RunPluginAction { render_location: UiRenderLocation, plugin_id: PluginId, @@ -179,7 +179,7 @@ pub enum AppMsg { display: String }, OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus, - OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result: SearchResult }, + OnPrimaryActionMainViewNoPanel { search_result: SearchResult }, OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result: SearchResult }, OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus, OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { search_result: SearchResult, widget_id: UiWidgetId }, @@ -578,27 +578,25 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { AppMsg::RunSearchItemAction(search_result, action_index) => { match search_result.entrypoint_type { SearchResultEntrypointType::Command => { - match action_index { - None => { - Task::done(AppMsg::RunCommand { - entrypoint_id: search_result.entrypoint_id.clone(), - plugin_id: search_result.plugin_id.clone() - }) - } - Some(_) => Task::none() + if action_index == 0 { + Task::done(AppMsg::RunCommand { + entrypoint_id: search_result.entrypoint_id.clone(), + plugin_id: search_result.plugin_id.clone(), + }) + } else { + Task::none() } }, SearchResultEntrypointType::View => { - match action_index { - None => { - Task::done(AppMsg::OpenView { - plugin_id: search_result.plugin_id.clone(), - plugin_name: search_result.plugin_name.clone(), - entrypoint_id: search_result.entrypoint_id.clone(), - entrypoint_name: search_result.entrypoint_name.clone(), - }) - } - Some(_) => Task::none() + if action_index == 0 { + Task::done(AppMsg::OpenView { + plugin_id: search_result.plugin_id.clone(), + plugin_name: search_result.plugin_name.clone(), + entrypoint_id: search_result.entrypoint_id.clone(), + entrypoint_name: search_result.entrypoint_name.clone(), + }) + } else { + Task::none() } }, SearchResultEntrypointType::GeneratedCommand => { @@ -1090,24 +1088,20 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus => { Task::done(AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index: 0 }) } - AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result } => { - Task::done(AppMsg::RunSearchItemAction(search_result, None)) + AppMsg::OnPrimaryActionMainViewNoPanel { search_result } => { + Task::done(AppMsg::RunSearchItemAction(search_result, 0)) } AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus => { Task::done(AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index: 1 }) } AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result } => { - Task::done(AppMsg::RunSearchItemAction(search_result, Some(0))) + Task::done(AppMsg::RunSearchItemAction(search_result, 1)) } AppMsg::OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id } => { - let run_action_command = if widget_id == 0 { - Task::done(AppMsg::RunSearchItemAction(search_result, None)) - } else { - Task::done(AppMsg::RunSearchItemAction(search_result, Some(widget_id - 1))) - }; + let index = widget_id; Task::batch([ - run_action_command, + Task::done(AppMsg::RunSearchItemAction(search_result, index)), Task::done(AppMsg::ResetMainViewState) ]) } @@ -1147,7 +1141,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { GlobalState::MainView { focused_search_result, .. } => { if let Some(search_result) = focused_search_result.get(&state.search_results) { let search_result = search_result.clone(); - Task::done(AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result }) + Task::done(AppMsg::OnPrimaryActionMainViewNoPanel { search_result }) } else { Task::done(AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus) } @@ -1600,7 +1594,7 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { .themed(TextInputStyle::MainSearch); let search_list = search_list(&state.search_results, &focused_search_result) - .map(|search_result| AppMsg::RunSearchItemAction(search_result, None)); + .map(|search_result| AppMsg::OnPrimaryActionMainViewNoPanel { search_result }); let search_list = container(search_list) .width(Length::Fill) @@ -1652,13 +1646,7 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { ]).into(); let (primary_action, action_panel) = if let Some(search_item) = focused_search_result.get(&state.search_results) { - let label = match search_item.entrypoint_type { - SearchResultEntrypointType::Command => "Run Command", - SearchResultEntrypointType::View => "Open View", - SearchResultEntrypointType::GeneratedCommand => "Run Command", - }.to_string(); - - let default_shortcut = PhysicalShortcut { + let primary_shortcut = PhysicalShortcut { physical_key: PhysicalKey::Enter, modifier_shift: false, modifier_control: false, @@ -1666,49 +1654,93 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { modifier_meta: false, }; - let mut actions: Vec<_> = search_item.entrypoint_actions - .iter() - .enumerate() - .map(|(index, action)| { - let physical_shortcut = if index == 0 { - Some(PhysicalShortcut { // secondary action - physical_key: PhysicalKey::Enter, - modifier_shift: true, - modifier_control: false, - modifier_alt: false, - modifier_meta: false, - }) - } else { - action.shortcut.clone() + let secondary_shortcut = PhysicalShortcut { + physical_key: PhysicalKey::Enter, + modifier_shift: true, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }; + + let generate = |label: &str, primary_shortcut: PhysicalShortcut, secondary_shortcut: PhysicalShortcut| { + let mut actions: Vec<_> = search_item.entrypoint_actions + .iter() + .enumerate() + .map(|(index, action)| { + let physical_shortcut = if index == 0 { + Some(secondary_shortcut.clone()) + } else { + action.shortcut.clone() + }; + + ActionPanelItem::Action { + label: action.label.clone(), + widget_id: index, + physical_shortcut, + } + }) + .collect(); + + let primary_action_widget_id = 0; + + if actions.len() == 0 { + (Some((label.to_string(), primary_action_widget_id, primary_shortcut)), None) + } else { + let label = label.to_string(); + + let primary_action = ActionPanelItem::Action { + label: label.clone(), + widget_id: primary_action_widget_id, + physical_shortcut: Some(primary_shortcut.clone()), }; - ActionPanelItem::Action { - label: action.label.clone(), - widget_id: index + 1, - physical_shortcut, - } - }) - .collect(); + actions.insert(0, primary_action); - let primary_action_widget_id = 0; + let action_panel = ActionPanel { + title: Some(search_item.entrypoint_name.clone()), + items: actions, + }; - if actions.len() == 0 { - (Some((label, primary_action_widget_id, default_shortcut)), None) - } else { - let primary_action = ActionPanelItem::Action { - label: label.clone(), - widget_id: primary_action_widget_id, - physical_shortcut: Some(default_shortcut.clone()), - }; + (Some((label, primary_action_widget_id, primary_shortcut)), Some(action_panel)) + } + }; - actions.insert(0, primary_action); + match search_item.entrypoint_type { + SearchResultEntrypointType::Command => generate("Run Command", primary_shortcut, secondary_shortcut), + SearchResultEntrypointType::View => generate("Open View", primary_shortcut, secondary_shortcut), + SearchResultEntrypointType::GeneratedCommand => { + let label = search_item.entrypoint_actions + .first() + .map(|action| action.label.clone()) + .unwrap_or_else(|| "Run Command".to_string()); - let action_panel = ActionPanel { - title: Some(search_item.entrypoint_name.clone()), - items: actions, - }; + let mut actions: Vec<_> = search_item.entrypoint_actions + .iter() + .enumerate() + .map(|(index, action)| { + let physical_shortcut = match index { + 0 => Some(primary_shortcut.clone()), + 1 => Some(secondary_shortcut.clone()), + _ => action.shortcut.clone() + }; - (Some((label, primary_action_widget_id, default_shortcut)), Some(action_panel)) + ActionPanelItem::Action { + label: action.label.clone(), + widget_id: index, + physical_shortcut, + } + }) + .collect(); + + let primary_action_widget_id = 0; + + let action_panel = ActionPanel { + title: Some(search_item.entrypoint_name.clone()), + items: actions, + }; + + (Some((label, primary_action_widget_id, primary_shortcut)), Some(action_panel)) + }, } } else { match state.client_context.get_first_inline_view_action_panel() { @@ -1995,7 +2027,7 @@ impl AppModel { }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } - fn run_generated_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: Option) -> Task { + fn run_generated_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize) -> Task { let mut backend_client = self.backend_api.clone(); Task::perform(async move { diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 2abe893..a9a5952 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -138,7 +138,7 @@ impl Focus for GlobalState { MainViewState::None => { if let Some(search_result) = focused_search_result.get(focus_list) { let search_result = search_result.clone(); - Task::done(AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithFocus { search_result }) + Task::done(AppMsg::OnPrimaryActionMainViewNoPanel { search_result }) } else { Task::done(AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus) } diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index c90b725..80cf33c 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -223,7 +223,7 @@ pub enum BackendRequestData { RequestRunGeneratedCommand { plugin_id: PluginId, entrypoint_id: EntrypointId, - action_index: Option + action_index: usize }, SendViewEvent { plugin_id: PluginId, diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index 6dec40b..caecc7d 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -92,7 +92,7 @@ impl BackendForFrontendApi { Ok(()) } - pub async fn request_run_generated_command(&mut self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: Option) -> Result<(), BackendForFrontendApiError> { + pub async fn request_run_generated_command(&mut self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize) -> Result<(), BackendForFrontendApiError> { let request = BackendRequestData::RequestRunGeneratedCommand { plugin_id, entrypoint_id, diff --git a/rust/plugin_runtime/src/events.rs b/rust/plugin_runtime/src/events.rs index 3d988ca..72599b9 100644 --- a/rust/plugin_runtime/src/events.rs +++ b/rust/plugin_runtime/src/events.rs @@ -25,7 +25,7 @@ pub enum JsEvent { #[serde(rename = "entrypointId")] entrypoint_id: String, #[serde(rename = "actionIndex")] - action_index: Option + action_index: usize }, ViewEvent { #[serde(rename = "widgetId")] diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 18f92d8..851a31b 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -12,7 +12,7 @@ pub enum IntermediateUiEvent { }, RunGeneratedCommand { entrypoint_id: String, - action_index: Option + action_index: usize }, HandleViewEvent { widget_id: UiWidgetId, diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index c3a42f6..6cf2852 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -100,7 +100,7 @@ pub enum OnePluginCommandData { }, RunGeneratedCommand { entrypoint_id: String, - action_index: Option + action_index: usize }, HandleViewEvent { widget_id: UiWidgetId, diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 13f1ea4..ff9231d 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -361,7 +361,7 @@ impl ApplicationManager { self.mark_entrypoint_frecency(plugin_id, entrypoint_id).await } - pub async fn handle_run_generated_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: Option) { + pub async fn handle_run_generated_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize) { self.send_command(PluginCommand::One { id: plugin_id.clone(), data: OnePluginCommandData::RunGeneratedCommand { From 9434c88f834487fc455a379719b1a1758b2a28c1 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 25 Dec 2024 23:45:11 +0100 Subject: [PATCH 247/540] Add ability to open view with generated commands. Rename fn property into run, add view property --- bundled_plugins/gauntlet/src/applications.ts | 10 +- dev_plugin/gauntlet.toml | 2 +- ...and-generator.ts => command-generator.tsx} | 27 ++-- js/api/src/helpers.ts | 13 +- js/core/src/command-generator.ts | 106 +++++++++++-- js/core/src/core.tsx | 147 ++++-------------- js/core/src/render.tsx | 123 +++++++++++++++ js/core/typings/index.d.ts | 2 +- js/react_renderer/src/renderer.ts | 19 ++- js/typings/index.d.ts | 14 +- rust/client/src/ui/mod.rs | 140 +++++++++++------ rust/common/src/model.rs | 9 +- rust/plugin_runtime/src/api.rs | 9 +- rust/plugin_runtime/src/deno.rs | 6 +- rust/plugin_runtime/src/model.rs | 27 +++- rust/plugin_runtime/src/plugin_data.rs | 19 ++- rust/plugin_runtime/src/search.rs | 4 +- rust/plugin_runtime/src/ui.rs | 14 +- rust/server/src/plugins/js.rs | 31 ++-- rust/server/src/search.rs | 24 ++- 20 files changed, 506 insertions(+), 240 deletions(-) rename dev_plugin/src/{command-generator.ts => command-generator.tsx} (82%) create mode 100644 js/core/src/render.tsx diff --git a/bundled_plugins/gauntlet/src/applications.ts b/bundled_plugins/gauntlet/src/applications.ts index 2ed9bd4..e918bd3 100644 --- a/bundled_plugins/gauntlet/src/applications.ts +++ b/bundled_plugins/gauntlet/src/applications.ts @@ -31,7 +31,7 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro actions: [ { label: "Open application", - fn: () => { + run: () => { linux_open_application(id) }, } @@ -52,7 +52,7 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro actions: [ { label: "Open settings", - fn: () => { + run: () => { macos_open_setting_13_and_post(setting.preferences_id) }, } @@ -67,7 +67,7 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro actions: [ { label: "Open settings", - fn: () => { + run: () => { macos_open_setting_pre_13(setting.path) }, } @@ -88,7 +88,7 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro actions: [ { label: "Open application", - fn: () => { + run: () => { macos_open_application(data.path) }, } @@ -111,7 +111,7 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro actions: [ { label: "Open application", - fn: () => { + run: () => { macos_open_application(data.path) }, } diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 1656ae3..f190c3e 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -165,7 +165,7 @@ description = '' [[entrypoint]] id = 'command-generator' name = 'Command generator' -path = 'src/command-generator.ts' +path = 'src/command-generator.tsx' type = 'command-generator' description = '' diff --git a/dev_plugin/src/command-generator.ts b/dev_plugin/src/command-generator.tsx similarity index 82% rename from dev_plugin/src/command-generator.ts rename to dev_plugin/src/command-generator.tsx index bd80aa0..33e2cf1 100644 --- a/dev_plugin/src/command-generator.ts +++ b/dev_plugin/src/command-generator.tsx @@ -1,4 +1,15 @@ import { GeneratorProps, showHud } from "@project-gauntlet/api/helpers"; +import { ReactElement } from "react"; +import { List } from "@project-gauntlet/api/components"; + +function ListView(): ReactElement { + return ( + + + + ) +} + export default function CommandGenerator({ add, remove: _ }: GeneratorProps): void { add('generated-test-1', { @@ -6,7 +17,7 @@ export default function CommandGenerator({ add, remove: _ }: GeneratorProps): vo actions: [ { label: "Run Generated Item 1", - fn: () => { + run: () => { new Promise(() => { throw new Error("gen") }) @@ -22,7 +33,7 @@ export default function CommandGenerator({ add, remove: _ }: GeneratorProps): vo actions: [ { label: "Run Generated Item 2", - fn: () => { + run: () => { console.log('generated-test-2') sessionStorage.setItem("test", "test") @@ -30,18 +41,18 @@ export default function CommandGenerator({ add, remove: _ }: GeneratorProps): vo localStorage.setItem("test", "test") console.dir(localStorage.getItem("test")) - } + }, }, { label: "Test 1", - fn: () => { + run: () => { console.log('generated-action-1') } }, { ref: "testGeneratedAction1", label: "Test 2", - fn: () => { + run: () => { console.log('generated-action-2') } } @@ -53,7 +64,7 @@ export default function CommandGenerator({ add, remove: _ }: GeneratorProps): vo actions: [ { label: "Run Generated Item 3", - fn: () => { + run: () => { showHud("HUD test display") console.log('generated-test-3') }, @@ -66,9 +77,7 @@ export default function CommandGenerator({ add, remove: _ }: GeneratorProps): vo actions: [ { label: "Run Generated Item 4", - fn: () => { - console.log('generated-test-4') - }, + view: () => } ], accessories: [ diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index d09f397..3435a0d 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -11,6 +11,7 @@ import { environment_plugin_cache_dir, environment_plugin_data_dir } from "ext:core/ops"; +import type { FC } from "react"; export function assetDataSync(path: string): ArrayBuffer { return getAssetDataSync(path) @@ -39,10 +40,18 @@ export interface GeneratedCommand { accessories?: GeneratedCommandAccessory[] } -export interface GeneratedCommandAction { +export type GeneratedCommandAction = GeneratedCommandActionRun | GeneratedCommandActionView + +export interface GeneratedCommandActionRun { ref?: string label: string - fn: () => void + run: () => void +} + +export interface GeneratedCommandActionView { + ref?: string + label: string + view: FC } export type GeneratedCommandAccessory = GeneratedCommandTextAccessory | GeneratedCommandIconAccessory; diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts index d0559f4..f0916d5 100644 --- a/js/core/src/command-generator.ts +++ b/js/core/src/command-generator.ts @@ -6,6 +6,8 @@ import { update_loading_bar } from "ext:core/ops"; import { reloadSearchIndex } from "./search-index"; +import type { FC } from "react"; +import { renderView } from "./render"; interface GeneratedCommand { // TODO is it possible to import api here name: string @@ -14,10 +16,18 @@ interface GeneratedCommand { // TODO is it possible to import api here accessories?: GeneratedCommandAccessory[] } -interface GeneratedCommandAction { +type GeneratedCommandAction = GeneratedCommandActionRun | GeneratedCommandActionView + +interface GeneratedCommandActionRun { ref?: string label: string - fn: () => void + run: () => void +} + +interface GeneratedCommandActionView { + ref?: string + label: string + view: FC } type GeneratorProps = { @@ -29,7 +39,29 @@ type GeneratorProps = { type Generator = (props: GeneratorProps) => void | (() => (void | Promise)) | Promise (void | Promise))> -type ProcessedGeneratedCommand = { generatorEntrypointId: string, uuid: string, command: GeneratedCommand }; +type ProcessedGeneratedCommand = { + generatorEntrypointId: string, + uuid: string, + command: GeneratedCommand + derivedActions: GeneratedCommandDerivedAction[] +}; + +type GeneratedCommandDerivedAction = GeneratedCommandDerivedActionRun | GeneratedCommandDerivedActionView + +interface GeneratedCommandDerivedActionRun { + type: "Command" + ref?: string + label: string + run: () => void +} + +interface GeneratedCommandDerivedActionView { + type: "View" + ref?: string + label: string + view: FC +} + type ProcessedGeneratedCommands = { [lookupEntrypointId: string]: ProcessedGeneratedCommand }; type GeneratorCleanups = { [generatorEntrypointId: string]: () => (void | Promise) }; @@ -65,12 +97,45 @@ export async function runCommandGenerators(): Promise { throw new Error(`Error when adding entry '${id}': at least one action should be provided`) } + const derivedActions: GeneratedCommandDerivedAction[] = [] + for (const action of data.actions) { + const label = action.label; + + const run = "run" in action; + const view = "view" in action; + + if (run && view) { + throw new Error(`only one of 'run' or 'view' properties can be specified in action: '${label}'`) + } + + if (!run && !view) { + throw new Error(`one of 'run' or 'view' properties has to be specified in action: '${label}'`) + } + + if (run) { + derivedActions.push({ + type: "Command", + ref: action.ref, + label: action.label, + run: action.run, + }) + } else if (view) { + derivedActions.push({ + type: "View", + ref: action.ref, + label: action.label, + view: action.view, + }) + } + } + const lookupId = generatorEntrypointId + ":" + id; storedGeneratedCommands[lookupId] = { generatorEntrypointId: generatorEntrypointId, uuid: crypto.randomUUID(), command: data, + derivedActions, } reloadSearchIndex(true) @@ -122,16 +187,17 @@ export async function runCommandGenerators(): Promise { } } -export function generatedCommandSearchIndex(): AdditionalSearchItem[] { +export function generatedCommandSearchIndex(): GeneratedSearchItem[] { return Object.entries(storedGeneratedCommands).map(([entrypointLookupId, value]) => ({ generator_entrypoint_id: value.generatorEntrypointId, entrypoint_id: entrypointLookupId, entrypoint_uuid: value.uuid, entrypoint_name: value.command.name, entrypoint_icon: value.command.icon, - entrypoint_actions: value.command.actions + entrypoint_actions: value.derivedActions .map(action => ({ id: action.ref, + action_type: action.type, label: action.label })), entrypoint_accessories: value.command.accessories || [] @@ -144,9 +210,9 @@ export async function runGeneratedCommandAction(entrypointId: string, key: strin if (command) { const id = await fetch_action_id_for_shortcut(command.generatorEntrypointId, key, modifierShift, modifierControl, modifierAlt, modifierMeta); if (id) { - const action = command.command.actions?.find(value => value.ref == id); + const action = command.derivedActions.find(value => value.ref == id); if (action) { - action.fn() + runAction(entrypointId, action) } } } @@ -156,13 +222,31 @@ export function runGeneratedCommand(entrypointId: string, action_index: number) const generatedCommand = storedGeneratedCommands[entrypointId]; if (generatedCommand) { - const actions = generatedCommand.command.actions; - if (actions) { - actions[action_index].fn() + const action = generatedCommand.derivedActions[action_index]; + if (action) { + runAction(entrypointId, action) } else { - throw new Error("Generated command with entrypoint id '" + entrypointId + "' doesn't have actions, action index: " + action_index) + throw new Error("Generated command with entrypoint id '" + entrypointId + "' doesn't have action with index: " + action_index) } } else { throw new Error("Generated command with entrypoint id '" + entrypointId + "' not found") } +} + +function runAction(entrypointId: string, action: GeneratedCommandDerivedAction) { + switch (action.type) { + case "Command": { + action.run() + + break; + } + case "View": { + const entrypointName = storedGeneratedCommands[entrypointId] + .command + .name + + renderView(entrypointId, entrypointName, action.view) + break; + } + } } \ No newline at end of file diff --git a/js/core/src/core.tsx b/js/core/src/core.tsx index e2c8ef5..76684c9 100644 --- a/js/core/src/core.tsx +++ b/js/core/src/core.tsx @@ -1,101 +1,17 @@ import type { FC } from "react"; import { runCommandGenerators, runGeneratedCommand, runGeneratedCommandAction } from "./command-generator"; import { reloadSearchIndex } from "./search-index"; -import { clearRenderer, render } from "ext:gauntlet/renderer.js"; +import { closeView, handleEvent, handlePluginViewKeyboardEvent, renderInlineView, renderView } from "./render"; import { - clear_inline_view, - entrypoint_preferences_required, - fetch_action_id_for_shortcut, - op_inline_view_endpoint_id, - op_log_debug, + entrypoint_preferences_required, op_entrypoint_names, + op_inline_view_entrypoint_id, op_log_trace, + op_plugin_get_pending_event, plugin_preferences_required, show_plugin_error_view, - show_preferences_required_view, - op_plugin_get_pending_event + show_preferences_required_view } from "ext:core/ops"; -let latestRootUiWidget: UiWidget | undefined = undefined - -function findWidgetWithId(widget: UiWidget, widgetId: number): UiWidget | undefined { - if (widget.widgetId === widgetId) { - return widget - } - - for (let widgetChild of widget.widgetChildren) { - const widgetWithId = findWidgetWithId(widgetChild, widgetId); - if (widgetWithId) { - return widgetWithId - } - } - - return undefined; -} - -function findAllActionHandlers(widget: UiWidget): { id: string, onAction: () => void }[] { - if (widget.widgetType === "gauntlet:action") { - const id = widget.widgetProperties["id"]; - const onAction = widget.widgetProperties["onAction"]; - if (!!id && !!onAction) { - return [{ id, onAction }] - } else { - return [] - } - } - - let result: { id: string, onAction: () => void }[] = [] - for (let widgetChild of widget.widgetChildren) { - const actionHandler = findAllActionHandlers(widgetChild); - - result.push(...actionHandler) - } - - return result; -} - -function handleEvent(event: ViewEvent) { - op_log_trace("plugin_event_handler", `Handling view event: ${Deno.inspect(event)}`); - op_log_trace("plugin_event_handler", `Root widget: ${Deno.inspect(latestRootUiWidget)}`); - if (latestRootUiWidget) { - const widgetWithId = findWidgetWithId(latestRootUiWidget, event.widgetId); - op_log_trace("plugin_event_handler", `Found widget with id ${event.widgetId}: ${Deno.inspect(widgetWithId)}`) - - if (widgetWithId) { - const property = widgetWithId.widgetProperties[event.eventName]; - - op_log_trace("plugin_event_handler", `Found event handler with name ${event.eventName}: ${Deno.inspect(property)}`) - - if (property) { - if (typeof property === "function") { - - const eventArgs = event.eventArguments - .map(arg => { - switch (arg.type) { - case "Undefined": { - return undefined - } - case "String": { - return arg.value - } - case "Number": { - return arg.value - } - case "Bool": { - return arg.value - } - } - }); - - op_log_trace("plugin_event_handler", `Calling handler with arguments ${Deno.inspect(eventArgs)}`) - - property(...eventArgs); - } else { - throw new Error(`Event handler has type ${typeof property}, but should be function`) - } - } - } - } -} async function handleKeyboardEvent(event: NotReactsKeyboardEvent) { op_log_trace("plugin_event_handler", `Handling keyboard event: ${Deno.inspect(event)}`); @@ -105,19 +21,7 @@ async function handleKeyboardEvent(event: NotReactsKeyboardEvent) { break; } case "PluginView": { - if (latestRootUiWidget) { - const actionHandlers = findAllActionHandlers(latestRootUiWidget); - - const id = await fetch_action_id_for_shortcut(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta); - - if (id) { - const actionHandler = actionHandlers.find(value => value.id === id); - - if (actionHandler) { - actionHandler.onAction() - } - } - } + handlePluginViewKeyboardEvent(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta) break; } } @@ -169,21 +73,22 @@ export async function runPluginLoop() { break; } case "OpenView": { + const entrypointId = pluginEvent.entrypointId try { - if (await checkRequiredPreferencesAndAsk(pluginEvent.entrypointId)) { + if (await checkRequiredPreferencesAndAsk(entrypointId)) { break; } - const View: FC = (await import(`gauntlet:entrypoint?${pluginEvent.entrypointId}`)).default; - latestRootUiWidget = render(pluginEvent.entrypointId, "View", ); + const view: FC = (await import(`gauntlet:entrypoint?${entrypointId}`)).default; + renderView(entrypointId, getEntrypointName(entrypointId), view) } catch (e) { - console.error("Error occurred when rendering view", pluginEvent.entrypointId, e) - show_plugin_error_view(pluginEvent.entrypointId, "View") + console.error("Error occurred when rendering view", entrypointId, e) + show_plugin_error_view(entrypointId, "View") } break; } case "CloseView": { - clearRenderer() + closeView() break; } case "RunCommand": { @@ -208,22 +113,17 @@ export async function runPluginLoop() { break; } case "OpenInlineView": { - const endpointId = op_inline_view_endpoint_id(); + const entrypointId = op_inline_view_entrypoint_id(); - if (endpointId) { - if (await checkRequiredPreferences(endpointId)) { + if (entrypointId) { + if (await checkRequiredPreferences(entrypointId)) { break; } try { - const Handler: FC<{ text: string }> = (await import(`gauntlet:entrypoint?${endpointId}`)).default; + const handler: FC<{ text: string }> = (await import(`gauntlet:entrypoint?${entrypointId}`)).default; - latestRootUiWidget = render(endpointId, "InlineView", ); - - if (latestRootUiWidget.widgetChildren.length === 0) { - op_log_debug("plugin_loop", `Inline view rendered no children, clearing inline view...`) - clear_inline_view() - } + renderInlineView(entrypointId, getEntrypointName(entrypointId), handler, pluginEvent.text) } catch (e) { console.error("Error occurred when rendering inline view", e) } @@ -242,3 +142,14 @@ export async function runPluginLoop() { } } } + +function getEntrypointName(entrypointId: string): string { + const entrypointNames = op_entrypoint_names(); + const entrypointName = entrypointNames[entrypointId]; + + if (entrypointName) { + return entrypointName + } + + throw new Error(`Unable to get entrypoint name for entrypoint id: ${entrypointId}`) +} diff --git a/js/core/src/render.tsx b/js/core/src/render.tsx new file mode 100644 index 0000000..42bb487 --- /dev/null +++ b/js/core/src/render.tsx @@ -0,0 +1,123 @@ +import { + clear_inline_view, + fetch_action_id_for_shortcut, + op_log_debug, + op_log_trace +} from "ext:core/ops"; +import { clearRenderer, render } from "ext:gauntlet/renderer.js"; +import type { FC } from "react"; + +let latestRootUiWidget: UiWidget | undefined = undefined + +export function renderView(entrypointId: string, entrypointName: string, View: FC) { + latestRootUiWidget = render(entrypointId, entrypointName, "View", ); +} + +export function renderInlineView(entrypointId: string, entrypointName: string, Handler: FC<{ text: string }>, text: string) { + latestRootUiWidget = render(entrypointId, entrypointName, "InlineView", ); + + if (latestRootUiWidget.widgetChildren.length === 0) { + op_log_debug("plugin_loop", `Inline view rendered no children, clearing inline view...`) + clear_inline_view() + } +} + +export function closeView() { + clearRenderer() +} + +export async function handlePluginViewKeyboardEvent(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean) { + if (latestRootUiWidget) { + const actionHandlers = findAllActionHandlers(latestRootUiWidget); + + const id = await fetch_action_id_for_shortcut(entrypointId, key, modifierShift, modifierControl, modifierAlt, modifierMeta); + + if (id) { + const actionHandler = actionHandlers.find(value => value.id === id); + + if (actionHandler) { + actionHandler.onAction() + } + } + } +} + +function findAllActionHandlers(widget: UiWidget): { id: string, onAction: () => void }[] { + if (widget.widgetType === "gauntlet:action") { + const id = widget.widgetProperties["id"]; + const onAction = widget.widgetProperties["onAction"]; + if (!!id && !!onAction) { + return [{ id, onAction }] + } else { + return [] + } + } + + let result: { id: string, onAction: () => void }[] = [] + for (let widgetChild of widget.widgetChildren) { + const actionHandler = findAllActionHandlers(widgetChild); + + result.push(...actionHandler) + } + + return result; +} + +export function handleEvent(event: ViewEvent) { + op_log_trace("plugin_event_handler", `Handling view event: ${Deno.inspect(event)}`); + op_log_trace("plugin_event_handler", `Root widget: ${Deno.inspect(latestRootUiWidget)}`); + if (latestRootUiWidget) { + const widgetWithId = findWidgetWithId(latestRootUiWidget, event.widgetId); + op_log_trace("plugin_event_handler", `Found widget with id ${event.widgetId}: ${Deno.inspect(widgetWithId)}`) + + if (widgetWithId) { + const property = widgetWithId.widgetProperties[event.eventName]; + + op_log_trace("plugin_event_handler", `Found event handler with name ${event.eventName}: ${Deno.inspect(property)}`) + + if (property) { + if (typeof property === "function") { + + const eventArgs = event.eventArguments + .map(arg => { + switch (arg.type) { + case "Undefined": { + return undefined + } + case "String": { + return arg.value + } + case "Number": { + return arg.value + } + case "Bool": { + return arg.value + } + } + }); + + op_log_trace("plugin_event_handler", `Calling handler with arguments ${Deno.inspect(eventArgs)}`) + + property(...eventArgs); + } else { + throw new Error(`Event handler has type ${typeof property}, but should be function`) + } + } + } + } +} + +function findWidgetWithId(widget: UiWidget, widgetId: number): UiWidget | undefined { + if (widget.widgetId === widgetId) { + return widget + } + + for (let widgetChild of widget.widgetChildren) { + const widgetWithId = findWidgetWithId(widgetChild, widgetId); + if (widgetWithId) { + return widgetWithId + } + } + + return undefined; +} diff --git a/js/core/typings/index.d.ts b/js/core/typings/index.d.ts index a33cb66..44d9679 100644 --- a/js/core/typings/index.d.ts +++ b/js/core/typings/index.d.ts @@ -1,7 +1,7 @@ declare module "ext:gauntlet/renderer.js" { import { ReactNode } from "react"; - export const render: (entrypointId: string, renderLocation: RenderLocation, component: ReactNode) => UiWidget; + export const render: (entrypointId: string, entrypointName: string, renderLocation: RenderLocation, component: ReactNode) => UiWidget; export const clearRenderer: () => void; } diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index 92380a5..875d2bd 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -47,10 +47,12 @@ class GauntletContextValue { private _renderLocation: RenderLocation | undefined private _rerender: ((node: ReactNode) => void) | undefined private _entrypointId: string | undefined; + private _entrypointName: string | undefined; private _clear: (() => void) | undefined; - reset(entrypointId: string, renderLocation: RenderLocation, view: ReactNode, rerender: (node: ReactNode) => void, clear: () => void) { + reset(entrypointId: string, entrypointName: string, renderLocation: RenderLocation, view: ReactNode, rerender: (node: ReactNode) => void, clear: () => void) { this._entrypointId = entrypointId + this._entrypointName = entrypointName this._renderLocation = renderLocation this._rerender = rerender this._clear = clear @@ -74,6 +76,10 @@ class GauntletContextValue { return this._entrypointId!! } + entrypointName = () => { + return this._entrypointName!! + } + rerender = (component: ReactNode) => { this._rerender!!(component) }; @@ -341,7 +347,13 @@ export const createHostConfig = (): HostConfig< // op_log_info("renderer_js_persistence", `Converted container: ${Deno.inspect(containerComponent, { depth: Number.MAX_VALUE })}`) - op_react_replace_view(gauntletContextValue.renderLocation(), gauntletContextValue.isBottommostView(), gauntletContextValue.entrypointId(), containerComponent) + op_react_replace_view( + gauntletContextValue.renderLocation(), + gauntletContextValue.isBottommostView(), + gauntletContextValue.entrypointId(), + gauntletContextValue.entrypointName(), + containerComponent + ) }, cloneHiddenInstance( @@ -425,7 +437,7 @@ export function clearRenderer() { gauntletContextValue.clear() } -export function render(entrypointId: string, renderLocation: RenderLocation, view: ReactNode): UiWidget { +export function render(entrypointId: string, entrypointName: string, renderLocation: RenderLocation, view: ReactNode): UiWidget { const hostConfig = createHostConfig(); // const reconciler = ReactReconciler(createTracedHostConfig(hostConfig)); @@ -440,6 +452,7 @@ export function render(entrypointId: string, renderLocation: RenderLocation, vie gauntletContextValue.reset( entrypointId, + entrypointName, renderLocation, view, (node: ReactNode) => { diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 774cac1..62183b9 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -123,17 +123,18 @@ interface GeneratedCommandIconAccessory { tooltip?: string } -type AdditionalSearchItem = { +type GeneratedSearchItem = { entrypoint_name: string, entrypoint_id: string, entrypoint_uuid: string, entrypoint_icon: ArrayBuffer | undefined, - entrypoint_actions: AdditionalSearchItemAction[], + entrypoint_actions: GeneratedSearchItemAction[], entrypoint_accessories: GeneratedCommandAccessory[], } -type AdditionalSearchItemAction = { +type GeneratedSearchItemAction = { id?: string, + action_type: "Command" | "View" label: string, } @@ -196,7 +197,8 @@ declare module "ext:core/ops" { function asset_data(path: string): Promise; function asset_data_blocking(path: string): number[]; - function op_inline_view_endpoint_id(): string | null; + function op_inline_view_entrypoint_id(): string | null; + function op_entrypoint_names(): Record; function clear_inline_view(): void; function op_plugin_get_pending_event(): Promise; @@ -208,12 +210,12 @@ declare module "ext:core/ops" { function entrypoint_preferences_required(entrypointId: string): Promise; function show_preferences_required_view(entrypointId: string, pluginPreferencesRequired: boolean, entrypointPreferencesRequired: boolean): void; - function reload_search_index(searchItems: AdditionalSearchItem[], refreshSearchList: boolean): Promise; + function reload_search_index(searchItems: GeneratedSearchItem[], refreshSearchList: boolean): Promise; function show_hud(display: string): void; function update_loading_bar(entrypoint_id: string, show: boolean): void; - function op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, entrypoint_id: string, container: any): void; + function op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, entrypoint_id: string, entrypoint_name: string, container: any): void; function show_plugin_error_view(entrypoint_id: string, render_location: RenderLocation): void; function fetch_action_id_for_shortcut(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean): Promise; diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 42e45f6..7dba7c1 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -24,7 +24,7 @@ use serde::Deserialize; use tokio::sync::{Mutex as TokioMutex, RwLock as TokioRwLock}; use client_context::ClientContext; -use gauntlet_common::model::{BackendRequestData, BackendResponseData, EntrypointId, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; +use gauntlet_common::model::{BackendRequestData, BackendResponseData, EntrypointId, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointActionType, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; use gauntlet_common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFrontendApiError}; use gauntlet_common::scenario_convert::{ui_render_location_from_scenario}; use gauntlet_common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; @@ -97,11 +97,18 @@ pub enum AppMsg { entrypoint_id: EntrypointId, entrypoint_name: String, }, + OpenGeneratedView { + plugin_id: PluginId, + plugin_name: String, + entrypoint_id: EntrypointId, + entrypoint_name: String, + action_index: usize + }, RunCommand { plugin_id: PluginId, entrypoint_id: EntrypointId, }, - RunGeneratedCommandEvent { + RunGeneratedCommand { plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize @@ -542,7 +549,32 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::batch([ state.open_plugin_view(plugin_id, entrypoint_id), - Task::perform(async move { AppMsg::PendingPluginViewLoadingBar }, std::convert::identity) + Task::done(AppMsg::PendingPluginViewLoadingBar) + ]) + } + GlobalState::ErrorView { .. } => { + Task::none() + } + GlobalState::PluginView { .. } => { + Task::none() + } + } + } + AppMsg::OpenGeneratedView { plugin_id, plugin_name, entrypoint_id, entrypoint_name, action_index } => { + match &mut state.global_state { + GlobalState::MainView { pending_plugin_view_data, .. } => { + *pending_plugin_view_data = Some(PluginViewData { + top_level_view: true, + plugin_id: plugin_id.clone(), + plugin_name, + entrypoint_id: entrypoint_id.clone(), + entrypoint_name, + action_shortcuts: HashMap::new(), + }); + + Task::batch([ + state.run_generated_command(plugin_id, entrypoint_id, action_index), + Task::done(AppMsg::PendingPluginViewLoadingBar) ]) } GlobalState::ErrorView { .. } => { @@ -559,7 +591,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { state.run_command(plugin_id, entrypoint_id), ]) } - AppMsg::RunGeneratedCommandEvent { plugin_id, entrypoint_id, action_index } => { + AppMsg::RunGeneratedCommand { plugin_id, entrypoint_id, action_index } => { Task::batch([ state.hide_window(), state.run_generated_command(plugin_id, entrypoint_id, action_index), @@ -599,12 +631,26 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } }, - SearchResultEntrypointType::GeneratedCommand => { - Task::done(AppMsg::RunGeneratedCommandEvent { - entrypoint_id: search_result.entrypoint_id.clone(), - plugin_id: search_result.plugin_id.clone(), - action_index, - }) + SearchResultEntrypointType::Generated => { + let action = &search_result.entrypoint_actions[action_index]; + match &action.action_type { + SearchResultEntrypointActionType::Command => { + Task::done(AppMsg::RunGeneratedCommand { + entrypoint_id: search_result.entrypoint_id.clone(), + plugin_id: search_result.plugin_id.clone(), + action_index, + }) + } + SearchResultEntrypointActionType::View => { + Task::done(AppMsg::OpenGeneratedView { + plugin_id: search_result.plugin_id.clone(), + plugin_name: search_result.plugin_name.clone(), + entrypoint_id: search_result.entrypoint_id.clone(), + entrypoint_name: action.label.clone(), + action_index, + }) + } + } }, } } @@ -1662,7 +1708,7 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { modifier_meta: false, }; - let generate = |label: &str, primary_shortcut: PhysicalShortcut, secondary_shortcut: PhysicalShortcut| { + let create_static = |label: &str, primary_shortcut: PhysicalShortcut, secondary_shortcut: PhysicalShortcut| { let mut actions: Vec<_> = search_item.entrypoint_actions .iter() .enumerate() @@ -1705,42 +1751,44 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { } }; + let create_generated = |label: &str, primary_shortcut: PhysicalShortcut, secondary_shortcut: PhysicalShortcut| { + let label = search_item.entrypoint_actions + .first() + .map(|action| action.label.clone()) + .unwrap_or_else(|| label.to_string()); // should never happen, because there is always at least one action + + let mut actions: Vec<_> = search_item.entrypoint_actions + .iter() + .enumerate() + .map(|(index, action)| { + let physical_shortcut = match index { + 0 => Some(primary_shortcut.clone()), + 1 => Some(secondary_shortcut.clone()), + _ => action.shortcut.clone() + }; + + ActionPanelItem::Action { + label: action.label.clone(), + widget_id: index, + physical_shortcut, + } + }) + .collect(); + + let primary_action_widget_id = 0; + + let action_panel = ActionPanel { + title: Some(search_item.entrypoint_name.clone()), + items: actions, + }; + + (Some((label, primary_action_widget_id, primary_shortcut)), Some(action_panel)) + }; + match search_item.entrypoint_type { - SearchResultEntrypointType::Command => generate("Run Command", primary_shortcut, secondary_shortcut), - SearchResultEntrypointType::View => generate("Open View", primary_shortcut, secondary_shortcut), - SearchResultEntrypointType::GeneratedCommand => { - let label = search_item.entrypoint_actions - .first() - .map(|action| action.label.clone()) - .unwrap_or_else(|| "Run Command".to_string()); - - let mut actions: Vec<_> = search_item.entrypoint_actions - .iter() - .enumerate() - .map(|(index, action)| { - let physical_shortcut = match index { - 0 => Some(primary_shortcut.clone()), - 1 => Some(secondary_shortcut.clone()), - _ => action.shortcut.clone() - }; - - ActionPanelItem::Action { - label: action.label.clone(), - widget_id: index, - physical_shortcut, - } - }) - .collect(); - - let primary_action_widget_id = 0; - - let action_panel = ActionPanel { - title: Some(search_item.entrypoint_name.clone()), - items: actions, - }; - - (Some((label, primary_action_widget_id, primary_shortcut)), Some(action_panel)) - }, + SearchResultEntrypointType::Command => create_static("Run Command", primary_shortcut, secondary_shortcut), + SearchResultEntrypointType::View => create_static("Open View", primary_shortcut, secondary_shortcut), + SearchResultEntrypointType::Generated => create_generated("Run Command", primary_shortcut, secondary_shortcut), } } else { match state.client_context.get_first_inline_view_action_panel() { diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 80cf33c..66d2ca7 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -131,15 +131,22 @@ pub enum SearchResultAccessory { #[derive(Debug, Clone)] pub struct SearchResultEntrypointAction { + pub action_type: SearchResultEntrypointActionType, pub label: String, pub shortcut: Option, } +#[derive(Debug, Clone)] +pub enum SearchResultEntrypointActionType { + Command, + View, +} + #[derive(Debug, Clone)] pub enum SearchResultEntrypointType { Command, View, - GeneratedCommand, + Generated, } #[derive(Debug)] diff --git a/rust/plugin_runtime/src/api.rs b/rust/plugin_runtime/src/api.rs index 226b4b4..c0191c8 100644 --- a/rust/plugin_runtime/src/api.rs +++ b/rust/plugin_runtime/src/api.rs @@ -1,4 +1,4 @@ -use crate::model::{JsAdditionalSearchItem, JsClipboardData, JsPreferenceUserData}; +use crate::model::{JsGeneratedSearchItem, JsClipboardData, JsPreferenceUserData}; use crate::{JsRequest, JsResponse, JsUiRenderLocation}; use gauntlet_common::model::{EntrypointId, RootWidget, UiRenderLocation}; use std::collections::HashMap; @@ -7,7 +7,7 @@ use gauntlet_utils::channel::{RequestError, RequestSender}; #[allow(async_fn_in_trait)] pub trait BackendForPluginRuntimeApi { - async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> ; + async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> ; async fn get_asset_data(&self, path: &str) -> anyhow::Result>; async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result>; async fn get_plugin_preferences(&self) -> anyhow::Result>; @@ -33,6 +33,7 @@ pub trait BackendForPluginRuntimeApi { async fn ui_render( &self, entrypoint_id: EntrypointId, + entrypoint_name: String, render_location: UiRenderLocation, top_level_view: bool, container: RootWidget, @@ -77,7 +78,7 @@ impl BackendForPluginRuntimeApiProxy { } impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { - async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { + async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { let request = JsRequest::ReloadSearchIndex { generated_commands, refresh_search_list, @@ -240,12 +241,14 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { async fn ui_render( &self, entrypoint_id: EntrypointId, + entrypoint_name: String, render_location: UiRenderLocation, top_level_view: bool, container: RootWidget, ) -> anyhow::Result<()> { let request = JsRequest::Render { entrypoint_id, + entrypoint_name, render_location: match render_location { UiRenderLocation::InlineView => JsUiRenderLocation::InlineView, UiRenderLocation::View => JsUiRenderLocation::View diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index 5ed7710..226ae35 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -34,7 +34,7 @@ use crate::plugins::numbat::{run_numbat, NumbatContext}; use crate::plugins::settings::open_settings; use crate::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; use crate::search::reload_search_index; -use crate::ui::{clear_inline_view, fetch_action_id_for_shortcut, op_component_model, op_inline_view_endpoint_id, op_react_replace_view, show_hud, show_plugin_error_view, show_preferences_required_view, update_loading_bar}; +use crate::ui::{clear_inline_view, fetch_action_id_for_shortcut, op_component_model, op_entrypoint_names, op_inline_view_entrypoint_id, op_react_replace_view, show_hud, show_plugin_error_view, show_preferences_required_view, update_loading_bar}; @@ -174,7 +174,8 @@ deno_core::extension!( // ui op_react_replace_view, - op_inline_view_endpoint_id, + op_inline_view_entrypoint_id, + op_entrypoint_names, show_plugin_error_view, clear_inline_view, show_preferences_required_view, @@ -387,6 +388,7 @@ pub async fn start_js_runtime( init.plugin_cache_dir, init.plugin_data_dir, init.inline_view_entrypoint_id, + init.entrypoint_names, home_dir ), ComponentModel::new(), diff --git a/rust/plugin_runtime/src/model.rs b/rust/plugin_runtime/src/model.rs index 816e257..d4de5c5 100644 --- a/rust/plugin_runtime/src/model.rs +++ b/rust/plugin_runtime/src/model.rs @@ -30,6 +30,7 @@ pub struct JsInit { pub code: JsPluginCode, pub permissions: JsPluginPermissions, pub inline_view_entrypoint_id: Option, + pub entrypoint_names: HashMap, pub dev_plugin: bool, pub home_dir: String, pub local_storage_dir: String, @@ -108,6 +109,7 @@ pub enum JsResponse { pub enum JsRequest { Render { entrypoint_id: EntrypointId, + entrypoint_name: String, render_location: JsUiRenderLocation, top_level_view: bool, container: RootWidget, @@ -130,7 +132,7 @@ pub enum JsRequest { show: bool }, ReloadSearchIndex { - generated_commands: Vec, + generated_commands: Vec, refresh_search_list: bool }, GetAssetData { @@ -165,35 +167,44 @@ pub enum JsRequest { } #[derive(Deserialize, Serialize, Encode, Decode)] -pub struct JsAdditionalSearchItem { +pub struct JsGeneratedSearchItem { pub entrypoint_name: String, pub generator_entrypoint_id: String, pub entrypoint_id: String, pub entrypoint_uuid: String, pub entrypoint_icon: Option>, - pub entrypoint_actions: Vec, - pub entrypoint_accessories: Vec, + pub entrypoint_actions: Vec, + pub entrypoint_accessories: Vec, } -impl fmt::Debug for JsAdditionalSearchItem { +impl fmt::Debug for JsGeneratedSearchItem { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { // exclude entrypoint_icon - fmt.debug_struct("JsAdditionalSearchItem") + fmt.debug_struct("JsGeneratedSearchItem") .field("entrypoint_name", &self.entrypoint_name) .field("generator_entrypoint_id", &self.generator_entrypoint_id) .field("entrypoint_id", &self.entrypoint_id) .field("entrypoint_uuid", &self.entrypoint_uuid) .field("entrypoint_actions", &self.entrypoint_actions) + .field("entrypoint_accessories", &self.entrypoint_accessories) .finish() } } #[derive(Debug, Deserialize, Serialize, Encode, Decode)] -pub struct JsAdditionalSearchItemAction { +pub struct JsGeneratedSearchItemAction { pub id: Option, + pub action_type: JsGeneratedSearchItemActionType, pub label: String, } + +#[derive(Debug, Deserialize, Serialize, Encode, Decode)] +pub enum JsGeneratedSearchItemActionType { + View, + Command, +} + #[derive(Debug, Deserialize, Serialize, Encode, Decode)] #[serde(untagged)] pub enum JsPreferenceUserData { @@ -206,7 +217,7 @@ pub enum JsPreferenceUserData { #[derive(Debug, Deserialize, Serialize, Encode, Decode)] #[serde(untagged)] -pub enum JsAdditionalSearchItemAccessory { +pub enum JsGeneratedSearchItemAccessory { TextAccessory { text: String, icon: Option, diff --git a/rust/plugin_runtime/src/plugin_data.rs b/rust/plugin_runtime/src/plugin_data.rs index 8ac1ff3..13fb7be 100644 --- a/rust/plugin_runtime/src/plugin_data.rs +++ b/rust/plugin_runtime/src/plugin_data.rs @@ -1,5 +1,6 @@ +use gauntlet_common::model::{EntrypointId, PluginId}; +use std::collections::HashMap; use std::path::PathBuf; -use gauntlet_common::model::PluginId; pub struct PluginData { plugin_id: PluginId, @@ -7,6 +8,7 @@ pub struct PluginData { plugin_cache_dir: String, plugin_data_dir: String, inline_view_entrypoint_id: Option, + entrypoint_names: HashMap, home_dir: PathBuf, } @@ -17,14 +19,21 @@ impl PluginData { plugin_cache_dir: String, plugin_data_dir: String, inline_view_entrypoint_id: Option, + entrypoint_names: HashMap, home_dir: PathBuf, ) -> Self { + let entrypoint_names = entrypoint_names + .into_iter() + .map(|(entrypoint_id, value)| (entrypoint_id.to_string(), value)) + .collect(); + Self { plugin_id, plugin_uuid, plugin_cache_dir, plugin_data_dir, inline_view_entrypoint_id, + entrypoint_names, home_dir } } @@ -45,8 +54,12 @@ impl PluginData { &self.plugin_data_dir } - pub fn inline_view_entrypoint_id(&self) -> Option { - self.inline_view_entrypoint_id.clone() + pub fn inline_view_entrypoint_id(&self) -> &Option { + &self.inline_view_entrypoint_id + } + + pub fn entrypoint_names(&self) -> &HashMap { + &self.entrypoint_names } pub fn home_dir(&self) -> PathBuf { diff --git a/rust/plugin_runtime/src/search.rs b/rust/plugin_runtime/src/search.rs index 0a2ce30..d0dadcc 100644 --- a/rust/plugin_runtime/src/search.rs +++ b/rust/plugin_runtime/src/search.rs @@ -2,10 +2,10 @@ use deno_core::{op2, OpState}; use std::cell::RefCell; use std::rc::Rc; use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; -use crate::model::JsAdditionalSearchItem; +use crate::model::JsGeneratedSearchItem; #[op2(async)] -pub async fn reload_search_index(state: Rc>, #[serde] generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { +pub async fn reload_search_index(state: Rc>, #[serde] generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { let api = { let state = state.borrow(); diff --git a/rust/plugin_runtime/src/ui.rs b/rust/plugin_runtime/src/ui.rs index 8a42f91..c9939f4 100644 --- a/rust/plugin_runtime/src/ui.rs +++ b/rust/plugin_runtime/src/ui.rs @@ -86,13 +86,22 @@ pub fn clear_inline_view(state: Rc>) -> anyhow::Result<()> { #[op2] #[string] -pub fn op_inline_view_endpoint_id(state: Rc>) -> Option { +pub fn op_inline_view_entrypoint_id(state: Rc>) -> Option { state.borrow() .borrow::() .inline_view_entrypoint_id() .clone() } +#[op2] +#[serde] +pub fn op_entrypoint_names(state: Rc>) -> HashMap { + state.borrow() + .borrow::() + .entrypoint_names() + .clone() +} + #[op2] pub fn op_react_replace_view<'a>( scope: &mut v8::HandleScope, @@ -100,6 +109,7 @@ pub fn op_react_replace_view<'a>( #[serde] render_location: JsUiRenderLocation, top_level_view: bool, #[string] entrypoint_id: &str, + #[string] entrypoint_name: &str, #[serde] container: serde_v8::Value<'a>, ) -> anyhow::Result<()> { tracing::trace!(target = "renderer_rs", "Calling op_react_replace_view..."); @@ -109,6 +119,7 @@ pub fn op_react_replace_view<'a>( let container = RootWidget::deserialize(&mut deserializer)?; let entrypoint_id = EntrypointId::from_string(entrypoint_id); + let entrypoint_name = entrypoint_name.to_string(); let (api, outer_handle) = { let state = state.borrow(); @@ -133,6 +144,7 @@ pub fn op_react_replace_view<'a>( outer_handle.spawn(async move { api.ui_render( entrypoint_id, + entrypoint_name, render_location, top_level_view, container, diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 6cf2852..658e550 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -29,13 +29,13 @@ use gauntlet_common::dirs::Dirs; use gauntlet_common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, SearchResultAccessory, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; use gauntlet_common::rpc::frontend_api::FrontendApi; use gauntlet_common::settings_env_data_to_string; -use gauntlet_plugin_runtime::{recv_message, send_message, BackendForPluginRuntimeApi, JsAdditionalSearchItem, JsClipboardData, JsInit, JsKeyboardEventOrigin, JsPluginCode, JsPluginPermissions, JsPreferenceUserData, JsEvent, JsUiPropertyValue, JsRequest, JsUiRenderLocation, JsResponse, JsMessage, JsPluginPermissionsFileSystem, JsPluginPermissionsExec, JsPluginPermissionsMainSearchBar, JsMessageSide, JsPluginRuntimeMessage, JsAdditionalSearchItemAccessory}; +use gauntlet_plugin_runtime::{recv_message, send_message, BackendForPluginRuntimeApi, JsGeneratedSearchItem, JsClipboardData, JsInit, JsKeyboardEventOrigin, JsPluginCode, JsPluginPermissions, JsPreferenceUserData, JsEvent, JsUiPropertyValue, JsRequest, JsUiRenderLocation, JsResponse, JsMessage, JsPluginPermissionsFileSystem, JsPluginPermissionsExec, JsPluginPermissionsMainSearchBar, JsMessageSide, JsPluginRuntimeMessage, JsGeneratedSearchItemAccessory, JsGeneratedSearchItemActionType}; use crate::model::{IntermediateUiEvent}; use crate::plugins::clipboard::Clipboard; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; use crate::plugins::run_status::RunStatusGuard; -use crate::search::{SearchIndex, SearchIndexItem, SearchIndexItemAction}; +use crate::search::{SearchIndex, SearchIndexItem, SearchIndexItemAction, SearchIndexItemActionActionType}; use crate::{PLUGIN_RUNTIME_ENV, SETTINGS_ENV}; use crate::plugins::image_gatherer::ImageGatherer; @@ -142,7 +142,6 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run data.uuid.clone(), data.id.clone(), data.name, - data.entrypoint_names, runtime_permissions, ); @@ -246,6 +245,7 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run code: data.code, permissions, inline_view_entrypoint_id: data.inline_view_entrypoint_id, + entrypoint_names: data.entrypoint_names, dev_plugin, home_dir, local_storage_dir, @@ -474,13 +474,13 @@ async fn request_loop(recv: &mut RecvHalf, send: &Mutex, api: &Backend async fn handle_message(message: JsRequest, api: &BackendForPluginRuntimeApiImpl) -> anyhow::Result { match message { - JsRequest::Render { entrypoint_id, render_location, top_level_view, container } => { + JsRequest::Render { entrypoint_id, entrypoint_name, render_location, top_level_view, container } => { let render_location = match render_location { JsUiRenderLocation::InlineView => UiRenderLocation::InlineView, JsUiRenderLocation::View => UiRenderLocation::View }; - api.ui_render(entrypoint_id, render_location, top_level_view, container).await?; + api.ui_render(entrypoint_id, entrypoint_name, render_location, top_level_view, container).await?; Ok(JsResponse::Nothing) } @@ -669,7 +669,6 @@ pub struct BackendForPluginRuntimeApiImpl { plugin_uuid: String, plugin_id: PluginId, plugin_name: String, - entrypoint_names: HashMap, permissions: PluginRuntimePermissions } @@ -683,7 +682,6 @@ impl BackendForPluginRuntimeApiImpl { plugin_uuid: String, plugin_id: PluginId, plugin_name: String, - entrypoint_names: HashMap, permissions: PluginRuntimePermissions ) -> Self { Self { @@ -695,14 +693,13 @@ impl BackendForPluginRuntimeApiImpl { plugin_uuid, plugin_id, plugin_name, - entrypoint_names, permissions } } } impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { - async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { + async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { self.icon_cache.clear_plugin_icon_cache_dir(&self.plugin_uuid) .context("error when clearing up icon cache before recreating it")?; @@ -748,6 +745,10 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { SearchIndexItemAction { label: action.label.clone(), + action_type: match action.action_type { + JsGeneratedSearchItemActionType::View => SearchIndexItemActionActionType::View, + JsGeneratedSearchItemActionType::Command => SearchIndexItemActionActionType::Command, + }, shortcut, } }) @@ -756,10 +757,10 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { let entrypoint_accessories = item.entrypoint_accessories.into_iter() .map(|accessory| { match accessory { - JsAdditionalSearchItemAccessory::TextAccessory { text, icon, tooltip } => { + JsGeneratedSearchItemAccessory::TextAccessory { text, icon, tooltip } => { SearchResultAccessory::TextAccessory { text, icon, tooltip } } - JsAdditionalSearchItemAccessory::IconAccessory { icon, tooltip } => { + JsGeneratedSearchItemAccessory::IconAccessory { icon, tooltip } => { SearchResultAccessory::IconAccessory { icon, tooltip } } } @@ -767,7 +768,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { .collect(); Ok(SearchIndexItem { - entrypoint_type: SearchResultEntrypointType::GeneratedCommand, + entrypoint_type: SearchResultEntrypointType::Generated, entrypoint_id: EntrypointId::from_string(item.entrypoint_id), entrypoint_name: item.entrypoint_name, entrypoint_icon_path, @@ -1012,16 +1013,12 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { async fn ui_render( &self, entrypoint_id: EntrypointId, + entrypoint_name: String, render_location: UiRenderLocation, top_level_view: bool, container: RootWidget, ) -> anyhow::Result<()> { - let entrypoint_name = self.entrypoint_names - .get(&entrypoint_id) - .expect("entrypoint name for id should always exist") - .to_string(); - let images = ImageGatherer::run_gatherer(&self, &container).await?; self.frontend_api.replace_view( diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index c5edb03..47ee13d 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -6,7 +6,7 @@ use tantivy::collector::TopDocs; use tantivy::query::{AllQuery, BooleanQuery, FuzzyTermQuery, Query, RegexQuery, TermQuery}; use tantivy::schema::*; use tantivy::tokenizer::TokenizerManager; -use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult, SearchResultAccessory, SearchResultEntrypointAction, SearchResultEntrypointType}; +use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult, SearchResultAccessory, SearchResultEntrypointAction, SearchResultEntrypointActionType, SearchResultEntrypointType}; use gauntlet_common::rpc::frontend_api::FrontendApi; #[derive(Clone)] @@ -34,9 +34,15 @@ struct EntrypointData { struct EntrypointActionData { label: String, + action_type: EntrypointActionType, shortcut: Option, } +enum EntrypointActionType { + Command, + View, +} + #[derive(Clone, Debug)] pub struct SearchIndexItem { pub entrypoint_type: SearchResultEntrypointType, @@ -51,9 +57,17 @@ pub struct SearchIndexItem { #[derive(Clone, Debug)] pub struct SearchIndexItemAction { pub label: String, + pub action_type: SearchIndexItemActionActionType, pub shortcut: Option, } +#[derive(Debug, Clone)] +pub enum SearchIndexItemActionActionType { + Command, + View, +} + + impl SearchIndex { pub fn create_index(frontend_api: FrontendApi) -> tantivy::Result { let schema = { @@ -140,6 +154,10 @@ impl SearchIndex { let actions = item.entrypoint_actions.into_iter() .map(|action| EntrypointActionData { label: action.label, + action_type: match action.action_type { + SearchIndexItemActionActionType::Command => EntrypointActionType::Command, + SearchIndexItemActionActionType::View => EntrypointActionType::View, + }, shortcut: action.shortcut, }) .collect(); @@ -254,6 +272,10 @@ impl SearchIndex { let entrypoint_actions = entrypoint_data.actions.iter() .map(|data| SearchResultEntrypointAction { + action_type: match data.action_type { + EntrypointActionType::Command => SearchResultEntrypointActionType::Command, + EntrypointActionType::View => SearchResultEntrypointActionType::View + }, label: data.label.clone(), shortcut: data.shortcut.clone(), }) From 1c1deca67ece7b8d1fa33fad4af59c0fe864cba9 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 25 Dec 2024 23:59:45 +0100 Subject: [PATCH 248/540] Rename command-generator entrypoint type into entrypoint-generator --- CHANGELOG.md | 2 +- bundled_plugins/gauntlet/gauntlet.toml | 2 +- bundled_plugins/gauntlet/package.json | 2 +- dev_plugin/gauntlet.toml | 8 ++++---- dev_plugin/package.json | 2 +- ...generator.tsx => entrypoint-generator.tsx} | 2 +- js/core/src/core.tsx | 6 +++--- ...d-generator.ts => entrypoint-generator.ts} | 20 +++++++++---------- js/core/src/search-index.ts | 2 +- js/typings/index.d.ts | 2 +- package-lock.json | 6 +++--- rust/common/src/model.rs | 2 +- rust/common/src/rpc/backend_api.rs | 2 +- rust/common/src/rpc/backend_server.rs | 2 +- .../src/views/plugins/table.rs | 2 +- rust/plugin_runtime/src/api.rs | 8 ++++---- rust/plugin_runtime/src/deno.rs | 6 +++--- ...generators.rs => entrypoint_generators.rs} | 4 ++-- rust/plugin_runtime/src/lib.rs | 2 +- rust/plugin_runtime/src/model.rs | 4 ++-- rust/server/src/plugins/data_db_repository.rs | 6 +++--- rust/server/src/plugins/js.rs | 12 +++++------ rust/server/src/plugins/loader.rs | 6 +++--- rust/server/src/plugins/mod.rs | 2 +- schema/backend.proto | 2 +- 25 files changed, 57 insertions(+), 57 deletions(-) rename dev_plugin/src/{command-generator.tsx => entrypoint-generator.tsx} (96%) rename js/core/src/{command-generator.ts => entrypoint-generator.ts} (87%) rename rust/plugin_runtime/src/{command_generators.rs => entrypoint_generators.rs} (67%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eeefbe..27a44db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ### Plugin API - Added `` component in `` and `` which is text input field above content of the respective view - `"command-generator"` entrypoints have been reworked - - Now it is possible to update list of generated entrypoints (add or remove) after the main command generator function has finished running + - Now it is possible to update list of generated entrypoints (add or remove) after the main entrypoint generator function has finished running - **BREAKING CHANGE**: Command Generator entrypoint function now accepts an object with `add: (id: string, data: GeneratedCommand) => void` and `remove: (id: string) => void` functions - **BREAKING CHANGE**: Command Generator entrypoint function now should return nothing or a cleanup function e.g. close file watcher. Currently, it is called when disabling/enabling any of entrypoints in plugin, but it is not called when whole plugin is stopped - While generator function itself is running (given that the function is async) the loading bar and "Indexing..." text in bottom panel will be shown in main window diff --git a/bundled_plugins/gauntlet/gauntlet.toml b/bundled_plugins/gauntlet/gauntlet.toml index 035b002..278f6a0 100644 --- a/bundled_plugins/gauntlet/gauntlet.toml +++ b/bundled_plugins/gauntlet/gauntlet.toml @@ -6,7 +6,7 @@ description = 'Default Gauntlet functionality as a bundled plugin' id = 'applications' name = 'Applications' path = 'src/applications.ts' -type = 'command-generator' +type = 'entrypoint-generator' description = 'Run installed applications from your system' [[entrypoint]] diff --git a/bundled_plugins/gauntlet/package.json b/bundled_plugins/gauntlet/package.json index 677f5dd..e1f8ed9 100644 --- a/bundled_plugins/gauntlet/package.json +++ b/bundled_plugins/gauntlet/package.json @@ -12,7 +12,7 @@ }, "devDependencies": { "@types/deno": "^2.0.0", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#6b77be418d6eb48c4139979ff1b8d0350c5b5268", "@types/react": "^18.3.18", "typescript": "^5.7.2" } diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index f190c3e..40e026f 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -163,10 +163,10 @@ type = 'view' description = '' [[entrypoint]] -id = 'command-generator' -name = 'Command generator' -path = 'src/command-generator.tsx' -type = 'command-generator' +id = 'entrypoint-generator' +name = 'Entrypoint generator' +path = 'src/entrypoint-generator.tsx' +type = 'entrypoint-generator' description = '' [[entrypoint.actions]] diff --git a/dev_plugin/package.json b/dev_plugin/package.json index f5ad565..887bd38 100644 --- a/dev_plugin/package.json +++ b/dev_plugin/package.json @@ -13,7 +13,7 @@ "devDependencies": { "@types/react": "^18.3.18", "@types/deno": "^2.0.0", - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#6b77be418d6eb48c4139979ff1b8d0350c5b5268", "typescript": "^5.7.2" } } diff --git a/dev_plugin/src/command-generator.tsx b/dev_plugin/src/entrypoint-generator.tsx similarity index 96% rename from dev_plugin/src/command-generator.tsx rename to dev_plugin/src/entrypoint-generator.tsx index 33e2cf1..4861c01 100644 --- a/dev_plugin/src/command-generator.tsx +++ b/dev_plugin/src/entrypoint-generator.tsx @@ -11,7 +11,7 @@ function ListView(): ReactElement { } -export default function CommandGenerator({ add, remove: _ }: GeneratorProps): void { +export default function EntrypointGenerator({ add, remove: _ }: GeneratorProps): void { add('generated-test-1', { name: 'Generated Item 1', actions: [ diff --git a/js/core/src/core.tsx b/js/core/src/core.tsx index 76684c9..bd8c519 100644 --- a/js/core/src/core.tsx +++ b/js/core/src/core.tsx @@ -1,5 +1,5 @@ import type { FC } from "react"; -import { runCommandGenerators, runGeneratedCommand, runGeneratedCommandAction } from "./command-generator"; +import { runEntrypointGenerators, runGeneratedCommand, runGeneratedCommandAction } from "./entrypoint-generator"; import { reloadSearchIndex } from "./search-index"; import { closeView, handleEvent, handlePluginViewKeyboardEvent, renderInlineView, renderView } from "./render"; import { @@ -47,7 +47,7 @@ async function checkRequiredPreferencesAndAsk(entrypointId: string): Promise (void | Promis let storedGeneratedCommands: ProcessedGeneratedCommands = {} let generatorCleanups: GeneratorCleanups = {} -export async function runCommandGenerators(): Promise { +export async function runEntrypointGenerators(): Promise { for (let [generatorEntrypointId, cleanup] of Object.entries(generatorCleanups)) { try { await cleanup() @@ -83,15 +83,15 @@ export async function runCommandGenerators(): Promise { await reloadSearchIndex(true) - const entrypointIds = await get_command_generator_entrypoint_ids(); + const entrypointIds = await get_entrypoint_generator_entrypoint_ids(); for (const generatorEntrypointId of entrypointIds) { try { const generator: Generator = (await import(`gauntlet:entrypoint?${generatorEntrypointId}`)).default; - op_log_info("command_generator", `Running command generator entrypoint ${generatorEntrypointId}`) + op_log_info("entrypoint_generator", `Running entrypoint generator entrypoint ${generatorEntrypointId}`) const add = (id: string, data: GeneratedCommand) => { - op_log_info("command_generator", `Adding entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) + op_log_info("entrypoint_generator", `Adding entry '${id}' by entrypoint generator entrypoint '${generatorEntrypointId}'`) if (data.actions.length < 1) { throw new Error(`Error when adding entry '${id}': at least one action should be provided`) @@ -141,7 +141,7 @@ export async function runCommandGenerators(): Promise { reloadSearchIndex(true) } const remove = (id: string) => { - op_log_info("command_generator", `Removing entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) + op_log_info("entrypoint_generator", `Removing entry '${id}' by entrypoint generator entrypoint '${generatorEntrypointId}'`) const lookupId = generatorEntrypointId + ":" + id; delete storedGeneratedCommands[lookupId] @@ -150,7 +150,7 @@ export async function runCommandGenerators(): Promise { } const get = (id: string) => { - op_log_debug("command_generator", `Getting entry '${id}' by command generator entrypoint '${generatorEntrypointId}'`) + op_log_debug("entrypoint_generator", `Getting entry '${id}' by entrypoint generator entrypoint '${generatorEntrypointId}'`) const lookupId = generatorEntrypointId + ":" + id; const generatedCommand = storedGeneratedCommands[lookupId]; @@ -162,7 +162,7 @@ export async function runCommandGenerators(): Promise { } const getAll = (): GeneratedCommand[] => { - op_log_debug("command_generator", `Getting all entries by command generator entrypoint '${generatorEntrypointId}'`) + op_log_debug("entrypoint_generator", `Getting all entries by entrypoint generator entrypoint '${generatorEntrypointId}'`) return Object.entries(storedGeneratedCommands) .map(([_, value]) => value.command) @@ -178,11 +178,11 @@ export async function runCommandGenerators(): Promise { generatorCleanups[generatorEntrypointId] = cleanup } } catch (e) { - console.error(`Error occurred when calling command generator for entrypoint: ${generatorEntrypointId}`, e) + console.error(`Error occurred when calling entrypoint generator for entrypoint: ${generatorEntrypointId}`, e) } })() } catch (e) { - console.error(`Error occurred when importing command generator for entrypoint: ${generatorEntrypointId}`, e) + console.error(`Error occurred when importing entrypoint generator for entrypoint: ${generatorEntrypointId}`, e) } } } diff --git a/js/core/src/search-index.ts b/js/core/src/search-index.ts index 0ae9ba8..e890c5c 100644 --- a/js/core/src/search-index.ts +++ b/js/core/src/search-index.ts @@ -1,4 +1,4 @@ -import { generatedCommandSearchIndex } from "./command-generator"; +import { generatedCommandSearchIndex } from "./entrypoint-generator"; import { reload_search_index } from "ext:core/ops"; export async function reloadSearchIndex(refreshSearchList: boolean) { diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 62183b9..e890f62 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -202,7 +202,7 @@ declare module "ext:core/ops" { function clear_inline_view(): void; function op_plugin_get_pending_event(): Promise; - function get_command_generator_entrypoint_ids(): Promise + function get_entrypoint_generator_entrypoint_ids(): Promise function get_plugin_preferences(): Record; function get_entrypoint_preferences(entrypointId: string): Record; diff --git a/package-lock.json b/package-lock.json index b1719ac..0f18683 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@std/fs": "npm:@jsr/std__fs@^1.0.8" }, "devDependencies": { - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#6b77be418d6eb48c4139979ff1b8d0350c5b5268", "@types/deno": "^2.0.0", "@types/react": "^18.3.18", "typescript": "^5.7.2" @@ -42,7 +42,7 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "@project-gauntlet/tools": "git://github.com/project-gauntlet/tools.git#6b77be418d6eb48c4139979ff1b8d0350c5b5268", "@types/deno": "^2.0.0", "@types/react": "^18.3.18", "typescript": "^5.7.2" @@ -716,7 +716,7 @@ }, "node_modules/@project-gauntlet/tools": { "version": "0.9.0", - "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#480520d3b63a1179dacbee7ba3948c4be4742b68", + "resolved": "git+ssh://git@github.com/project-gauntlet/tools.git#6b77be418d6eb48c4139979ff1b8d0350c5b5268", "integrity": "sha512-2vPxXVErfhJKMEfjf9ZtxgXO5HV9VJ6sjSK73eQRxS55pO+OKGZ5I7b8elhkBhPcHXPlNgmx8ZAhuLXJ86NPYg==", "dev": true, "dependencies": { diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 66d2ca7..fed28de 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -586,7 +586,7 @@ pub enum SettingsEntrypointType { Command, View, InlineView, - CommandGenerator, + EntrypointGenerator, } #[derive(Debug, Clone)] diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index caecc7d..869827b 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -285,7 +285,7 @@ impl BackendApi { RpcEntrypointTypeSettings::SCommand => SettingsEntrypointType::Command, RpcEntrypointTypeSettings::SView => SettingsEntrypointType::View, RpcEntrypointTypeSettings::SInlineView => SettingsEntrypointType::InlineView, - RpcEntrypointTypeSettings::SCommandGenerator => SettingsEntrypointType::CommandGenerator + RpcEntrypointTypeSettings::SEntrypointGenerator => SettingsEntrypointType::EntrypointGenerator }; let entrypoint = SettingsEntrypoint { diff --git a/rust/common/src/rpc/backend_server.rs b/rust/common/src/rpc/backend_server.rs index f79c519..cc9e8a0 100644 --- a/rust/common/src/rpc/backend_server.rs +++ b/rust/common/src/rpc/backend_server.rs @@ -132,7 +132,7 @@ impl RpcBackend for RpcBackendServerImpl { SettingsEntrypointType::Command => RpcEntrypointTypeSettings::SCommand, SettingsEntrypointType::View => RpcEntrypointTypeSettings::SView, SettingsEntrypointType::InlineView => RpcEntrypointTypeSettings::SInlineView, - SettingsEntrypointType::CommandGenerator => RpcEntrypointTypeSettings::SCommandGenerator, + SettingsEntrypointType::EntrypointGenerator => RpcEntrypointTypeSettings::SEntrypointGenerator, }.into(), preferences: entrypoint.preferences.into_iter() .map(|(key, value)| (key, plugin_preference_to_rpc(value))) diff --git a/rust/management_client/src/views/plugins/table.rs b/rust/management_client/src/views/plugins/table.rs index f653033..b6d697b 100644 --- a/rust/management_client/src/views/plugins/table.rs +++ b/rust/management_client/src/views/plugins/table.rs @@ -331,7 +331,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo SettingsEntrypointType::Command => "Command", SettingsEntrypointType::View => "View", SettingsEntrypointType::InlineView => "Inline View", - SettingsEntrypointType::CommandGenerator => "Command Generator" + SettingsEntrypointType::EntrypointGenerator => "Entrypoint Generator" }; container(text(entrypoint_type.to_string())) diff --git a/rust/plugin_runtime/src/api.rs b/rust/plugin_runtime/src/api.rs index c0191c8..df1a5ba 100644 --- a/rust/plugin_runtime/src/api.rs +++ b/rust/plugin_runtime/src/api.rs @@ -9,7 +9,7 @@ use gauntlet_utils::channel::{RequestError, RequestSender}; pub trait BackendForPluginRuntimeApi { async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> ; async fn get_asset_data(&self, path: &str) -> anyhow::Result>; - async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result>; + async fn get_entrypoint_generator_entrypoint_ids(&self) -> anyhow::Result>; async fn get_plugin_preferences(&self) -> anyhow::Result>; async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result>; async fn plugin_preferences_required(&self) -> anyhow::Result; @@ -101,11 +101,11 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { } } - async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result> { - let request = JsRequest::GetCommandGeneratorEntrypointIds; + async fn get_entrypoint_generator_entrypoint_ids(&self) -> anyhow::Result> { + let request = JsRequest::GetEntrypointGeneratorEntrypointIds; match self.request(request).await? { - JsResponse::CommandGeneratorEntrypointIds { data } => Ok(data), + JsResponse::EntrypointGeneratorEntrypointIds { data } => Ok(data), value @ _ => panic!("Unexpected JsResponse type: {:?}", value) } } diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index 226ae35..ca89c60 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -20,7 +20,7 @@ use gauntlet_common::model::PluginId; use crate::api::BackendForPluginRuntimeApiProxy; use crate::assets::{asset_data, asset_data_blocking}; use crate::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; -use crate::command_generators::get_command_generator_entrypoint_ids; +use crate::entrypoint_generators::get_entrypoint_generator_entrypoint_ids; use crate::component_model::ComponentModel; use crate::environment::{environment_gauntlet_version, environment_is_development, environment_plugin_cache_dir, environment_plugin_data_dir}; use crate::events::{op_plugin_get_pending_event, EventReceiver, JsEvent}; @@ -165,8 +165,8 @@ deno_core::extension!( op_log_warn, op_log_error, - // command generators - get_command_generator_entrypoint_ids, + // entrypoint generators + get_entrypoint_generator_entrypoint_ids, // assets asset_data, diff --git a/rust/plugin_runtime/src/command_generators.rs b/rust/plugin_runtime/src/entrypoint_generators.rs similarity index 67% rename from rust/plugin_runtime/src/command_generators.rs rename to rust/plugin_runtime/src/entrypoint_generators.rs index 366e1d6..70bb2d3 100644 --- a/rust/plugin_runtime/src/command_generators.rs +++ b/rust/plugin_runtime/src/entrypoint_generators.rs @@ -5,7 +5,7 @@ use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; #[op2(async)] #[serde] -pub async fn get_command_generator_entrypoint_ids(state: Rc>) -> anyhow::Result> { +pub async fn get_entrypoint_generator_entrypoint_ids(state: Rc>) -> anyhow::Result> { let api = { let state = state.borrow(); @@ -16,5 +16,5 @@ pub async fn get_command_generator_entrypoint_ids(state: Rc>) - api }; - api.get_command_generator_entrypoint_ids().await + api.get_entrypoint_generator_entrypoint_ids().await } diff --git a/rust/plugin_runtime/src/lib.rs b/rust/plugin_runtime/src/lib.rs index 870103a..c3c63c2 100644 --- a/rust/plugin_runtime/src/lib.rs +++ b/rust/plugin_runtime/src/lib.rs @@ -1,7 +1,7 @@ mod api; mod assets; mod clipboard; -mod command_generators; +mod entrypoint_generators; mod component_model; mod deno; mod environment; diff --git a/rust/plugin_runtime/src/model.rs b/rust/plugin_runtime/src/model.rs index d4de5c5..39b194f 100644 --- a/rust/plugin_runtime/src/model.rs +++ b/rust/plugin_runtime/src/model.rs @@ -79,7 +79,7 @@ pub enum JsResponse { AssetData { data: Vec }, - CommandGeneratorEntrypointIds { + EntrypointGeneratorEntrypointIds { data: Vec }, PluginPreferences { @@ -138,7 +138,7 @@ pub enum JsRequest { GetAssetData { path: String, }, - GetCommandGeneratorEntrypointIds, + GetEntrypointGeneratorEntrypointIds, GetPluginPreferences, GetEntrypointPreferences { entrypoint_id: EntrypointId, diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 17f8c95..db08220 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -102,7 +102,7 @@ pub enum DbPluginEntrypointType { Command, View, InlineView, - CommandGenerator, + EntrypointGenerator, } #[derive(Debug, Clone)] @@ -1077,7 +1077,7 @@ pub fn db_entrypoint_to_str(value: DbPluginEntrypointType) -> &'static str { DbPluginEntrypointType::Command => "command", DbPluginEntrypointType::View => "view", DbPluginEntrypointType::InlineView => "inline-view", - DbPluginEntrypointType::CommandGenerator => "command-generator" + DbPluginEntrypointType::EntrypointGenerator => "command-generator" // command-generator in db for backwards compatibility } } @@ -1086,7 +1086,7 @@ pub fn db_entrypoint_from_str(value: &str) -> DbPluginEntrypointType { "command" => DbPluginEntrypointType::Command, "view" => DbPluginEntrypointType::View, "inline-view" => DbPluginEntrypointType::InlineView, - "command-generator" => DbPluginEntrypointType::CommandGenerator, + "command-generator" => DbPluginEntrypointType::EntrypointGenerator, _ => panic!("illegal entrypoint_type: {}", value) } } diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 658e550..aa09b43 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -526,10 +526,10 @@ async fn handle_message(message: JsRequest, api: &BackendForPluginRuntimeApiImpl data }) } - JsRequest::GetCommandGeneratorEntrypointIds => { - let data = api.get_command_generator_entrypoint_ids().await?; + JsRequest::GetEntrypointGeneratorEntrypointIds => { + let data = api.get_entrypoint_generator_entrypoint_ids().await?; - Ok(JsResponse::CommandGeneratorEntrypointIds { + Ok(JsResponse::EntrypointGeneratorEntrypointIds { data }) } @@ -835,7 +835,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { entrypoint_accessories: vec![], })) }, - DbPluginEntrypointType::CommandGenerator | DbPluginEntrypointType::InlineView => { + DbPluginEntrypointType::EntrypointGenerator | DbPluginEntrypointType::InlineView => { Ok(None) } } @@ -860,11 +860,11 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { Ok(data) } - async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result> { + async fn get_entrypoint_generator_entrypoint_ids(&self) -> anyhow::Result> { let result = self.repository.get_entrypoints_by_plugin_id(&self.plugin_id.to_string()).await? .into_iter() .filter(|entrypoint| entrypoint.enabled) - .filter(|entrypoint| matches!(db_entrypoint_from_str(&entrypoint.entrypoint_type), DbPluginEntrypointType::CommandGenerator)) + .filter(|entrypoint| matches!(db_entrypoint_from_str(&entrypoint.entrypoint_type), DbPluginEntrypointType::EntrypointGenerator)) .map(|entrypoint| entrypoint.id) .collect::>(); diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index 3ae8bcd..904257f 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -232,7 +232,7 @@ impl PluginLoader { PluginManifestEntrypointTypes::Command => DbPluginEntrypointType::Command, PluginManifestEntrypointTypes::View => DbPluginEntrypointType::View, PluginManifestEntrypointTypes::InlineView => DbPluginEntrypointType::InlineView, - PluginManifestEntrypointTypes::CommandGenerator => DbPluginEntrypointType::CommandGenerator, + PluginManifestEntrypointTypes::EntrypointGenerator => DbPluginEntrypointType::EntrypointGenerator, }).to_owned(), preferences: entrypoint.preferences .into_iter() @@ -691,8 +691,8 @@ pub enum PluginManifestEntrypointTypes { View, #[serde(rename = "inline-view")] InlineView, - #[serde(rename = "command-generator")] - CommandGenerator, + #[serde(rename = "entrypoint-generator")] + EntrypointGenerator, } #[derive(Debug, Deserialize)] diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index ff9231d..6bc9014 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -193,7 +193,7 @@ impl ApplicationManager { DbPluginEntrypointType::Command => SettingsEntrypointType::Command, DbPluginEntrypointType::View => SettingsEntrypointType::View, DbPluginEntrypointType::InlineView => SettingsEntrypointType::InlineView, - DbPluginEntrypointType::CommandGenerator => SettingsEntrypointType::CommandGenerator, + DbPluginEntrypointType::EntrypointGenerator => SettingsEntrypointType::EntrypointGenerator, }.into(), preferences: entrypoint.preferences.into_iter() .map(|(key, value)| { diff --git a/schema/backend.proto b/schema/backend.proto index b19ac39..2aec6a4 100644 --- a/schema/backend.proto +++ b/schema/backend.proto @@ -145,7 +145,7 @@ enum RpcEntrypointTypeSettings { S_COMMAND = 0; S_VIEW = 1; S_INLINE_VIEW = 2; - S_COMMAND_GENERATOR = 3; + S_ENTRYPOINT_GENERATOR = 3; } message RpcPlugin { From 11af8eaff2416c94b70705e5db17369feb307b7a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 26 Dec 2024 11:10:41 +0100 Subject: [PATCH 249/540] Update nix hash --- nix/overlay.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/overlay.nix b/nix/overlay.nix index a84b768..dbbbfff 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -6,7 +6,7 @@ flake.overlays.default = final: _: let # These must be updated following the instructions in ./nix/README.md when dependencies are updated or the version bumped version = "v12"; - npmDepsHash = "sha256-TlfUwNsmyN4dzqBh3CW33pGXxBZHLhSDyAqS4fJCmPU="; + npmDepsHash = "sha256-NADYbP3NUUezlMN5nPqZ3qpBRZNkSWf8HBwn9zqsEYw="; RUSTY_V8_ARCHIVE = fetchRustyV8 "130.0.2" { aarch64-darwin = "sha256-aWZ/4Q4Wttx37xOdBmTCPGP+eYGhr4CM1UkYq8pC7Qs="; aarch64-linux = "sha256-p9+tHmKIM5wBABubHIAstpwfzO19ypPzOuaV4b6loCU="; From f584f28a74f7d7aa16b80928eccb11c600d7ba2f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 28 Dec 2024 21:00:20 +0100 Subject: [PATCH 250/540] Slightly throttle search index reload when calling add/remove in entrypoint generator --- js/core/package.json | 3 +++ js/core/src/entrypoint-generator.ts | 7 +++++-- nix/overlay.nix | 2 +- package-lock.json | 3 +++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/js/core/package.json b/js/core/package.json index 1398cae..d6d8422 100644 --- a/js/core/package.json +++ b/js/core/package.json @@ -4,6 +4,9 @@ "scripts": { "build": "tsc --noEmit && rollup --config rollup.config.ts --configPlugin typescript" }, + "dependencies": { + "@std/async": "npm:@jsr/std__async@^1.0.9" + }, "devDependencies": { "@project-gauntlet/api": "*", "@project-gauntlet/typings": "*", diff --git a/js/core/src/entrypoint-generator.ts b/js/core/src/entrypoint-generator.ts index 9aa651b..e624fd8 100644 --- a/js/core/src/entrypoint-generator.ts +++ b/js/core/src/entrypoint-generator.ts @@ -8,6 +8,7 @@ import { import { reloadSearchIndex } from "./search-index"; import type { FC } from "react"; import { renderView } from "./render"; +import { throttle } from "@std/async/unstable-throttle"; interface GeneratedCommand { // TODO is it possible to import api here name: string @@ -83,6 +84,8 @@ export async function runEntrypointGenerators(): Promise { await reloadSearchIndex(true) + const throttledReloadSearchIndex = throttle(() => reloadSearchIndex(true), 100) + const entrypointIds = await get_entrypoint_generator_entrypoint_ids(); for (const generatorEntrypointId of entrypointIds) { try { @@ -138,7 +141,7 @@ export async function runEntrypointGenerators(): Promise { derivedActions, } - reloadSearchIndex(true) + throttledReloadSearchIndex() } const remove = (id: string) => { op_log_info("entrypoint_generator", `Removing entry '${id}' by entrypoint generator entrypoint '${generatorEntrypointId}'`) @@ -146,7 +149,7 @@ export async function runEntrypointGenerators(): Promise { delete storedGeneratedCommands[lookupId] - reloadSearchIndex(true) + throttledReloadSearchIndex() } const get = (id: string) => { diff --git a/nix/overlay.nix b/nix/overlay.nix index dbbbfff..c380c36 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -6,7 +6,7 @@ flake.overlays.default = final: _: let # These must be updated following the instructions in ./nix/README.md when dependencies are updated or the version bumped version = "v12"; - npmDepsHash = "sha256-NADYbP3NUUezlMN5nPqZ3qpBRZNkSWf8HBwn9zqsEYw="; + npmDepsHash = "sha256-Y5IgbkaeKz4RYzgi3fdL1C0ztBZ0FNxvXcdtAnGMWgo="; RUSTY_V8_ARCHIVE = fetchRustyV8 "130.0.2" { aarch64-darwin = "sha256-aWZ/4Q4Wttx37xOdBmTCPGP+eYGhr4CM1UkYq8pC7Qs="; aarch64-linux = "sha256-p9+tHmKIM5wBABubHIAstpwfzO19ypPzOuaV4b6loCU="; diff --git a/package-lock.json b/package-lock.json index 0f18683..34e579e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,6 +98,9 @@ }, "js/core": { "name": "@project-gauntlet/core", + "dependencies": { + "@std/async": "npm:@jsr/std__async@^1.0.9" + }, "devDependencies": { "@project-gauntlet/api": "*", "@project-gauntlet/typings": "*", From 873220ef895dcf929d59349ce5bb8a50697f5983 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 28 Dec 2024 23:03:06 +0100 Subject: [PATCH 251/540] Revert "Slightly throttle search index reload when calling add/remove in entrypoint generator" This reverts commit f584f28a74f7d7aa16b80928eccb11c600d7ba2f. --- js/core/package.json | 3 --- js/core/src/entrypoint-generator.ts | 7 ++----- nix/overlay.nix | 2 +- package-lock.json | 3 --- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/js/core/package.json b/js/core/package.json index d6d8422..1398cae 100644 --- a/js/core/package.json +++ b/js/core/package.json @@ -4,9 +4,6 @@ "scripts": { "build": "tsc --noEmit && rollup --config rollup.config.ts --configPlugin typescript" }, - "dependencies": { - "@std/async": "npm:@jsr/std__async@^1.0.9" - }, "devDependencies": { "@project-gauntlet/api": "*", "@project-gauntlet/typings": "*", diff --git a/js/core/src/entrypoint-generator.ts b/js/core/src/entrypoint-generator.ts index e624fd8..9aa651b 100644 --- a/js/core/src/entrypoint-generator.ts +++ b/js/core/src/entrypoint-generator.ts @@ -8,7 +8,6 @@ import { import { reloadSearchIndex } from "./search-index"; import type { FC } from "react"; import { renderView } from "./render"; -import { throttle } from "@std/async/unstable-throttle"; interface GeneratedCommand { // TODO is it possible to import api here name: string @@ -84,8 +83,6 @@ export async function runEntrypointGenerators(): Promise { await reloadSearchIndex(true) - const throttledReloadSearchIndex = throttle(() => reloadSearchIndex(true), 100) - const entrypointIds = await get_entrypoint_generator_entrypoint_ids(); for (const generatorEntrypointId of entrypointIds) { try { @@ -141,7 +138,7 @@ export async function runEntrypointGenerators(): Promise { derivedActions, } - throttledReloadSearchIndex() + reloadSearchIndex(true) } const remove = (id: string) => { op_log_info("entrypoint_generator", `Removing entry '${id}' by entrypoint generator entrypoint '${generatorEntrypointId}'`) @@ -149,7 +146,7 @@ export async function runEntrypointGenerators(): Promise { delete storedGeneratedCommands[lookupId] - throttledReloadSearchIndex() + reloadSearchIndex(true) } const get = (id: string) => { diff --git a/nix/overlay.nix b/nix/overlay.nix index c380c36..dbbbfff 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -6,7 +6,7 @@ flake.overlays.default = final: _: let # These must be updated following the instructions in ./nix/README.md when dependencies are updated or the version bumped version = "v12"; - npmDepsHash = "sha256-Y5IgbkaeKz4RYzgi3fdL1C0ztBZ0FNxvXcdtAnGMWgo="; + npmDepsHash = "sha256-NADYbP3NUUezlMN5nPqZ3qpBRZNkSWf8HBwn9zqsEYw="; RUSTY_V8_ARCHIVE = fetchRustyV8 "130.0.2" { aarch64-darwin = "sha256-aWZ/4Q4Wttx37xOdBmTCPGP+eYGhr4CM1UkYq8pC7Qs="; aarch64-linux = "sha256-p9+tHmKIM5wBABubHIAstpwfzO19ypPzOuaV4b6loCU="; diff --git a/package-lock.json b/package-lock.json index 34e579e..0f18683 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,9 +98,6 @@ }, "js/core": { "name": "@project-gauntlet/core", - "dependencies": { - "@std/async": "npm:@jsr/std__async@^1.0.9" - }, "devDependencies": { "@project-gauntlet/api": "*", "@project-gauntlet/typings": "*", From d4548c0cda1ea1b76a04dd5d0c3cac84c463c520 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 29 Dec 2024 13:22:52 +0100 Subject: [PATCH 252/540] Implement window tracking per application for x11 --- Cargo.lock | 66 +++ bundled_plugins/gauntlet/gauntlet.toml | 2 +- .../src/{applications.ts => applications.tsx} | 85 +++- .../gauntlet/src/window/shared.tsx | 136 ++++++ bundled_plugins/gauntlet/src/window/x11.ts | 353 ++++++++++++++++ bundled_plugins/gauntlet/tsconfig.json | 3 +- js/api/src/helpers.ts | 2 +- js/core/src/entrypoint-generator.ts | 12 +- js/core/src/internal-all.ts | 2 + js/typings/index.d.ts | 4 + rust/plugin_runtime/Cargo.toml | 2 + rust/plugin_runtime/src/deno.rs | 11 +- .../src/plugins/applications.rs | 137 +++++- .../src/plugins/applications/linux.rs | 20 +- .../src/plugins/applications/x11.rs | 391 ++++++++++++++++++ 15 files changed, 1186 insertions(+), 40 deletions(-) rename bundled_plugins/gauntlet/src/{applications.ts => applications.tsx} (69%) create mode 100644 bundled_plugins/gauntlet/src/window/shared.tsx create mode 100644 bundled_plugins/gauntlet/src/window/x11.ts create mode 100644 rust/plugin_runtime/src/plugins/applications/x11.rs diff --git a/Cargo.lock b/Cargo.lock index c19535b..ac9c737 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3280,6 +3280,70 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -4042,6 +4106,7 @@ dependencies = [ "cacao", "deno_core", "deno_runtime", + "encoding", "freedesktop-icons", "freedesktop_entry_parser", "futures", @@ -4068,6 +4133,7 @@ dependencies = [ "typed-path", "walkdir", "which 7.0.1", + "x11rb", ] [[package]] diff --git a/bundled_plugins/gauntlet/gauntlet.toml b/bundled_plugins/gauntlet/gauntlet.toml index 278f6a0..1f86d0d 100644 --- a/bundled_plugins/gauntlet/gauntlet.toml +++ b/bundled_plugins/gauntlet/gauntlet.toml @@ -5,7 +5,7 @@ description = 'Default Gauntlet functionality as a bundled plugin' [[entrypoint]] id = 'applications' name = 'Applications' -path = 'src/applications.ts' +path = 'src/applications.tsx' type = 'entrypoint-generator' description = 'Run installed applications from your system' diff --git a/bundled_plugins/gauntlet/src/applications.ts b/bundled_plugins/gauntlet/src/applications.tsx similarity index 69% rename from bundled_plugins/gauntlet/src/applications.ts rename to bundled_plugins/gauntlet/src/applications.tsx index e918bd3..1ad705b 100644 --- a/bundled_plugins/gauntlet/src/applications.ts +++ b/bundled_plugins/gauntlet/src/applications.tsx @@ -1,12 +1,8 @@ import { GeneratedCommand, GeneratorProps } from "@project-gauntlet/api/helpers"; import { walk, WalkOptions } from "@std/fs/walk"; import { debounce } from "@std/async/debounce"; -import { current_os } from "gauntlet:bridge/internal-all"; -import { - linux_app_from_path, - linux_application_dirs, - linux_open_application, -} from "gauntlet:bridge/internal-linux"; +import { current_os, wayland } from "gauntlet:bridge/internal-all"; +import { linux_app_from_path, linux_application_dirs, linux_open_application, } from "gauntlet:bridge/internal-linux"; import { macos_app_from_arbitrary_path, macos_app_from_path, @@ -19,28 +15,71 @@ import { macos_settings_pre_13, macos_system_applications } from "gauntlet:bridge/internal-macos"; +import { applicationAccessories, applicationActions, OpenWindowData } from "./window/shared"; +import { applicationEventLoopX11 } from "./window/x11"; + +export default async function Applications({ add, remove, get, getAll }: GeneratorProps): Promise void)> { + const openWindows: Record = {}; -export default async function Applications({ add, remove }: GeneratorProps): Promise void)> { switch (current_os()) { case "linux": { - return await genericGenerator( + const cleanup = await genericGenerator( linux_application_dirs(), path => linux_app_from_path(path), - (id, data) => ({ - name: data.name, - actions: [ - { - label: "Open application", - run: () => { - linux_open_application(id) - }, + (id, data) => { + if (wayland()) { + // TODO + return { + name: data.name, + actions: [ + { + label: "Open application", + run: () => { + linux_open_application(id) + }, + } + ], + accessories: applicationAccessories(id, openWindows), + icon: data.icon, } - ], - icon: data.icon, // TODO lazy icons - }), + } else { + return { + name: data.name, + actions: applicationActions( + id, + () => { + linux_open_application(id) + }, + (windowId: string) => { + // TODO + console.log(`focusing window: ${windowId}`) + }, + openWindows + ), + accessories: applicationAccessories(id, openWindows), + icon: data.icon, // TODO lazy icons + } + } + }, add, - remove + remove, ); + + if (wayland()) { + // TODO + } else { + applicationEventLoopX11( + openWindows, + (windowId: string) => { + console.log(`focusing window: ${windowId}`) + }, + add, + get, + getAll + ); + } + + return cleanup; } case "macos": { const majorVersion = macos_major_version(); @@ -103,7 +142,7 @@ export default async function Applications({ add, remove }: GeneratorProps): Pro } } - return await genericGenerator( + return await genericGenerator( macos_application_dirs(), path => macos_app_from_arbitrary_path(path), (_id, data) => ({ @@ -193,9 +232,11 @@ async function genericGenerator( for await (const event of watcher) { handle(event) } - })() + })(); return () => { watcher.close() } } + + diff --git a/bundled_plugins/gauntlet/src/window/shared.tsx b/bundled_plugins/gauntlet/src/window/shared.tsx new file mode 100644 index 0000000..f0bb0cd --- /dev/null +++ b/bundled_plugins/gauntlet/src/window/shared.tsx @@ -0,0 +1,136 @@ +import { GeneratedCommand, GeneratedCommandAccessory, GeneratedCommandAction } from "@project-gauntlet/api/helpers"; +import { List } from "@project-gauntlet/api/components"; +import { X11WindowData } from "./x11"; + +export type OpenWindowData = { + id: string, + title: string, + appId: string +} + +export function applicationActions( + id: string, + openApplication: () => void, + focusWindow: (windowId: string) => void, + openWindows: Record +): GeneratedCommandAction[] { + const appWindows = Object.entries(openWindows) + .filter(([_, windowData]) => windowData.appId == id) + + // TODO ability to close window + + if (appWindows.length == 0) { + return [ + { + label: "Open application", + run: () => { + openApplication() + }, + } + ] + } else if (appWindows.length == 1) { + return [ + { + label: "Focus window", + run: () => { + let [appWindow] = appWindows; + let [windowId, _] = appWindow!!; + focusWindow(windowId) + }, + } + ] + } else if (appWindows.length > 1) { + return [ + { + label: "Show windows", + view: () => { + return ( + + { + appWindows + .map(([_, windowData]) => ( + { + focusWindow(windowData.id) + }} + /> + )) + } + + ) + } + } + ] + } else { + return [] + } +} + +export function applicationAccessories(id: string, openWindows: Record): GeneratedCommandAccessory[] { + const appWindows = Object.entries(openWindows) + .filter(([_, windowData]) => windowData.appId == id) + + if (appWindows.length == 0) { + return [] + } else if (appWindows.length == 1) { + return [{ text: "1 window open" }] + } else if (appWindows.length > 1) { + return [{ text: `${appWindows.length} windows open` }] + } else { + return [] + } +} + +export function addOpenWindow( + appId: string, + window: X11WindowData, + openWindows: Record, + openApplication: () => void, + focusWindow: (windowId: string) => void, + add: (id: string, data: GeneratedCommand) => void, + getAll: () => { [id: string]: GeneratedCommand }, +) { + const generated = getAll(); + + const generatedEntrypoint = generated[appId]; + + if (generatedEntrypoint) { + openWindows[window.id] = { + id: window.id, + appId: appId, + title: window.title + } + + add(appId, { + ...generatedEntrypoint, + actions: applicationActions(appId, openApplication, focusWindow, openWindows), + accessories: applicationAccessories(appId, openWindows) + }) + } +} + +export function deleteOpenWindow( + openWindows: Record, + windowId: string, + openApplication: (appId: string) => (() => void), + focusWindow: (windowId: string) => void, + get: (id: string) => GeneratedCommand | undefined, + add: (id: string, data: GeneratedCommand) => void, +) { + const openWindow = openWindows[windowId]; + if (openWindow) { + const generatedEntrypoint = get(openWindow.appId); + + delete openWindows[windowId]; + + if (generatedEntrypoint) { + add(openWindow.appId, { + ...generatedEntrypoint, + actions: applicationActions(openWindow.appId, openApplication(openWindow.appId), focusWindow, openWindows), + accessories: applicationAccessories(openWindow.appId, openWindows) + }) + } + } +} diff --git a/bundled_plugins/gauntlet/src/window/x11.ts b/bundled_plugins/gauntlet/src/window/x11.ts new file mode 100644 index 0000000..f4e50fc --- /dev/null +++ b/bundled_plugins/gauntlet/src/window/x11.ts @@ -0,0 +1,353 @@ +import { GeneratedCommand } from "@project-gauntlet/api/helpers"; +import { application_pending_event } from "gauntlet:bridge/internal-all"; +import { addOpenWindow, deleteOpenWindow, OpenWindowData } from "./shared"; +import { linux_open_application } from "gauntlet:bridge/internal-linux"; + +export type X11WindowProtocol = "DeleteWindow" | "TakeFocus" +export type X11WindowType = "DropdownMenu" | "Dialog" | "Menu" | "Notification" | "Normal" | "PopupMenu" | "Splash" | "Toolbar" | "Tooltip" | "Utility" +export type X11WindowId = string + +export type X11WindowData = { + // x11 window id + id: X11WindowId, + // x11 parent window id + parentId: X11WindowId, + // do not show override_redirect windows in list of windows + overrideRedirect: boolean, + // window is visibly only if it and all of its parents are mapped + mapped: boolean, + // _NET_WM_NAME, or WM_NAME if that is not present + title: string, + // WM_CLASS + class: string, + // WM_CLASS + instance: string, + // WM_PROTOCOLS + protocols: X11WindowProtocol[], + // WM_HINTS + windowGroup: X11WindowId | undefined, + // todo icon + // WM_TRANSIENT_FOR + transientFor: string | undefined, + // _NET_WM_WINDOW_TYPE + windowTypes: X11WindowType[], + // _KDE_NET_WM_DESKTOP_FILE or _GTK_APPLICATION_ID + desktopFileName: string | undefined, +} + +type X11ApplicationEvent = X11ApplicationEventInit + | X11ApplicationEventCreateNotify + | X11ApplicationEventDestroyNotify + | X11ApplicationEventMapNotify + | X11ApplicationEventUnmapNotify + | X11ApplicationEventReparentNotify + | X11ApplicationEventTitlePropertyNotify + | X11ApplicationEventClassPropertyNotify + | X11ApplicationEventHintsPropertyNotify + | X11ApplicationEventProtocolsPropertyNotify + | X11ApplicationEventTransientForPropertyNotify + | X11ApplicationEventWindowTypePropertyNotify + | X11ApplicationEventDesktopFileNamePropertyNotify; + + +type X11ApplicationEventInit = { + type: "Init", + id: X11WindowId, + parent_id: X11WindowId, + override_redirect: boolean, + mapped: boolean, +}; + +type X11ApplicationEventCreateNotify = { + type: "CreateNotify", + id: X11WindowId, + parent_id: X11WindowId, + override_redirect: boolean, +}; + +type X11ApplicationEventDestroyNotify = { + type: "DestroyNotify", + id: X11WindowId, +} + +type X11ApplicationEventMapNotify = { + type: "MapNotify", + id: X11WindowId, +}; + +type X11ApplicationEventUnmapNotify = { + type: "UnmapNotify", + id: X11WindowId, +}; + +type X11ApplicationEventReparentNotify = { + type: "ReparentNotify", + id: X11WindowId, +}; + +type X11ApplicationEventTitlePropertyNotify = { + type: "TitlePropertyNotify", + id: X11WindowId, + title: string +}; + +type X11ApplicationEventClassPropertyNotify = { + type: "ClassPropertyNotify", + id: X11WindowId, + class: string, + instance: string +}; + +type X11ApplicationEventHintsPropertyNotify = { + type: "HintsPropertyNotify", + id: X11WindowId, + window_group: X11WindowId | undefined, +}; + +type X11ApplicationEventProtocolsPropertyNotify = { + type: "ProtocolsPropertyNotify", + id: X11WindowId, + protocols: X11WindowProtocol[], +}; + +type X11ApplicationEventTransientForPropertyNotify = { + type: "TransientForPropertyNotify", + id: X11WindowId, + transient_for: X11WindowId | undefined, +}; + +type X11ApplicationEventWindowTypePropertyNotify = { + type: "WindowTypePropertyNotify", + id: X11WindowId, + window_types: X11WindowType[] +}; + +type X11ApplicationEventDesktopFileNamePropertyNotify = { + type: "DesktopFileNamePropertyNotify", + id: X11WindowId, + desktop_file_name: string +}; + +function openApplication(appId: string) { + return () => { + linux_open_application(appId) + } +} + +export function applicationEventLoopX11( + openWindows: Record, + focusWindow: (windowId: string) => void, + add: (id: string, data: GeneratedCommand) => void, + get: (id: string) => GeneratedCommand | undefined, + getAll: () => { [id: string]: GeneratedCommand }, +) { + const windows: Record = {}; + + // noinspection ES6MissingAwait + (async () => { + // noinspection InfiniteLoopJS + while (true) { + const applicationEvent = await application_pending_event() as X11ApplicationEvent; + switch (applicationEvent.type) { + case "Init": { + windows[applicationEvent.id] = { + id: applicationEvent.id, + parentId: applicationEvent.parent_id, + overrideRedirect: applicationEvent.override_redirect, + mapped: applicationEvent.mapped, + class: "", + instance: "", + protocols: [], + title: "", + transientFor: undefined, + windowGroup: undefined, + windowTypes: [], + desktopFileName: undefined, + } + break; + } + case "CreateNotify": { + windows[applicationEvent.id] = { + id: applicationEvent.id, + parentId: applicationEvent.parent_id, + overrideRedirect: applicationEvent.override_redirect, + mapped: false, + class: "", + instance: "", + protocols: [], + title: "", + transientFor: undefined, + windowGroup: undefined, + windowTypes: [], + desktopFileName: undefined, + } + break; + } + case "DestroyNotify": { + delete windows[applicationEvent.id] + + deleteOpenWindow(openWindows, applicationEvent.id, openApplication, focusWindow, get, add) + + break; + } + case "MapNotify": { + const window = windows[applicationEvent.id]; + if (window) { + window.mapped = true; + + validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + } + + break; + } + case "UnmapNotify": { + const window = windows[applicationEvent.id]; + if (window) { + window.mapped = false; + + deleteOpenWindow(openWindows, applicationEvent.id, openApplication, focusWindow, get, add) + } + + break; + } + case "ReparentNotify": { + // for Dolphin FileManger map event doesn't seem to be fired, does reparent imply map? + const window = windows[applicationEvent.id]; + if (window) { + window.mapped = true; + + validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + } + + break; + } + case "TitlePropertyNotify": { + const window = windows[applicationEvent.id]; + if (window) { + window.title = applicationEvent.title; + + validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + } + + break; + } + case "ClassPropertyNotify": { + const window = windows[applicationEvent.id]; + if (window) { + window.class = applicationEvent.class; + window.instance = applicationEvent.instance; + + validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + } + + break; + } + case "HintsPropertyNotify": { + const window = windows[applicationEvent.id]; + if (window) { + window.windowGroup = applicationEvent.window_group; + + validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + } + + break; + } + case "ProtocolsPropertyNotify": { + const window = windows[applicationEvent.id]; + if (window) { + window.protocols = applicationEvent.protocols; + + validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + } + + break; + } + case "TransientForPropertyNotify": { + const window = windows[applicationEvent.id]; + if (window) { + window.transientFor = applicationEvent.transient_for; + + validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + } + + break; + } + case "WindowTypePropertyNotify": { + const window = windows[applicationEvent.id]; + if (window) { + window.windowTypes = applicationEvent.window_types; + + validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + } + + break; + } + case "DesktopFileNamePropertyNotify": { + const window = windows[applicationEvent.id]; + if (window) { + window.desktopFileName = applicationEvent.desktop_file_name; + + validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + } + + break; + } + } + } + })() +} + +function validateAndAddOpenWindow( + window: X11WindowData, + openWindows: Record, + windows: Record, + openApplication: (appId: string) => (() => void), + focusWindow: (windowId: string) => void, + add: (id: string, data: GeneratedCommand) => void, + getAll: () => { [id: string]: GeneratedCommand }, +) { + + if (window.overrideRedirect) { + return; + } + + if (window.transientFor != undefined) { + return; + } + + if (!window.mapped) { + return; + } + + if (!window.windowTypes.includes("Normal")) { + return; + } + + let parentWindow = windows[window.parentId] + while (parentWindow != undefined) { + if (!parentWindow.mapped) { + return; + } + + parentWindow = windows[parentWindow.parentId] + } + + + let appId = window.desktopFileName; + if (appId) { + addOpenWindow( + appId, + window, + openWindows, + openApplication(appId), + focusWindow, + add, + getAll + ) + } + + // https://nicolasfella.de/posts/importance-of-desktop-file-mapping/ + // TODO do the heuristics + // https://github.com/KDE/plasma-workspace/blob/e2cf987971088640a149d871bcdfe63fa2aae855/libtaskmanager/xwindowtasksmodel.cpp#L519 + // https://github.com/GNOME/gnome-shell/blob/8fbaa5e55a8d65454c4d2a6f53ceb8bcaa687af5/src/shell-window-tracker.c#L390 +} \ No newline at end of file diff --git a/bundled_plugins/gauntlet/tsconfig.json b/bundled_plugins/gauntlet/tsconfig.json index e866b96..2179ea7 100644 --- a/bundled_plugins/gauntlet/tsconfig.json +++ b/bundled_plugins/gauntlet/tsconfig.json @@ -6,7 +6,8 @@ "target": "ES2022", "moduleResolution": "bundler", "jsx": "react-jsx", - "types": ["@project-gauntlet/typings", "@types/deno"] + "types": ["@project-gauntlet/typings", "@types/deno"], + "noUncheckedIndexedAccess": true }, "lib": ["ES2020"] } \ No newline at end of file diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index 3435a0d..b4a7c22 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -71,7 +71,7 @@ export type GeneratorProps = { add: (id: string, data: GeneratedCommand) => void, remove: (id: string) => void, get: (id: string) => GeneratedCommand | undefined - getAll: () => GeneratedCommand[] + getAll: () => { [id: string]: GeneratedCommand }, }; export const Clipboard: Clipboard = { diff --git a/js/core/src/entrypoint-generator.ts b/js/core/src/entrypoint-generator.ts index 9aa651b..e9ee1bf 100644 --- a/js/core/src/entrypoint-generator.ts +++ b/js/core/src/entrypoint-generator.ts @@ -34,13 +34,14 @@ type GeneratorProps = { add: (id: string, data: GeneratedCommand) => void, remove: (id: string) => void, get: (id: string) => GeneratedCommand | undefined - getAll: () => GeneratedCommand[] + getAll: () => { [id: string]: GeneratedCommand } }; type Generator = (props: GeneratorProps) => void | (() => (void | Promise)) | Promise (void | Promise))> type ProcessedGeneratedCommand = { generatorEntrypointId: string, + id: string, uuid: string, command: GeneratedCommand derivedActions: GeneratedCommandDerivedAction[] @@ -133,6 +134,7 @@ export async function runEntrypointGenerators(): Promise { storedGeneratedCommands[lookupId] = { generatorEntrypointId: generatorEntrypointId, + id: id, uuid: crypto.randomUUID(), command: data, derivedActions, @@ -161,11 +163,13 @@ export async function runEntrypointGenerators(): Promise { } } - const getAll = (): GeneratedCommand[] => { + const getAll = (): { [id: string]: GeneratedCommand } => { op_log_debug("entrypoint_generator", `Getting all entries by entrypoint generator entrypoint '${generatorEntrypointId}'`) - return Object.entries(storedGeneratedCommands) - .map(([_, value]) => value.command) + return Object.fromEntries( + Object.entries(storedGeneratedCommands) + .map(([_lookupId, value]) => [value.id, value.command]) + ) } // noinspection ES6MissingAwait diff --git a/js/core/src/internal-all.ts b/js/core/src/internal-all.ts index 8175bda..ed94a69 100644 --- a/js/core/src/internal-all.ts +++ b/js/core/src/internal-all.ts @@ -2,4 +2,6 @@ export { run_numbat, open_settings, current_os, + wayland, + application_pending_event, } from "ext:core/ops"; diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index e890f62..64bc040 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -142,6 +142,8 @@ declare module "gauntlet:bridge/internal-all" { function open_settings(): void function run_numbat(input: string): { left: string, right: string } function current_os(): string + function wayland(): boolean + function application_pending_event(): Promise } declare module "gauntlet:bridge/internal-linux" { @@ -170,6 +172,8 @@ declare module "ext:core/ops" { function run_numbat(input: string): { left: string, right: string } function current_os(): string + function wayland(): boolean + function application_pending_event(): Promise function linux_open_application(desktop_id: string): void function linux_application_dirs(): string[] diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml index 89eb7c1..6fe53e3 100644 --- a/rust/plugin_runtime/Cargo.toml +++ b/rust/plugin_runtime/Cargo.toml @@ -35,6 +35,8 @@ which = "7.0.1" [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] libc = "0.2" +x11rb = { version = "0.13", features = ["extra-traits"] } +encoding = "0.2" [target.'cfg(target_os = "linux")'.dependencies] freedesktop_entry_parser = "1.3" diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index ca89c60..752c5e9 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -29,7 +29,7 @@ use crate::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_ use crate::model::JsInit; use crate::permissions::{permissions_to_deno}; use crate::plugin_data::PluginData; -use crate::plugins::applications::current_os; +use crate::plugins::applications::{application_pending_event, current_os, wayland, ApplicationContext}; use crate::plugins::numbat::{run_numbat, NumbatContext}; use crate::plugins::settings::open_settings; use crate::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; @@ -265,6 +265,8 @@ deno_core::extension!( // plugins applications current_os, + wayland, + application_pending_event, // plugins settings open_settings, @@ -276,9 +278,11 @@ deno_core::extension!( ], options = { numbat_context: NumbatContext, + application_context: ApplicationContext, }, state = |state, options| { state.put(options.numbat_context); + state.put(options.application_context); }, ); @@ -399,7 +403,10 @@ pub async fn start_js_runtime( ]; if init.plugin_id.to_string() == "bundled://gauntlet" { - extensions.push(gauntlet_internal_all::init_ops_and_esm(NumbatContext::new())); + extensions.push(gauntlet_internal_all::init_ops_and_esm( + NumbatContext::new(), + ApplicationContext::new() + )); #[cfg(target_os = "macos")] extensions.push(gauntlet_internal_macos::init_ops_and_esm()); diff --git a/rust/plugin_runtime/src/plugins/applications.rs b/rust/plugin_runtime/src/plugins/applications.rs index 2271efa..79dd4e7 100644 --- a/rust/plugin_runtime/src/plugins/applications.rs +++ b/rust/plugin_runtime/src/plugins/applications.rs @@ -2,14 +2,19 @@ use std::cell::RefCell; use deno_core::{op2, OpState}; use std::path::PathBuf; use std::rc::Rc; +use anyhow::anyhow; use image::ImageFormat; use image::imageops::FilterType; -use serde::Serialize; +use serde::{Deserialize, Serialize}; +use tokio::runtime::Handle; +use tokio::sync::mpsc::Receiver; use tokio::task::spawn_blocking; use crate::plugin_data::PluginData; #[cfg(target_os = "linux")] mod linux; +#[cfg(target_os = "linux")] +mod x11; #[cfg(target_os = "macos")] mod macos; @@ -72,6 +77,136 @@ pub fn current_os() -> &'static str { std::env::consts::OS } +#[op2(fast)] +pub fn wayland() -> bool { + let wayland = std::env::var("WAYLAND_DISPLAY") + .or_else(|_| std::env::var("WAYLAND_SOCKET")) + .is_ok(); + + wayland +} + +pub struct ApplicationContext { + pub receiver: Rc>>, +} + +impl ApplicationContext { + pub fn new() -> Self { + let (sender, receiver) = tokio::sync::mpsc::channel(100); + + let handle = Handle::current(); + + #[cfg(target_os = "linux")] + std::thread::spawn(|| { + if let Err(e) = x11::listen_on_x11_events(handle, sender) { + tracing::error!("Error while listening on x11 events: {}", e); + } + }); + + Self { + receiver: Rc::new(RefCell::new(receiver)) + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum JsX11ApplicationEvent { + Init { + id: String, + parent_id: String, + override_redirect: bool, + mapped: bool + }, + CreateNotify { + id: String, + parent_id: String, + override_redirect: bool + }, + DestroyNotify { + id: String, + }, + MapNotify { + id: String, + }, + UnmapNotify { + id: String, + }, + ReparentNotify { + id: String, + }, + TitlePropertyNotify { + id: String, + title: String + }, + ClassPropertyNotify { + id: String, + class: String, + instance: String + }, + HintsPropertyNotify { + id: String, + window_group: Option, + }, + ProtocolsPropertyNotify { + id: String, + protocols: Vec, + }, + TransientForPropertyNotify { + id: String, + transient_for: Option, + }, + WindowTypePropertyNotify { + id: String, + window_types: Vec + }, + DesktopFileNamePropertyNotify { + id: String, + desktop_file_name: String + }, +} + +#[derive(Debug, Deserialize, Serialize)] +enum JSX11WindowProtocol { + TakeFocus, + DeleteWindow, +} + +#[derive(Debug, Deserialize, Serialize)] +enum JSX11WindowType { + DropdownMenu, + Dialog, + Menu, + Notification, + Normal, + PopupMenu, + Splash, + Toolbar, + Tooltip, + Utility, +} + + +#[op2(async)] +#[serde] +pub async fn application_pending_event(state: Rc>) -> anyhow::Result { + let receiver = { + state.borrow() + .borrow::() + .receiver + .clone() + }; + + let mut receiver = receiver.borrow_mut(); + let event = receiver.recv() + .await + .ok_or_else(|| anyhow!("plugin event stream was suddenly closed"))?; + + tracing::trace!("Received application event {:?}", event); + + Ok(event) +} + #[cfg(target_os = "linux")] #[op2(async)] #[serde] diff --git a/rust/plugin_runtime/src/plugins/applications/linux.rs b/rust/plugin_runtime/src/plugins/applications/linux.rs index 2bf5cfe..67955d2 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux.rs @@ -1,7 +1,6 @@ use std::collections::{HashMap, HashSet}; use std::fs::Metadata; use std::path::{Path, PathBuf}; -use std::{env, fs}; use crate::plugins::applications::{resize_icon, DesktopApplication, DesktopPathAction}; use freedesktop_entry_parser::parse_entry; @@ -11,7 +10,7 @@ use image::ImageFormat; use walkdir::WalkDir; pub fn linux_application_dirs(home_dir: PathBuf) -> Vec { - let data_home = match env::var_os("XDG_DATA_HOME") { + let data_home = match std::env::var_os("XDG_DATA_HOME") { Some(val) => { PathBuf::from(val) }, @@ -22,9 +21,9 @@ pub fn linux_application_dirs(home_dir: PathBuf) -> Vec { } }; - let mut extra_data_dirs = match env::var_os("XDG_DATA_DIRS") { + let mut extra_data_dirs = match std::env::var_os("XDG_DATA_DIRS") { Some(val) => { - env::split_paths(&val).map(PathBuf::from).collect() + std::env::split_paths(&val).map(PathBuf::from).collect() }, None => { vec![ @@ -73,14 +72,19 @@ pub fn linux_app_from_path(home_dir: PathBuf, path: PathBuf) -> Option Option, app_event: JsX11ApplicationEvent) { + let sender = sender.clone(); + tokio_handle.spawn(async move { + if let Err(e) = sender.send(app_event).await { + tracing::error!("Error while sending x11 connection: {:?}", e); + } + }); +} + +pub fn listen_on_x11_events( + tokio_handle: Handle, + sender: Sender, +) -> anyhow::Result { + let (conn, screen_num) = RustConnection::connect(None)?; + let screen = &conn.setup().roots[screen_num]; + let atoms = atoms::Atoms::new(&conn)?.reply()?; + + let aux = ChangeWindowAttributesAux::new() + .event_mask(EventMask::SUBSTRUCTURE_NOTIFY | EventMask::PROPERTY_CHANGE); + + conn.change_window_attributes(screen.root, &aux)?.check()?; + + let _ = fetch_existing_windows(screen.root, &conn, &tokio_handle, &sender, atoms); + + loop { + match conn.wait_for_event()? { + Event::CreateNotify(event) => { + tracing::trace!("CreateNotify: {:?}", event); + + let aux = ChangeWindowAttributesAux::new() + .event_mask(EventMask::PROPERTY_CHANGE); + + conn.change_window_attributes(event.window, &aux)?; + conn.flush()?; + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::CreateNotify { + id: format!("{}", event.window), + parent_id: format!("{}", event.parent), + override_redirect: event.override_redirect + }); + + update_properties(event.window, &conn, &tokio_handle, &sender, atoms); + } + Event::DestroyNotify(event) => { + tracing::trace!("DestroyNotify: {:?}", event); + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::DestroyNotify { + id: format!("{}", event.window), + }) + } + Event::MapNotify(event) => { + tracing::trace!("MapNotify: {:?}", event); + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::MapNotify { + id: format!("{}", event.window), + }); + } + Event::UnmapNotify(event) => { + tracing::trace!("UnmapNotify: {:?}", event); + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::UnmapNotify { + id: format!("{}", event.window), + }); + } + Event::ReparentNotify(event) => { + tracing::trace!("ReparentNotify: {:?}", event); + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::ReparentNotify { + id: format!("{}", event.window), + }); + } + Event::PropertyNotify(event) => { + tracing::trace!("PropertyNotify: {:?}", event); + + match event.atom { + atom if atom == atoms._NET_WM_NAME || atom == Atom::from(AtomEnum::WM_NAME) => { + let _ = update_title(event.window, &conn, &tokio_handle, &sender, atoms); + } + atom if atom == Atom::from(AtomEnum::WM_CLASS) => { + let _ = update_class(event.window, &conn, &tokio_handle, &sender); + }, + atom if atom == atoms.WM_PROTOCOLS => { + let _ = update_protocols(event.window, &conn, &tokio_handle, &sender, atoms); + }, + atom if atom == atoms.WM_HINTS => { + let _ = update_hints(event.window, &conn, &tokio_handle, &sender); + }, + atom if atom == Atom::from(AtomEnum::WM_TRANSIENT_FOR) => { + let _ = update_transient_for(event.window, &conn, &tokio_handle, &sender); + }, + atom if atom == atoms._NET_WM_WINDOW_TYPE => { + let _ = update_net_window_type(event.window, &conn, &tokio_handle, &sender, atoms); + }, + atom if atom == atoms._KDE_NET_WM_DESKTOP_FILE || atom == atoms._GTK_APPLICATION_ID => { + let _ = update_desktop_file_name(event.window, &conn, &tokio_handle, &sender, atoms); + }, + _ => {}, + } + } + _ => {} + } + } +} + +fn fetch_existing_windows( + window_id: Window, + conn: &RustConnection, + tokio_handle: &Handle, + sender: &Sender, + atoms: atoms::Atoms, +) -> anyhow::Result<()> { + let query_tree = conn.query_tree(window_id)?.reply()?; + + let attributes = conn.get_window_attributes(window_id)?.reply()?; + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::Init { + id: format!("{}", window_id), + parent_id: format!("{}", query_tree.parent), + override_redirect: attributes.override_redirect, + mapped: match attributes.map_state { + MapState::UNMAPPED => false, + MapState::UNVIEWABLE => true, + MapState::VIEWABLE => true, + unknown @ _ => Err(anyhow::anyhow!("Unknown map state: {:?}", unknown))? + }, + }); + + update_properties( + window_id, + &conn, + tokio_handle, + sender, + atoms, + ); + + for window in query_tree.children { + let _ = fetch_existing_windows(window, conn, tokio_handle, sender, atoms); + } + + Ok(()) +} + + +fn update_properties( + window_id: Window, + conn: &RustConnection, + tokio_handle: &Handle, + sender: &Sender, + atoms: atoms::Atoms +) { + let _ = update_title(window_id, conn, tokio_handle, sender, atoms); + let _ = update_class(window_id, conn, tokio_handle, sender); + let _ = update_hints(window_id, conn, tokio_handle, sender); + let _ = update_protocols(window_id, conn, tokio_handle, sender, atoms); + let _ = update_transient_for(window_id, conn, tokio_handle, sender); + let _ = update_net_window_type(window_id, conn, tokio_handle, sender, atoms); + let _ = update_desktop_file_name(window_id, &conn, &tokio_handle, &sender, atoms); +} + +fn update_title(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender, atoms: atoms::Atoms) -> anyhow::Result<()> { + let net_wm_name = read_window_property_string(window_id, conn, atoms, atoms._NET_WM_NAME)?; + let wm_name = read_window_property_string(window_id, conn, atoms, AtomEnum::WM_NAME)?; + + // tracing::trace!("title - _NET_WM_NAME: {:?}", net_wm_name); + // tracing::trace!("title - WM_NAME: {:?}", wm_name); + + let title = net_wm_name.or(wm_name).unwrap_or_default(); + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::TitlePropertyNotify { + id: format!("{}", window_id), + title + }); + + Ok(()) +} + +fn update_class(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender) -> anyhow::Result<()> { + let (class, instance) = match WmClass::get(conn, window_id)?.reply() { + Ok(Some(wm_class)) => { + let class = encoding::all::ISO_8859_1 + .decode(wm_class.class(), DecoderTrap::Replace) + .ok() + .unwrap_or_default(); + + let instance = encoding::all::ISO_8859_1 + .decode(wm_class.instance(), DecoderTrap::Replace) + .ok() + .unwrap_or_default(); + + (class, instance) + }, + Ok(None) => (Default::default(), Default::default()), + Err(err) => Err(err)?, + }; + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::ClassPropertyNotify { + id: format!("{}", window_id), + class, + instance + }); + + Ok(()) +} + +fn update_hints(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender) -> anyhow::Result<()> { + let hints = match WmHints::get(conn, window_id)?.reply() { + Ok(hints) => hints, + Err(err) => Err(err)?, + }; + + let window_group = hints + .and_then(|hints| hints.window_group) + .map(|window| format!("{}", window)); + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::HintsPropertyNotify { + id: format!("{}", window_id), + window_group + }); + + Ok(()) +} + +fn update_protocols(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender, atoms: atoms::Atoms) -> anyhow::Result<()> { + let reply = conn + .get_property(false, window_id, atoms.WM_PROTOCOLS, AtomEnum::ATOM, 0, 2048)? + .reply()?; + + let protocols = reply.value32() + .map(|vals| vals.collect::>()); + + let Some(protocols) = protocols else { + return Ok(()) + }; + + let protocols = protocols + .into_iter() + .filter_map(|atom| match atom { + x if x == atoms.WM_TAKE_FOCUS => Some(JSX11WindowProtocol::TakeFocus), + x if x == atoms.WM_DELETE_WINDOW => Some(JSX11WindowProtocol::DeleteWindow), + _ => None, + }) + .collect::>(); + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::ProtocolsPropertyNotify { + id: format!("{}", window_id), + protocols + }); + + Ok(()) +} + +fn update_transient_for(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender) -> anyhow::Result<()> { + let reply = conn.get_property(false, window_id, AtomEnum::WM_TRANSIENT_FOR, AtomEnum::WINDOW, 0, 2048)? + .reply()?; + + let transient_for = reply + .value32() + .and_then(|mut iter| iter.next()) + .filter(|w| *w != 0); + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::TransientForPropertyNotify { + id: format!("{}", window_id), + transient_for: transient_for.map(|window_id| format!("{}", window_id)) + }); + + Ok(()) +} + +fn update_net_window_type(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender, atoms: atoms::Atoms) -> anyhow::Result<()> { + let reply = conn + .get_property(false, window_id, atoms._NET_WM_WINDOW_TYPE, AtomEnum::ATOM, 0, 1024)? + .reply()?; + + let window_types = reply.value32() + .map(|iter| iter.collect::>()) + .unwrap_or_default() + .into_iter() + .flat_map(|atom| { + match atom { + atom if atom == atoms._NET_WM_WINDOW_TYPE_DROPDOWN_MENU => Some(JSX11WindowType::DropdownMenu), + atom if atom == atoms._NET_WM_WINDOW_TYPE_DIALOG => Some(JSX11WindowType::Dialog), + atom if atom == atoms._NET_WM_WINDOW_TYPE_MENU => Some(JSX11WindowType::Menu), + atom if atom == atoms._NET_WM_WINDOW_TYPE_NOTIFICATION => Some(JSX11WindowType::Notification), + atom if atom == atoms._NET_WM_WINDOW_TYPE_NORMAL => Some(JSX11WindowType::Normal), + atom if atom == atoms._NET_WM_WINDOW_TYPE_POPUP_MENU => Some(JSX11WindowType::PopupMenu), + atom if atom == atoms._NET_WM_WINDOW_TYPE_SPLASH => Some(JSX11WindowType::Splash), + atom if atom == atoms._NET_WM_WINDOW_TYPE_TOOLBAR => Some(JSX11WindowType::Toolbar), + atom if atom == atoms._NET_WM_WINDOW_TYPE_TOOLTIP => Some(JSX11WindowType::Tooltip), + atom if atom == atoms._NET_WM_WINDOW_TYPE_UTILITY => Some(JSX11WindowType::Utility), + _ => None + } + }) + .collect(); + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::WindowTypePropertyNotify { + id: format!("{}", window_id), + window_types + }); + + Ok(()) +} + +fn update_desktop_file_name(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender, atoms: atoms::Atoms) -> anyhow::Result<()> { + let kde_net_wm_desktop_file = read_window_property_string(window_id, conn, atoms, atoms._KDE_NET_WM_DESKTOP_FILE)?; + let gtk_application_id = read_window_property_string(window_id, conn, atoms, atoms._GTK_APPLICATION_ID)?; + + let desktop_file_name = kde_net_wm_desktop_file.or(gtk_application_id).unwrap_or_default(); + + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::DesktopFileNamePropertyNotify { + id: format!("{}", window_id), + desktop_file_name + }); + + Ok(()) +} + +fn read_window_property_string(window_id: Window, conn: &RustConnection, atoms: atoms::Atoms, atom: impl Into) -> anyhow::Result> { + let reply = conn + .get_property(false, window_id, atom, AtomEnum::ANY, 0, 2048)? + .reply()?; + + let Some(bytes) = reply.value8() else { + return Ok(None) + }; + + let bytes = bytes.collect::>(); + + match reply.type_ { + x if x == Atom::from(AtomEnum::STRING) => { + let decoded = encoding::all::ISO_8859_1 + .decode(&bytes, DecoderTrap::Replace) + .ok(); + + Ok(decoded) + }, + x if x == atoms.UTF8_STRING => { + Ok(String::from_utf8(bytes).ok()) + }, + _ => Ok(None), + } +} +mod atoms { + x11rb::atom_manager! { + pub Atoms: + AtomsCookie { + // data formats + UTF8_STRING, + + // client -> server + WM_HINTS, + WM_PROTOCOLS, + WM_TAKE_FOCUS, + WM_DELETE_WINDOW, + _NET_WM_NAME, + _NET_WM_PID, + _NET_WM_WINDOW_TYPE, + _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + _NET_WM_WINDOW_TYPE_DIALOG, + _NET_WM_WINDOW_TYPE_MENU, + _NET_WM_WINDOW_TYPE_NOTIFICATION, + _NET_WM_WINDOW_TYPE_NORMAL, + _NET_WM_WINDOW_TYPE_POPUP_MENU, + _NET_WM_WINDOW_TYPE_SPLASH, + _NET_WM_WINDOW_TYPE_TOOLBAR, + _NET_WM_WINDOW_TYPE_TOOLTIP, + _NET_WM_WINDOW_TYPE_UTILITY, + _NET_WM_STATE_MODAL, + + // non-standard + _KDE_NET_WM_DESKTOP_FILE, + _GTK_APPLICATION_ID, + } + } +} + + From 9767290ecbddc46704b1f3dd2bf34786edad5afb Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 29 Dec 2024 17:36:32 +0100 Subject: [PATCH 253/540] Match x11 window to app id also based on StartupWmClass and WM_CLASS --- bundled_plugins/gauntlet/src/applications.tsx | 6 ++ .../gauntlet/src/window/shared.tsx | 6 +- bundled_plugins/gauntlet/src/window/x11.ts | 64 ++++++++++++++++--- js/typings/index.d.ts | 1 + .../src/plugins/applications.rs | 1 + .../src/plugins/applications/linux.rs | 2 + 6 files changed, 65 insertions(+), 15 deletions(-) diff --git a/bundled_plugins/gauntlet/src/applications.tsx b/bundled_plugins/gauntlet/src/applications.tsx index 1ad705b..a644cb1 100644 --- a/bundled_plugins/gauntlet/src/applications.tsx +++ b/bundled_plugins/gauntlet/src/applications.tsx @@ -41,6 +41,9 @@ export default async function Applications({ add, remove, get, getAll }: Generat ], accessories: applicationAccessories(id, openWindows), icon: data.icon, + "__linux__": { + startupWmClass: data.startup_wm_class + } } } else { return { @@ -58,6 +61,9 @@ export default async function Applications({ add, remove, get, getAll }: Generat ), accessories: applicationAccessories(id, openWindows), icon: data.icon, // TODO lazy icons + "__linux__": { + startupWmClass: data.startup_wm_class + } } } }, diff --git a/bundled_plugins/gauntlet/src/window/shared.tsx b/bundled_plugins/gauntlet/src/window/shared.tsx index f0bb0cd..af5e263 100644 --- a/bundled_plugins/gauntlet/src/window/shared.tsx +++ b/bundled_plugins/gauntlet/src/window/shared.tsx @@ -85,17 +85,13 @@ export function applicationAccessories(id: string, openWindows: Record, openApplication: () => void, focusWindow: (windowId: string) => void, add: (id: string, data: GeneratedCommand) => void, - getAll: () => { [id: string]: GeneratedCommand }, ) { - const generated = getAll(); - - const generatedEntrypoint = generated[appId]; - if (generatedEntrypoint) { openWindows[window.id] = { id: window.id, diff --git a/bundled_plugins/gauntlet/src/window/x11.ts b/bundled_plugins/gauntlet/src/window/x11.ts index f4e50fc..5606786 100644 --- a/bundled_plugins/gauntlet/src/window/x11.ts +++ b/bundled_plugins/gauntlet/src/window/x11.ts @@ -332,22 +332,66 @@ function validateAndAddOpenWindow( parentWindow = windows[parentWindow.parentId] } + function process(appId: string) { + const generatedEntrypoint = generated[appId]; + + if (generatedEntrypoint) { + addOpenWindow( + appId, + generatedEntrypoint, + window, + openWindows, + openApplication(appId), + focusWindow, + add, + ) + } + } + + const generated = getAll(); let appId = window.desktopFileName; if (appId) { - addOpenWindow( - appId, - window, - openWindows, - openApplication(appId), - focusWindow, - add, - getAll - ) + process(appId) + return; + } + + const startupWmClassToAppId = Object.fromEntries( + Object.entries(generated) + .map(([appId, generated]): [string | undefined, string] => [((generated as any)["__linux__"]).startupWmClass, appId]) + .filter((val): val is [string, string] => { + const [wmClass, _appId] = val + return wmClass != undefined + }) + ); + + const appIdFromWmClassInstance = startupWmClassToAppId[window.instance]; + if (appIdFromWmClassInstance) { + process(appIdFromWmClassInstance) + return; + } + + const appIdFromWmClass = startupWmClassToAppId[window.class]; + if (appIdFromWmClass) { + process(appIdFromWmClass) + return; + } + + const wmClassInstanceAsAppId = window.instance; + if (wmClassInstanceAsAppId) { + process(wmClassInstanceAsAppId) + return; + } + + const wmClassAsAppId = window.class; + if (wmClassAsAppId) { + process(wmClassAsAppId) + return; } // https://nicolasfella.de/posts/importance-of-desktop-file-mapping/ - // TODO do the heuristics + // TODO do the rest of heuristics + // OR just tell users to use wayland? // https://github.com/KDE/plasma-workspace/blob/e2cf987971088640a149d871bcdfe63fa2aae855/libtaskmanager/xwindowtasksmodel.cpp#L519 // https://github.com/GNOME/gnome-shell/blob/8fbaa5e55a8d65454c4d2a6f53ceb8bcaa687af5/src/shell-window-tracker.c#L390 } \ No newline at end of file diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 64bc040..e505ce8 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -17,6 +17,7 @@ type DesktopPathActionRemove = { type LinuxDesktopApplicationData = { name: string icon: ArrayBuffer | undefined, + startup_wm_class: string | undefined, } type MacOSDesktopApplicationData = { diff --git a/rust/plugin_runtime/src/plugins/applications.rs b/rust/plugin_runtime/src/plugins/applications.rs index 79dd4e7..33ed3b7 100644 --- a/rust/plugin_runtime/src/plugins/applications.rs +++ b/rust/plugin_runtime/src/plugins/applications.rs @@ -38,6 +38,7 @@ pub enum DesktopPathAction { pub struct DesktopApplication { name: String, icon: Option>, + startup_wm_class: Option, } #[cfg(target_os = "macos")] diff --git a/rust/plugin_runtime/src/plugins/applications/linux.rs b/rust/plugin_runtime/src/plugins/applications/linux.rs index 67955d2..616466f 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux.rs @@ -116,6 +116,7 @@ fn create_app_entry(desktop_file_path: &Path) -> Option { let icon = entry.attr("Icon").map(|s| s.to_string()); let no_display = entry.attr("NoDisplay").map(|val| val == "true").unwrap_or(false); let hidden = entry.attr("Hidden").map(|val| val == "true").unwrap_or(false); + let startup_wm_class = entry.attr("StartupWMClass").map(|s| s.to_string());; // TODO NotShowIn, OnlyShowIn https://wiki.archlinux.org/title/desktop_entries if no_display || hidden { @@ -177,5 +178,6 @@ fn create_app_entry(desktop_file_path: &Path) -> Option { Some(DesktopApplication { name: name.to_string(), icon, + startup_wm_class, }) } From a84d3ee3da3300360f28b58ecc53750d22edad02 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:45:33 +0100 Subject: [PATCH 254/540] Implement focusing for window tracking on x11 --- bundled_plugins/gauntlet/src/applications.tsx | 11 +--- bundled_plugins/gauntlet/src/window/x11.ts | 6 +- js/core/src/internal-linux.ts | 1 + js/typings/index.d.ts | 2 + rust/plugin_runtime/src/deno.rs | 1 + .../src/plugins/applications.rs | 9 +++ .../src/plugins/applications/x11.rs | 56 ++++++++++++++++++- 7 files changed, 76 insertions(+), 10 deletions(-) diff --git a/bundled_plugins/gauntlet/src/applications.tsx b/bundled_plugins/gauntlet/src/applications.tsx index a644cb1..b55ef29 100644 --- a/bundled_plugins/gauntlet/src/applications.tsx +++ b/bundled_plugins/gauntlet/src/applications.tsx @@ -16,7 +16,7 @@ import { macos_system_applications } from "gauntlet:bridge/internal-macos"; import { applicationAccessories, applicationActions, OpenWindowData } from "./window/shared"; -import { applicationEventLoopX11 } from "./window/x11"; +import { applicationEventLoopX11, focusX11Window } from "./window/x11"; export default async function Applications({ add, remove, get, getAll }: GeneratorProps): Promise void)> { const openWindows: Record = {}; @@ -53,10 +53,7 @@ export default async function Applications({ add, remove, get, getAll }: Generat () => { linux_open_application(id) }, - (windowId: string) => { - // TODO - console.log(`focusing window: ${windowId}`) - }, + focusX11Window, openWindows ), accessories: applicationAccessories(id, openWindows), @@ -76,9 +73,7 @@ export default async function Applications({ add, remove, get, getAll }: Generat } else { applicationEventLoopX11( openWindows, - (windowId: string) => { - console.log(`focusing window: ${windowId}`) - }, + focusX11Window, add, get, getAll diff --git a/bundled_plugins/gauntlet/src/window/x11.ts b/bundled_plugins/gauntlet/src/window/x11.ts index 5606786..f2e80f3 100644 --- a/bundled_plugins/gauntlet/src/window/x11.ts +++ b/bundled_plugins/gauntlet/src/window/x11.ts @@ -1,7 +1,7 @@ import { GeneratedCommand } from "@project-gauntlet/api/helpers"; import { application_pending_event } from "gauntlet:bridge/internal-all"; import { addOpenWindow, deleteOpenWindow, OpenWindowData } from "./shared"; -import { linux_open_application } from "gauntlet:bridge/internal-linux"; +import { linux_open_application, linux_x11_focus_window } from "gauntlet:bridge/internal-linux"; export type X11WindowProtocol = "DeleteWindow" | "TakeFocus" export type X11WindowType = "DropdownMenu" | "Dialog" | "Menu" | "Notification" | "Normal" | "PopupMenu" | "Splash" | "Toolbar" | "Tooltip" | "Utility" @@ -128,6 +128,10 @@ type X11ApplicationEventDesktopFileNamePropertyNotify = { desktop_file_name: string }; +export function focusX11Window(windowId: string) { + linux_x11_focus_window(windowId) +} + function openApplication(appId: string) { return () => { linux_open_application(appId) diff --git a/js/core/src/internal-linux.ts b/js/core/src/internal-linux.ts index 8505a84..37ab8a8 100644 --- a/js/core/src/internal-linux.ts +++ b/js/core/src/internal-linux.ts @@ -2,4 +2,5 @@ export { linux_app_from_path, linux_application_dirs, linux_open_application, + linux_x11_focus_window, } from "ext:core/ops"; diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index e505ce8..3f6af62 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -149,6 +149,7 @@ declare module "gauntlet:bridge/internal-all" { declare module "gauntlet:bridge/internal-linux" { function linux_open_application(desktop_id: string): void + function linux_x11_focus_window(window_id: string): void function linux_application_dirs(): string[] function linux_app_from_path(path: string): Promise> @@ -177,6 +178,7 @@ declare module "ext:core/ops" { function application_pending_event(): Promise function linux_open_application(desktop_id: string): void + function linux_x11_focus_window(window_id: string): void function linux_application_dirs(): string[] function linux_app_from_path(path: string): Promise> diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index 752c5e9..cc7f9a7 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -294,6 +294,7 @@ deno_core::extension!( crate::plugins::applications::linux_app_from_path, crate::plugins::applications::linux_application_dirs, crate::plugins::applications::linux_open_application, + crate::plugins::applications::linux_x11_focus_window, ], esm_entry_point = "ext:gauntlet/internal-linux/bootstrap.js", esm = [ diff --git a/rust/plugin_runtime/src/plugins/applications.rs b/rust/plugin_runtime/src/plugins/applications.rs index 33ed3b7..f3d060c 100644 --- a/rust/plugin_runtime/src/plugins/applications.rs +++ b/rust/plugin_runtime/src/plugins/applications.rs @@ -254,6 +254,15 @@ pub fn linux_open_application(#[string] desktop_file_id: String) -> anyhow::Resu Ok(()) } +#[cfg(target_os = "linux")] +#[op2(fast)] +pub fn linux_x11_focus_window(#[string] x11_window_id: String) -> anyhow::Result<()> { + + x11::focus_window(x11_window_id)?; + + Ok(()) +} + #[cfg(target_os = "macos")] #[op2(fast)] pub fn macos_major_version() -> u8 { diff --git a/rust/plugin_runtime/src/plugins/applications/x11.rs b/rust/plugin_runtime/src/plugins/applications/x11.rs index 8fe12e8..461c684 100644 --- a/rust/plugin_runtime/src/plugins/applications/x11.rs +++ b/rust/plugin_runtime/src/plugins/applications/x11.rs @@ -1,18 +1,71 @@ use std::collections::HashMap; use crate::plugins::applications::{JSX11WindowProtocol, JSX11WindowType, JsX11ApplicationEvent}; use std::convert::Infallible; +use std::str::FromStr; +use anyhow::anyhow; use encoding::{DecoderTrap, Encoding}; use tokio::runtime::Handle; use tokio::sync::mpsc::Sender; use x11rb::connection::Connection; use x11rb::errors::ConnectionError; use x11rb::properties::{WmClass, WmHints}; -use x11rb::protocol::xproto::{Atom, AtomEnum, MapState, Window}; +use x11rb::protocol::xproto::{Atom, AtomEnum, ClientMessageEvent, ConfigureWindowAux, InputFocus, MapState, StackMode, Window}; use x11rb::protocol::xproto::ConnectionExt; use x11rb::protocol::xproto::{ChangeWindowAttributesAux, EventMask}; use x11rb::protocol::Event; use x11rb::rust_connection::RustConnection; + +pub fn focus_window(window_id: String) -> anyhow::Result<()> { + // https://github.com/freedesktop-unofficial-mirror/xcb__util-wm/blob/24eb17df2e1245885e72c9d4bbb0a0f69f0700f2/ewmh/xcb_ewmh.h.m4#L1268 + // this basically reimplementation of xcb_ewmh_request_change_active_window + + let window_to_activate = Window::from_str(&window_id)?; + + let (conn, screen_num) = RustConnection::connect(None)?; + let screen = &conn.setup().roots[screen_num]; + let atoms = atoms::Atoms::new(&conn)?.reply()?; + + // TODO change desktop? + // https://github.com/davatorium/rofi/blob/f9491690fdfbffc5fe13438b26323c05c67acd7b/source/modes/window.c#L820 + + let active_window_property = conn.get_property(false, screen.root, atoms._NET_ACTIVE_WINDOW, AtomEnum::WINDOW, 0, 2048)? + .reply()?; + + let current_active_window = active_window_property + .value32() + .and_then(|mut iter| iter.next()) + .unwrap_or(0); // no focused window + + // these values are same as rofi + let source_indication: u32 = 2; // XCB_EWMH_CLIENT_SOURCE_TYPE_OTHER + let timestamp: u32 = x11rb::CURRENT_TIME; + + let event = ClientMessageEvent::new( + 32, + window_to_activate, + atoms._NET_ACTIVE_WINDOW, + [ + source_indication, + timestamp, + current_active_window, + 0, + 0 + ], + ); + + conn.send_event( + false, + screen.root, + EventMask::SUBSTRUCTURE_NOTIFY | EventMask::SUBSTRUCTURE_REDIRECT, + event + )?; + + conn.flush()?; + + Ok(()) +} + fn send_event(tokio_handle: &Handle, sender: &Sender, app_event: JsX11ApplicationEvent) { let sender = sender.clone(); tokio_handle.spawn(async move { @@ -380,6 +433,7 @@ mod atoms { _NET_WM_WINDOW_TYPE_TOOLTIP, _NET_WM_WINDOW_TYPE_UTILITY, _NET_WM_STATE_MODAL, + _NET_ACTIVE_WINDOW, // non-standard _KDE_NET_WM_DESKTOP_FILE, From a3532e960284860dfcbfc74bf4ef190371bc8ecd Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 31 Dec 2024 19:15:57 +0100 Subject: [PATCH 255/540] Implement window tracking and focusing for WMs that support wlr-foreign-toplevel-management-unstable-v1 protocol --- Cargo.lock | 30 +++ bundled_plugins/gauntlet/src/applications.tsx | 58 ++-- .../gauntlet/src/window/shared.tsx | 16 +- .../gauntlet/src/window/wayland.ts | 140 ++++++++++ bundled_plugins/gauntlet/src/window/x11.ts | 135 ++-------- js/core/src/internal-all.ts | 3 +- js/core/src/internal-linux.ts | 1 + js/typings/index.d.ts | 130 ++++++++- rust/plugin_runtime/Cargo.toml | 4 + rust/plugin_runtime/src/deno.rs | 24 +- .../src/plugins/applications.rs | 197 ++------------ .../applications/{linux.rs => linux/mod.rs} | 115 +++++++- .../plugins/applications/linux/wayland/mod.rs | 252 ++++++++++++++++++ .../plugins/applications/linux/wayland/wlr.rs | 173 ++++++++++++ .../plugins/applications/{ => linux}/x11.rs | 156 ++++++++++- 15 files changed, 1081 insertions(+), 353 deletions(-) create mode 100644 bundled_plugins/gauntlet/src/window/wayland.ts rename rust/plugin_runtime/src/plugins/applications/{linux.rs => linux/mod.rs} (63%) create mode 100644 rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs create mode 100644 rust/plugin_runtime/src/plugins/applications/linux/wayland/wlr.rs rename rust/plugin_runtime/src/plugins/applications/{ => linux}/x11.rs (82%) diff --git a/Cargo.lock b/Cargo.lock index ac9c737..fddcf1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4127,11 +4127,15 @@ dependencies = [ "regex", "resvg", "serde", + "smithay-client-toolkit 0.19.2", "tokio", "tokio-util", "tracing", "typed-path", + "uuid", "walkdir", + "wayland-client", + "wayland-protocols-wlr 0.3.5", "which 7.0.1", "x11rb", ] @@ -6384,6 +6388,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memmap2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" +dependencies = [ + "libc", +] + [[package]] name = "memmap2" version = "0.9.5" @@ -9486,12 +9499,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ "bitflags 2.6.0", + "bytemuck", "calloop 0.13.0", "calloop-wayland-source 0.3.0", "cursor-icon", "libc", "log", "memmap2 0.9.5", + "pkg-config", "rustix", "thiserror 1.0.69", "wayland-backend", @@ -9501,6 +9516,7 @@ dependencies = [ "wayland-protocols 0.32.5", "wayland-protocols-wlr 0.3.5", "wayland-scanner", + "xkbcommon", "xkeysym", ] @@ -12720,6 +12736,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xkbcommon" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e" +dependencies = [ + "libc", + "memmap2 0.8.0", + "xkeysym", +] + [[package]] name = "xkbcommon-dl" version = "0.4.2" @@ -12738,6 +12765,9 @@ name = "xkeysym" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +dependencies = [ + "bytemuck", +] [[package]] name = "xml-rs" diff --git a/bundled_plugins/gauntlet/src/applications.tsx b/bundled_plugins/gauntlet/src/applications.tsx index b55ef29..3092df4 100644 --- a/bundled_plugins/gauntlet/src/applications.tsx +++ b/bundled_plugins/gauntlet/src/applications.tsx @@ -17,6 +17,7 @@ import { } from "gauntlet:bridge/internal-macos"; import { applicationAccessories, applicationActions, OpenWindowData } from "./window/shared"; import { applicationEventLoopX11, focusX11Window } from "./window/x11"; +import { applicationEventLoopWayland, focusWaylandWindow } from "./window/wayland"; export default async function Applications({ add, remove, get, getAll }: GeneratorProps): Promise void)> { const openWindows: Record = {}; @@ -28,21 +29,21 @@ export default async function Applications({ add, remove, get, getAll }: Generat path => linux_app_from_path(path), (id, data) => { if (wayland()) { - // TODO return { name: data.name, - actions: [ - { - label: "Open application", - run: () => { - linux_open_application(id) - }, - } - ], + actions: applicationActions( + id, + () => { + linux_open_application(id) + }, + focusWaylandWindow, + openWindows + ), accessories: applicationAccessories(id, openWindows), - icon: data.icon, + icon: data.icon, // TODO lazy icons "__linux__": { - startupWmClass: data.startup_wm_class + startupWmClass: data.startup_wm_class, + desktopFilePath: data.desktop_file_path } } } else { @@ -54,12 +55,13 @@ export default async function Applications({ add, remove, get, getAll }: Generat linux_open_application(id) }, focusX11Window, - openWindows + openWindows, ), accessories: applicationAccessories(id, openWindows), icon: data.icon, // TODO lazy icons "__linux__": { - startupWmClass: data.startup_wm_class + startupWmClass: data.startup_wm_class, + desktopFilePath: data.desktop_file_path } } } @@ -69,15 +71,29 @@ export default async function Applications({ add, remove, get, getAll }: Generat ); if (wayland()) { - // TODO + try { + applicationEventLoopWayland( + openWindows, + focusWaylandWindow, + add, + get, + getAll + ); + } catch (e) { + console.log("error when setting up wayland application event loop", e) + } } else { - applicationEventLoopX11( - openWindows, - focusX11Window, - add, - get, - getAll - ); + try { + applicationEventLoopX11( + openWindows, + focusX11Window, + add, + get, + getAll + ); + } catch (e) { + console.log("error when setting up x11 application event loop", e) + } } return cleanup; diff --git a/bundled_plugins/gauntlet/src/window/shared.tsx b/bundled_plugins/gauntlet/src/window/shared.tsx index af5e263..e5e0c8d 100644 --- a/bundled_plugins/gauntlet/src/window/shared.tsx +++ b/bundled_plugins/gauntlet/src/window/shared.tsx @@ -1,6 +1,7 @@ import { GeneratedCommand, GeneratedCommandAccessory, GeneratedCommandAction } from "@project-gauntlet/api/helpers"; import { List } from "@project-gauntlet/api/components"; import { X11WindowData } from "./x11"; +import { linux_open_application } from "gauntlet:bridge/internal-linux"; export type OpenWindowData = { id: string, @@ -86,17 +87,18 @@ export function applicationAccessories(id: string, openWindows: Record, openApplication: () => void, focusWindow: (windowId: string) => void, add: (id: string, data: GeneratedCommand) => void, ) { if (generatedEntrypoint) { - openWindows[window.id] = { - id: window.id, + openWindows[windowId] = { + id: windowId, appId: appId, - title: window.title + title: windowTitle } add(appId, { @@ -130,3 +132,9 @@ export function deleteOpenWindow( } } } + +export function openLinuxApplication(appId: string) { + return () => { + linux_open_application(appId) + } +} \ No newline at end of file diff --git a/bundled_plugins/gauntlet/src/window/wayland.ts b/bundled_plugins/gauntlet/src/window/wayland.ts new file mode 100644 index 0000000..9a04ef9 --- /dev/null +++ b/bundled_plugins/gauntlet/src/window/wayland.ts @@ -0,0 +1,140 @@ +import { addOpenWindow, deleteOpenWindow, openLinuxApplication, OpenWindowData } from "./shared"; +import { GeneratedCommand } from "@project-gauntlet/api/helpers"; +import { application_wayland_pending_event } from "gauntlet:bridge/internal-all"; +import { linux_wayland_focus_window } from "gauntlet:bridge/internal-linux"; + + +export function focusWaylandWindow(windowId: string) { + linux_wayland_focus_window(windowId) +} + +export function applicationEventLoopWayland( + openWindows: Record, + focusWindow: (windowId: string) => void, + add: (id: string, data: GeneratedCommand) => void, + get: (id: string) => GeneratedCommand | undefined, + getAll: () => { [id: string]: GeneratedCommand }, +) { + const knownWindows: Record = { }; + + // noinspection ES6MissingAwait + (async () => { + // noinspection InfiniteLoopJS + while (true) { + const applicationEvent = await application_wayland_pending_event(); + switch (applicationEvent.type) { + case "WindowOpened": { + knownWindows[applicationEvent.window_id] = { + appId: undefined, + title: undefined + } + + break; + } + case "WindowClosed": { + delete knownWindows[applicationEvent.window_id] + + deleteOpenWindow(openWindows, applicationEvent.window_id, openLinuxApplication, focusWindow, get, add) + + break; + } + case "WindowTitleChanged": { + const windowId = applicationEvent.window_id; + const knownWindow = knownWindows[windowId]; + + if (knownWindow) { + knownWindow.title = applicationEvent.title; + + const windowTitle = knownWindow.title; + const windowAppId = knownWindow.appId; + + if (typeof windowAppId == "string") { + addOpenWindowWayland( + windowId, + windowAppId, + windowTitle, + openWindows, + focusWindow, + add, + get, + getAll + ) + } + } + + break; + } + case "WindowAppIdChanged": { + const windowId = applicationEvent.window_id; + const knownWindow = knownWindows[windowId]; + + if (knownWindow) { + knownWindow.appId = applicationEvent.app_id; + + const windowTitle = knownWindow.title; + const windowAppId = knownWindow.appId; + + if (typeof windowTitle == "string") { + addOpenWindowWayland( + windowId, + windowAppId, + windowTitle, + openWindows, + focusWindow, + add, + get, + getAll + ) + } + } + break; + } + } + } + })() +} + +function addOpenWindowWayland( + windowId: string, + windowAppId: string, + windowTitle: string, + openWindows: Record, + focusWindow: (windowId: string) => void, + add: (id: string, data: GeneratedCommand) => void, + get: (id: string) => GeneratedCommand | undefined, + getAll: () => { [id: string]: GeneratedCommand }, +) { + let appId = windowAppId; + let generatedEntrypoint = get(windowAppId); + + if (generatedEntrypoint == undefined) { + const startupWmClassToAppId = Object.fromEntries( + Object.entries(getAll()) + .map(([appId, generated]): [string | undefined, string] => [((generated as any)["__linux__"]).startupWmClass, appId]) + .filter((val): val is [string, string] => { + const [wmClass, _appId] = val + return wmClass != undefined + }) + ); + + const appIdFromWmClass = startupWmClassToAppId[windowAppId]; + + if (appIdFromWmClass) { + appId = appIdFromWmClass; + generatedEntrypoint = get(appId); + } + } + + if (generatedEntrypoint) { + addOpenWindow( + appId, + generatedEntrypoint, + windowId, + windowTitle, + openWindows, + openLinuxApplication(appId), + focusWindow, + add, + ) + } +} \ No newline at end of file diff --git a/bundled_plugins/gauntlet/src/window/x11.ts b/bundled_plugins/gauntlet/src/window/x11.ts index f2e80f3..17127e8 100644 --- a/bundled_plugins/gauntlet/src/window/x11.ts +++ b/bundled_plugins/gauntlet/src/window/x11.ts @@ -1,11 +1,7 @@ import { GeneratedCommand } from "@project-gauntlet/api/helpers"; -import { application_pending_event } from "gauntlet:bridge/internal-all"; -import { addOpenWindow, deleteOpenWindow, OpenWindowData } from "./shared"; -import { linux_open_application, linux_x11_focus_window } from "gauntlet:bridge/internal-linux"; - -export type X11WindowProtocol = "DeleteWindow" | "TakeFocus" -export type X11WindowType = "DropdownMenu" | "Dialog" | "Menu" | "Notification" | "Normal" | "PopupMenu" | "Splash" | "Toolbar" | "Tooltip" | "Utility" -export type X11WindowId = string +import { application_x11_pending_event } from "gauntlet:bridge/internal-all"; +import { addOpenWindow, deleteOpenWindow, openLinuxApplication, OpenWindowData } from "./shared"; +import { linux_x11_focus_window } from "gauntlet:bridge/internal-linux"; export type X11WindowData = { // x11 window id @@ -35,109 +31,11 @@ export type X11WindowData = { desktopFileName: string | undefined, } -type X11ApplicationEvent = X11ApplicationEventInit - | X11ApplicationEventCreateNotify - | X11ApplicationEventDestroyNotify - | X11ApplicationEventMapNotify - | X11ApplicationEventUnmapNotify - | X11ApplicationEventReparentNotify - | X11ApplicationEventTitlePropertyNotify - | X11ApplicationEventClassPropertyNotify - | X11ApplicationEventHintsPropertyNotify - | X11ApplicationEventProtocolsPropertyNotify - | X11ApplicationEventTransientForPropertyNotify - | X11ApplicationEventWindowTypePropertyNotify - | X11ApplicationEventDesktopFileNamePropertyNotify; - - -type X11ApplicationEventInit = { - type: "Init", - id: X11WindowId, - parent_id: X11WindowId, - override_redirect: boolean, - mapped: boolean, -}; - -type X11ApplicationEventCreateNotify = { - type: "CreateNotify", - id: X11WindowId, - parent_id: X11WindowId, - override_redirect: boolean, -}; - -type X11ApplicationEventDestroyNotify = { - type: "DestroyNotify", - id: X11WindowId, -} - -type X11ApplicationEventMapNotify = { - type: "MapNotify", - id: X11WindowId, -}; - -type X11ApplicationEventUnmapNotify = { - type: "UnmapNotify", - id: X11WindowId, -}; - -type X11ApplicationEventReparentNotify = { - type: "ReparentNotify", - id: X11WindowId, -}; - -type X11ApplicationEventTitlePropertyNotify = { - type: "TitlePropertyNotify", - id: X11WindowId, - title: string -}; - -type X11ApplicationEventClassPropertyNotify = { - type: "ClassPropertyNotify", - id: X11WindowId, - class: string, - instance: string -}; - -type X11ApplicationEventHintsPropertyNotify = { - type: "HintsPropertyNotify", - id: X11WindowId, - window_group: X11WindowId | undefined, -}; - -type X11ApplicationEventProtocolsPropertyNotify = { - type: "ProtocolsPropertyNotify", - id: X11WindowId, - protocols: X11WindowProtocol[], -}; - -type X11ApplicationEventTransientForPropertyNotify = { - type: "TransientForPropertyNotify", - id: X11WindowId, - transient_for: X11WindowId | undefined, -}; - -type X11ApplicationEventWindowTypePropertyNotify = { - type: "WindowTypePropertyNotify", - id: X11WindowId, - window_types: X11WindowType[] -}; - -type X11ApplicationEventDesktopFileNamePropertyNotify = { - type: "DesktopFileNamePropertyNotify", - id: X11WindowId, - desktop_file_name: string -}; export function focusX11Window(windowId: string) { linux_x11_focus_window(windowId) } -function openApplication(appId: string) { - return () => { - linux_open_application(appId) - } -} - export function applicationEventLoopX11( openWindows: Record, focusWindow: (windowId: string) => void, @@ -151,7 +49,7 @@ export function applicationEventLoopX11( (async () => { // noinspection InfiniteLoopJS while (true) { - const applicationEvent = await application_pending_event() as X11ApplicationEvent; + const applicationEvent = await application_x11_pending_event(); switch (applicationEvent.type) { case "Init": { windows[applicationEvent.id] = { @@ -190,7 +88,7 @@ export function applicationEventLoopX11( case "DestroyNotify": { delete windows[applicationEvent.id] - deleteOpenWindow(openWindows, applicationEvent.id, openApplication, focusWindow, get, add) + deleteOpenWindow(openWindows, applicationEvent.id, openLinuxApplication, focusWindow, get, add) break; } @@ -199,7 +97,7 @@ export function applicationEventLoopX11( if (window) { window.mapped = true; - validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -209,7 +107,7 @@ export function applicationEventLoopX11( if (window) { window.mapped = false; - deleteOpenWindow(openWindows, applicationEvent.id, openApplication, focusWindow, get, add) + deleteOpenWindow(openWindows, applicationEvent.id, openLinuxApplication, focusWindow, get, add) } break; @@ -220,7 +118,7 @@ export function applicationEventLoopX11( if (window) { window.mapped = true; - validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -230,7 +128,7 @@ export function applicationEventLoopX11( if (window) { window.title = applicationEvent.title; - validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -241,7 +139,7 @@ export function applicationEventLoopX11( window.class = applicationEvent.class; window.instance = applicationEvent.instance; - validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -251,7 +149,7 @@ export function applicationEventLoopX11( if (window) { window.windowGroup = applicationEvent.window_group; - validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -261,7 +159,7 @@ export function applicationEventLoopX11( if (window) { window.protocols = applicationEvent.protocols; - validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -271,7 +169,7 @@ export function applicationEventLoopX11( if (window) { window.transientFor = applicationEvent.transient_for; - validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -281,7 +179,7 @@ export function applicationEventLoopX11( if (window) { window.windowTypes = applicationEvent.window_types; - validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -291,7 +189,7 @@ export function applicationEventLoopX11( if (window) { window.desktopFileName = applicationEvent.desktop_file_name; - validateAndAddOpenWindow(window, openWindows, windows, openApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -343,7 +241,8 @@ function validateAndAddOpenWindow( addOpenWindow( appId, generatedEntrypoint, - window, + window.id, + window.title, openWindows, openApplication(appId), focusWindow, diff --git a/js/core/src/internal-all.ts b/js/core/src/internal-all.ts index ed94a69..c55c90a 100644 --- a/js/core/src/internal-all.ts +++ b/js/core/src/internal-all.ts @@ -3,5 +3,6 @@ export { open_settings, current_os, wayland, - application_pending_event, + application_x11_pending_event, + application_wayland_pending_event, } from "ext:core/ops"; diff --git a/js/core/src/internal-linux.ts b/js/core/src/internal-linux.ts index 37ab8a8..ef6cda3 100644 --- a/js/core/src/internal-linux.ts +++ b/js/core/src/internal-linux.ts @@ -3,4 +3,5 @@ export { linux_application_dirs, linux_open_application, linux_x11_focus_window, + linux_wayland_focus_window, } from "ext:core/ops"; diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 3f6af62..f38c96d 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -17,6 +17,7 @@ type DesktopPathActionRemove = { type LinuxDesktopApplicationData = { name: string icon: ArrayBuffer | undefined, + desktop_file_path: string, startup_wm_class: string | undefined, } @@ -144,12 +145,14 @@ declare module "gauntlet:bridge/internal-all" { function run_numbat(input: string): { left: string, right: string } function current_os(): string function wayland(): boolean - function application_pending_event(): Promise + function application_x11_pending_event(): Promise + function application_wayland_pending_event(): Promise } declare module "gauntlet:bridge/internal-linux" { function linux_open_application(desktop_id: string): void function linux_x11_focus_window(window_id: string): void + function linux_wayland_focus_window(window_id: string): void function linux_application_dirs(): string[] function linux_app_from_path(path: string): Promise> @@ -175,10 +178,12 @@ declare module "ext:core/ops" { function current_os(): string function wayland(): boolean - function application_pending_event(): Promise + function application_x11_pending_event(): Promise + function application_wayland_pending_event(): Promise function linux_open_application(desktop_id: string): void function linux_x11_focus_window(window_id: string): void + function linux_wayland_focus_window(window_id: string): void function linux_application_dirs(): string[] function linux_app_from_path(path: string): Promise> @@ -342,3 +347,124 @@ type TypeImageArray = { type: "array" item: PropertyType } + +type WaylandApplicationEvent = WaylandApplicationEventWindowOpened + | WaylandApplicationEventWindowClosed + | WaylandApplicationEventWindowTitleChanged + | WaylandApplicationEventWindowAppIdChanged + +type WaylandApplicationEventWindowOpened = { + type: "WindowOpened", + window_id: string, +}; +type WaylandApplicationEventWindowClosed = { + type: "WindowClosed", + window_id: string, +}; +type WaylandApplicationEventWindowTitleChanged = { + type: "WindowTitleChanged", + window_id: string, + title: string, +}; +type WaylandApplicationEventWindowAppIdChanged = { + type: "WindowAppIdChanged", + window_id: string, + app_id: string, +}; + +type X11WindowProtocol = "DeleteWindow" | "TakeFocus" +type X11WindowType = "DropdownMenu" | "Dialog" | "Menu" | "Notification" | "Normal" | "PopupMenu" | "Splash" | "Toolbar" | "Tooltip" | "Utility" +type X11WindowId = string + +type X11ApplicationEvent = X11ApplicationEventInit + | X11ApplicationEventCreateNotify + | X11ApplicationEventDestroyNotify + | X11ApplicationEventMapNotify + | X11ApplicationEventUnmapNotify + | X11ApplicationEventReparentNotify + | X11ApplicationEventTitlePropertyNotify + | X11ApplicationEventClassPropertyNotify + | X11ApplicationEventHintsPropertyNotify + | X11ApplicationEventProtocolsPropertyNotify + | X11ApplicationEventTransientForPropertyNotify + | X11ApplicationEventWindowTypePropertyNotify + | X11ApplicationEventDesktopFileNamePropertyNotify; + + +type X11ApplicationEventInit = { + type: "Init", + id: X11WindowId, + parent_id: X11WindowId, + override_redirect: boolean, + mapped: boolean, +}; + +type X11ApplicationEventCreateNotify = { + type: "CreateNotify", + id: X11WindowId, + parent_id: X11WindowId, + override_redirect: boolean, +}; + +type X11ApplicationEventDestroyNotify = { + type: "DestroyNotify", + id: X11WindowId, +} + +type X11ApplicationEventMapNotify = { + type: "MapNotify", + id: X11WindowId, +}; + +type X11ApplicationEventUnmapNotify = { + type: "UnmapNotify", + id: X11WindowId, +}; + +type X11ApplicationEventReparentNotify = { + type: "ReparentNotify", + id: X11WindowId, +}; + +type X11ApplicationEventTitlePropertyNotify = { + type: "TitlePropertyNotify", + id: X11WindowId, + title: string +}; + +type X11ApplicationEventClassPropertyNotify = { + type: "ClassPropertyNotify", + id: X11WindowId, + class: string, + instance: string +}; + +type X11ApplicationEventHintsPropertyNotify = { + type: "HintsPropertyNotify", + id: X11WindowId, + window_group: X11WindowId | undefined, +}; + +type X11ApplicationEventProtocolsPropertyNotify = { + type: "ProtocolsPropertyNotify", + id: X11WindowId, + protocols: X11WindowProtocol[], +}; + +type X11ApplicationEventTransientForPropertyNotify = { + type: "TransientForPropertyNotify", + id: X11WindowId, + transient_for: X11WindowId | undefined, +}; + +type X11ApplicationEventWindowTypePropertyNotify = { + type: "WindowTypePropertyNotify", + id: X11WindowId, + window_types: X11WindowType[] +}; + +type X11ApplicationEventDesktopFileNamePropertyNotify = { + type: "DesktopFileNamePropertyNotify", + id: X11WindowId, + desktop_file_name: string +}; \ No newline at end of file diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml index 6fe53e3..a927ae4 100644 --- a/rust/plugin_runtime/Cargo.toml +++ b/rust/plugin_runtime/Cargo.toml @@ -32,6 +32,7 @@ deno_runtime = { version = "0.188.0" } resvg = { version = "0.44.0", default-features = false} numbat = "1.14.0" which = "7.0.1" +uuid = "1.11.0" [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] libc = "0.2" @@ -41,6 +42,9 @@ encoding = "0.2" [target.'cfg(target_os = "linux")'.dependencies] freedesktop_entry_parser = "1.3" freedesktop-icons = "0.2" +wayland-protocols-wlr = { version = "0.3.5", features = ["client"] } +wayland-client = "0.31.7" +smithay-client-toolkit = "0.19.2" [target.'cfg(target_os = "macos")'.dependencies] cacao = "0.3.2" diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index cc7f9a7..923eed5 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -29,7 +29,7 @@ use crate::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_ use crate::model::JsInit; use crate::permissions::{permissions_to_deno}; use crate::plugin_data::PluginData; -use crate::plugins::applications::{application_pending_event, current_os, wayland, ApplicationContext}; +use crate::plugins::applications::{current_os, wayland, ApplicationContext}; use crate::plugins::numbat::{run_numbat, NumbatContext}; use crate::plugins::settings::open_settings; use crate::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; @@ -266,7 +266,6 @@ deno_core::extension!( // plugins applications current_os, wayland, - application_pending_event, // plugins settings open_settings, @@ -286,23 +285,6 @@ deno_core::extension!( }, ); -#[cfg(target_os = "linux")] -deno_core::extension!( - gauntlet_internal_linux, - ops = [ - // plugins applications linux - crate::plugins::applications::linux_app_from_path, - crate::plugins::applications::linux_application_dirs, - crate::plugins::applications::linux_open_application, - crate::plugins::applications::linux_x11_focus_window, - ], - esm_entry_point = "ext:gauntlet/internal-linux/bootstrap.js", - esm = [ - "ext:gauntlet/internal-linux/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-linux-bootstrap.js", - "ext:gauntlet/internal-linux.js" = "../../js/core/dist/internal-linux.js", - ] -); - #[cfg(target_os = "macos")] deno_core::extension!( gauntlet_internal_macos, @@ -406,14 +388,14 @@ pub async fn start_js_runtime( if init.plugin_id.to_string() == "bundled://gauntlet" { extensions.push(gauntlet_internal_all::init_ops_and_esm( NumbatContext::new(), - ApplicationContext::new() + ApplicationContext::new()? )); #[cfg(target_os = "macos")] extensions.push(gauntlet_internal_macos::init_ops_and_esm()); #[cfg(target_os = "linux")] - extensions.push(gauntlet_internal_linux::init_ops_and_esm()); + extensions.push(crate::plugins::applications::gauntlet_internal_linux::init_ops_and_esm()); } let mut worker = MainWorker::bootstrap_from_options( diff --git a/rust/plugin_runtime/src/plugins/applications.rs b/rust/plugin_runtime/src/plugins/applications.rs index f3d060c..b053dd8 100644 --- a/rust/plugin_runtime/src/plugins/applications.rs +++ b/rust/plugin_runtime/src/plugins/applications.rs @@ -13,8 +13,9 @@ use crate::plugin_data::PluginData; #[cfg(target_os = "linux")] mod linux; + #[cfg(target_os = "linux")] -mod x11; +pub use linux::gauntlet_internal_linux; #[cfg(target_os = "macos")] mod macos; @@ -37,6 +38,7 @@ pub enum DesktopPathAction { #[derive(Debug, Serialize)] pub struct DesktopApplication { name: String, + desktop_file_path: String, icon: Option>, startup_wm_class: Option, } @@ -79,188 +81,45 @@ pub fn current_os() -> &'static str { } #[op2(fast)] -pub fn wayland() -> bool { - let wayland = std::env::var("WAYLAND_DISPLAY") - .or_else(|_| std::env::var("WAYLAND_SOCKET")) - .is_ok(); +pub fn wayland(state: Rc>) -> bool { + let wayland = { + state + .borrow() + .borrow::() + .desktop + .is_wayland() + }; wayland } -pub struct ApplicationContext { - pub receiver: Rc>>, +pub enum DesktopEnvironment { + Linux(linux::LinuxDesktopEnvironment), } -impl ApplicationContext { - pub fn new() -> Self { - let (sender, receiver) = tokio::sync::mpsc::channel(100); - - let handle = Handle::current(); - +impl DesktopEnvironment { + fn new() -> anyhow::Result { #[cfg(target_os = "linux")] - std::thread::spawn(|| { - if let Err(e) = x11::listen_on_x11_events(handle, sender) { - tracing::error!("Error while listening on x11 events: {}", e); - } - }); + Ok(Self::Linux(linux::LinuxDesktopEnvironment::new()?)) + } - Self { - receiver: Rc::new(RefCell::new(receiver)) + fn is_wayland(&self) -> bool { + match self { + DesktopEnvironment::Linux(linux) => linux.is_wayland() } } } -#[derive(Debug, Deserialize, Serialize)] -#[serde(tag = "type")] -pub enum JsX11ApplicationEvent { - Init { - id: String, - parent_id: String, - override_redirect: bool, - mapped: bool - }, - CreateNotify { - id: String, - parent_id: String, - override_redirect: bool - }, - DestroyNotify { - id: String, - }, - MapNotify { - id: String, - }, - UnmapNotify { - id: String, - }, - ReparentNotify { - id: String, - }, - TitlePropertyNotify { - id: String, - title: String - }, - ClassPropertyNotify { - id: String, - class: String, - instance: String - }, - HintsPropertyNotify { - id: String, - window_group: Option, - }, - ProtocolsPropertyNotify { - id: String, - protocols: Vec, - }, - TransientForPropertyNotify { - id: String, - transient_for: Option, - }, - WindowTypePropertyNotify { - id: String, - window_types: Vec - }, - DesktopFileNamePropertyNotify { - id: String, - desktop_file_name: String - }, +pub struct ApplicationContext { + desktop: DesktopEnvironment, } -#[derive(Debug, Deserialize, Serialize)] -enum JSX11WindowProtocol { - TakeFocus, - DeleteWindow, -} - -#[derive(Debug, Deserialize, Serialize)] -enum JSX11WindowType { - DropdownMenu, - Dialog, - Menu, - Notification, - Normal, - PopupMenu, - Splash, - Toolbar, - Tooltip, - Utility, -} - - -#[op2(async)] -#[serde] -pub async fn application_pending_event(state: Rc>) -> anyhow::Result { - let receiver = { - state.borrow() - .borrow::() - .receiver - .clone() - }; - - let mut receiver = receiver.borrow_mut(); - let event = receiver.recv() - .await - .ok_or_else(|| anyhow!("plugin event stream was suddenly closed"))?; - - tracing::trace!("Received application event {:?}", event); - - Ok(event) -} - -#[cfg(target_os = "linux")] -#[op2(async)] -#[serde] -pub async fn linux_app_from_path(state: Rc>, #[string] path: String) -> anyhow::Result> { - let home_dir = { - let state = state.borrow(); - - let home_dir = state - .borrow::() - .home_dir(); - - home_dir - }; - - Ok(spawn_blocking(|| linux::linux_app_from_path(home_dir, PathBuf::from(path))).await?) -} - -#[cfg(target_os = "linux")] -#[op2] -#[serde] -pub fn linux_application_dirs(state: Rc>) -> Vec { - let home_dir = { - let state = state.borrow(); - - let home_dir = state - .borrow::() - .home_dir(); - - home_dir - }; - - linux::linux_application_dirs(home_dir) - .into_iter() - .map(|path| path.to_str().expect("non-utf8 paths are not supported").to_string()) - .collect() -} - -#[cfg(target_os = "linux")] -#[op2(fast)] -pub fn linux_open_application(#[string] desktop_file_id: String) -> anyhow::Result<()> { - - spawn_detached("gtk-launch", &[desktop_file_id])?; - - Ok(()) -} - -#[cfg(target_os = "linux")] -#[op2(fast)] -pub fn linux_x11_focus_window(#[string] x11_window_id: String) -> anyhow::Result<()> { - - x11::focus_window(x11_window_id)?; - - Ok(()) +impl ApplicationContext { + pub fn new() -> anyhow::Result { + Ok(Self { + desktop: DesktopEnvironment::new()?, + }) + } } #[cfg(target_os = "macos")] diff --git a/rust/plugin_runtime/src/plugins/applications/linux.rs b/rust/plugin_runtime/src/plugins/applications/linux/mod.rs similarity index 63% rename from rust/plugin_runtime/src/plugins/applications/linux.rs rename to rust/plugin_runtime/src/plugins/applications/linux/mod.rs index 616466f..f270e57 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/mod.rs @@ -1,15 +1,109 @@ -use std::collections::{HashMap, HashSet}; -use std::fs::Metadata; -use std::path::{Path, PathBuf}; - -use crate::plugins::applications::{resize_icon, DesktopApplication, DesktopPathAction}; +use crate::plugin_data::PluginData; +use crate::plugins::applications::{linux, resize_icon, spawn_detached, DesktopApplication, DesktopPathAction}; +use deno_core::{op2, OpState}; use freedesktop_entry_parser::parse_entry; use freedesktop_icons::lookup; use image::imageops::FilterType; use image::ImageFormat; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::fs::Metadata; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use tokio::sync::mpsc::Sender; +use tokio::task::spawn_blocking; use walkdir::WalkDir; -pub fn linux_application_dirs(home_dir: PathBuf) -> Vec { +mod x11; +mod wayland; + +deno_core::extension!( + gauntlet_internal_linux, + ops = [ + // plugins applications + linux_app_from_path, + linux_application_dirs, + linux_open_application, + x11::linux_x11_focus_window, + x11::application_x11_pending_event, + wayland::linux_wayland_focus_window, + wayland::application_wayland_pending_event, + ], + esm_entry_point = "ext:gauntlet/internal-linux/bootstrap.js", + esm = [ + "ext:gauntlet/internal-linux/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-linux-bootstrap.js", + "ext:gauntlet/internal-linux.js" = "../../js/core/dist/internal-linux.js", + ] +); + +pub enum LinuxDesktopEnvironment { + X11(x11::X11DesktopEnvironment), + Wayland(wayland::WaylandDesktopEnvironment), +} + +impl LinuxDesktopEnvironment { + pub fn new() -> anyhow::Result { + let wayland = std::env::var("WAYLAND_DISPLAY") + .or_else(|_| std::env::var("WAYLAND_SOCKET")) + .is_ok(); + + if wayland { + Ok(LinuxDesktopEnvironment::Wayland(wayland::WaylandDesktopEnvironment::new()?)) + } else { + Ok(LinuxDesktopEnvironment::X11(x11::X11DesktopEnvironment::new())) + } + } + + pub fn is_wayland(&self) -> bool { + matches!(self, LinuxDesktopEnvironment::Wayland(_)) + } +} + +#[op2(async)] +#[serde] +async fn linux_app_from_path(state: Rc>, #[string] path: String) -> anyhow::Result> { + let home_dir = { + let state = state.borrow(); + + let home_dir = state + .borrow::() + .home_dir(); + + home_dir + }; + + Ok(spawn_blocking(|| linux_app_from_path_async(home_dir, PathBuf::from(path))).await?) +} + +#[op2] +#[serde] +fn linux_application_dirs(state: Rc>) -> Vec { + let home_dir = { + let state = state.borrow(); + + let home_dir = state + .borrow::() + .home_dir(); + + home_dir + }; + + linux_application_dirs_inner(home_dir) + .into_iter() + .map(|path| path.to_str().expect("non-utf8 paths are not supported").to_string()) + .collect() +} + +#[op2(fast)] +fn linux_open_application(#[string] desktop_file_id: String) -> anyhow::Result<()> { + + spawn_detached("gtk-launch", &[desktop_file_id])?; + + Ok(()) +} + + +fn linux_application_dirs_inner(home_dir: PathBuf) -> Vec { let data_home = match std::env::var_os("XDG_DATA_HOME") { Some(val) => { PathBuf::from(val) @@ -49,8 +143,8 @@ pub fn linux_application_dirs(home_dir: PathBuf) -> Vec { .collect() } -pub fn linux_app_from_path(home_dir: PathBuf, path: PathBuf) -> Option { - let app_directories = linux_application_dirs(home_dir); +fn linux_app_from_path_async(home_dir: PathBuf, path: PathBuf) -> Option { + let app_directories = linux_application_dirs_inner(home_dir); let relative_to_app_dir = app_directories .into_iter() @@ -110,6 +204,10 @@ fn create_app_entry(desktop_file_path: &Path) -> Option { .inspect_err(|err| tracing::warn!("error parsing .desktop file at path {:?}: {:?}", desktop_file_path, err)) .ok()?; + let desktop_file_path_str = desktop_file_path.to_str() + .expect("non-utf8 paths are not supported") + .to_string(); + let entry = entry.section("Desktop Entry"); let name = entry.attr("Name")?; @@ -177,6 +275,7 @@ fn create_app_entry(desktop_file_path: &Path) -> Option { Some(DesktopApplication { name: name.to_string(), + desktop_file_path: desktop_file_path_str, icon, startup_wm_class, }) diff --git a/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs b/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs new file mode 100644 index 0000000..552f345 --- /dev/null +++ b/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs @@ -0,0 +1,252 @@ +use std::cell::RefCell; +use std::rc::Rc; +use anyhow::anyhow; +use deno_core::{op2, OpState}; +use serde::{Deserialize, Serialize}; +use smithay_client_toolkit::reexports::calloop; +use smithay_client_toolkit::reexports::calloop::{EventLoop, InsertError, RegistrationToken}; +use smithay_client_toolkit::reexports::calloop::channel::{Channel, Event}; +use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource; +use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState}; +use tokio::runtime::Handle; +use tokio::sync::mpsc::{Receiver, Sender}; +use wayland_client::{Connection, Dispatch, QueueHandle}; +use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContents}; +use wayland_client::protocol::wl_registry; +use wayland_client::protocol::wl_seat::WlSeat; +use crate::plugins::applications::{linux, ApplicationContext, DesktopEnvironment}; + +pub mod wlr; + +pub struct WaylandDesktopEnvironment { + activate_sender: calloop::channel::Sender, + event_receiver: Rc>>, +} + +impl WaylandDesktopEnvironment { + pub fn new() -> anyhow::Result { + let (event_sender, event_receiver) = tokio::sync::mpsc::channel(100); + let (activate_sender, activate_receiver) = calloop::channel::channel(); + + let environment = WaylandDesktopEnvironment { + activate_sender, + event_receiver: Rc::new(RefCell::new(event_receiver)), + }; + + let handle = Handle::current(); + + std::thread::spawn(|| { + if let Err(e) = run_wayland_client(handle, event_sender, activate_receiver) { + tracing::error!("Error while running wayland client: {:?}", e); + } + }); + + Ok(environment) + } + + pub fn focus_window(&self, window_uuid: String) -> anyhow::Result<()> { + self.activate_sender.send(window_uuid)?; + + Ok(()) + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum JsWaylandApplicationEvent { + WindowOpened { + window_id: String + }, + WindowClosed { + window_id: String + }, + WindowTitleChanged { + window_id: String, + title: String, + }, + WindowAppIdChanged { + window_id: String, + app_id: String + } +} + +pub struct WaylandState { + seat_state: SeatState, + tokio_handle: Handle, + sender: Sender, + inner: WaylandStateInner +} + +impl WaylandState { + fn new( + tokio_handle: Handle, + sender: Sender, + seat_state: SeatState, + globals: &GlobalList, + queue_handle: &QueueHandle + ) -> anyhow::Result { + + let inner = wlr::WlrWaylandState::new(globals, queue_handle) + .map(|state| WaylandStateInner::Wlr(state)) + .or_else(|_| anyhow::Ok(WaylandStateInner::None))?; + + Ok(WaylandState { + seat_state, + tokio_handle, + sender, + inner, + }) + } +} + +pub enum WaylandStateInner { + Wlr(wlr::WlrWaylandState), + Cosmic, + None +} + +fn send_event(tokio_handle: &Handle, sender: &Sender, app_event: JsWaylandApplicationEvent) { + let sender = sender.clone(); + tokio_handle.spawn(async move { + if let Err(e) = sender.send(app_event).await { + tracing::error!("Error while sending wayland application event: {:?}", e); + } + }); +} + +fn run_wayland_client( + tokio_handle: Handle, + event_sender: Sender, + activate_receiver: Channel, +) -> anyhow::Result<()> { + let conn = Connection::connect_to_env()?; + let (globals, event_queue) = registry_queue_init::(&conn)?; + + let mut event_loop = EventLoop::::try_new()?; + let queue_handle = event_queue.handle(); + let wayland_source = WaylandSource::new(conn.clone(), event_queue); + let seat_state = SeatState::new(&globals, &queue_handle); + let loop_handle = event_loop.handle(); + + if let Err(err) = loop_handle.insert_source(activate_receiver, activation_handler) { + tracing::error!("Unable to insert activation source into event loop: {:?}", err); + + Err(anyhow!("Unable to insert activation source into event loop"))? + }; + + if let Err(err) = wayland_source.insert(loop_handle) { + tracing::error!("Unable to insert wayland source into event loop: {:?}", err); + + Err(anyhow!("Unable to insert wayland source into event loop"))? + }; + + let mut state = WaylandState::new(tokio_handle, event_sender, seat_state, &globals, &queue_handle)?; + + loop { + if let Err(err) = event_loop.dispatch(None, &mut state) { + tracing::error!("Wayland event queue has failed: {:?}", err); + break; + } + } + + Ok(()) +} + +#[op2(fast)] +pub fn linux_wayland_focus_window(state: Rc>, #[string] window_uuid: String) -> anyhow::Result<()> { + { + let state = state.borrow(); + + let context = state + .borrow::(); + + match &context.desktop { + DesktopEnvironment::Linux(linux::LinuxDesktopEnvironment::Wayland(env)) => { + env.focus_window(window_uuid)?; + }, + _ => Err(anyhow!("Calling linux_wayland_focus_window on non-wayland platform"))? + }; + }; + + Ok(()) +} + +fn activation_handler(event: Event, _metadata: &mut (), state: &mut WaylandState) { + let window_uuid = match event { + Event::Msg(window_uuid) => window_uuid, + Event::Closed => panic!("activation source was closed") + }; + + match &state.inner { + WaylandStateInner::Wlr(wlr) => { + if let Err(err) = wlr.focus_window(window_uuid, &state.seat_state) { + tracing::error!("Unable to focus wayland window: {:?}", err); + }; + } + WaylandStateInner::Cosmic => {} + WaylandStateInner::None => { + tracing::error!("Calling focus window when there is no supported wayland protocols available"); + } + } +} + + +#[op2(async)] +#[serde] +pub async fn application_wayland_pending_event(state: Rc>) -> anyhow::Result { + let receiver = { + let state = state.borrow(); + + let context = state + .borrow::(); + + match &context.desktop { + DesktopEnvironment::Linux(linux::LinuxDesktopEnvironment::Wayland(env)) => { + env.event_receiver.clone() + }, + _ => Err(anyhow!("Calling application_wayland_pending_event on non-wayland platform"))? + } + }; + + let mut receiver = receiver.borrow_mut(); + let event = receiver.recv() + .await + .ok_or_else(|| anyhow!("plugin event stream was suddenly closed"))?; + + tracing::trace!("Received application event {:?}", event); + + Ok(event) +} + + +impl Dispatch for WaylandState { + fn event( + _state: &mut WaylandState, + _proxy: &wl_registry::WlRegistry, + _event: wl_registry::Event, + _data: &GlobalListContents, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl SeatHandler for WaylandState { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat) { + } + + fn new_capability(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat, _capability: Capability) { + } + + fn remove_capability(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat, _capability: Capability) { + } + + fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat) { + } +} + +smithay_client_toolkit::delegate_seat!(WaylandState); diff --git a/rust/plugin_runtime/src/plugins/applications/linux/wayland/wlr.rs b/rust/plugin_runtime/src/plugins/applications/linux/wayland/wlr.rs new file mode 100644 index 0000000..287cd70 --- /dev/null +++ b/rust/plugin_runtime/src/plugins/applications/linux/wayland/wlr.rs @@ -0,0 +1,173 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use anyhow::anyhow; +use smithay_client_toolkit::reexports::calloop::channel::Sender; +use smithay_client_toolkit::seat::SeatState; +use tokio::runtime::Handle; +use crate::plugins::applications::linux::wayland::{send_event, JsWaylandApplicationEvent, WaylandState, WaylandStateInner}; +use wayland_client::globals::GlobalList; +use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle}; +use wayland_client::backend::ObjectId; +use wayland_client::protocol::wl_seat::WlSeat; +use wayland_protocols_wlr::foreign_toplevel::v1::client::{zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1}; + +pub struct WlrWaylandState { + uuid_to_obj_id: HashMap, + obj_id_to_uuid: HashMap, + toplevels: HashMap, +} + +impl WlrWaylandState { + pub fn new(globals: &GlobalList, queue_handle: &QueueHandle) -> anyhow::Result { + let _management = globals + .bind::( + &queue_handle, + 3..=3, + (), + )?; + + Ok(Self { + uuid_to_obj_id: HashMap::new(), + obj_id_to_uuid: HashMap::new(), + toplevels: HashMap::new(), + }) + } + + pub fn focus_window(&self, window_uuid: String, seat_state: &SeatState) -> anyhow::Result<()> { + let obj_id = self.uuid_to_obj_id + .get(&window_uuid) + .ok_or(anyhow!("Unable to find object id for window uuid: {}", window_uuid))?; + + let toplevel = self.toplevels + .get(&obj_id) + .ok_or(anyhow!("Unable to find object id for window uuid: {}", window_uuid))?; + + match seat_state.seats().next() { + Some(seat) => toplevel.activate(&seat), + None => Err(anyhow!("no wayland seats found"))? + }; + + Ok(()) + } +} + +impl Dispatch for WaylandState { + fn event( + state: &mut Self, + _proxy: &zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1, + event: ::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + match event { + zwlr_foreign_toplevel_manager_v1::Event::Toplevel { toplevel } => { + match &mut state.inner { + WaylandStateInner::Wlr(inner) => { + let window_id = uuid::Uuid::new_v4().to_string(); + + inner.uuid_to_obj_id.insert(window_id.clone(), toplevel.id()); + inner.obj_id_to_uuid.insert(toplevel.id(), window_id.clone()); + inner.toplevels.insert(toplevel.id(), toplevel); + + send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowOpened { + window_id, + }); + } + WaylandStateInner::Cosmic => { + todo!() + } + WaylandStateInner::None => {} + } + } + _ => {} + } + } + + event_created_child!(WaylandState, zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1, [ + zwlr_foreign_toplevel_manager_v1::EVT_TOPLEVEL_OPCODE => (zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, ()), + ]); +} + +impl Dispatch for WaylandState { + fn event( + state: &mut Self, + proxy: &zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, + event: ::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + match event { + zwlr_foreign_toplevel_handle_v1::Event::Title { title } => { + match &state.inner { + WaylandStateInner::Wlr(inner) => { + match inner.obj_id_to_uuid.get(&proxy.id()) { + Some(window_id) => { + send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowTitleChanged { + window_id: window_id.clone(), + title, + }); + } + None => { + tracing::warn!("Received event for wlr wayland toplevel that doesn't exist in state"); + } + } + } + WaylandStateInner::Cosmic => { + todo!() + } + WaylandStateInner::None => {} + } + } + zwlr_foreign_toplevel_handle_v1::Event::AppId { app_id } => { + match &state.inner { + WaylandStateInner::Wlr(inner) => { + match inner.obj_id_to_uuid.get(&proxy.id()) { + Some(window_id) => { + send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowAppIdChanged { + window_id: window_id.clone(), + app_id, + }); + } + None => { + tracing::warn!("Received event for wlr wayland toplevel that doesn't exist in state"); + } + } + } + WaylandStateInner::Cosmic => { + todo!() + } + WaylandStateInner::None => {} + } + } + zwlr_foreign_toplevel_handle_v1::Event::Closed => { + match &mut state.inner { + WaylandStateInner::Wlr(inner) => { + + inner.toplevels.remove(&proxy.id()); + match inner.obj_id_to_uuid.remove(&proxy.id()) { + Some(window_id) => { + inner.uuid_to_obj_id.remove(&window_id); + + send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowClosed { + window_id: window_id.clone(), + }); + } + None => { + tracing::warn!("Received event for wlr wayland toplevel that doesn't exist in state"); + } + } + } + WaylandStateInner::Cosmic => { + todo!() + } + WaylandStateInner::None => {} + } + } + _ => {} + } + } +} \ No newline at end of file diff --git a/rust/plugin_runtime/src/plugins/applications/x11.rs b/rust/plugin_runtime/src/plugins/applications/linux/x11.rs similarity index 82% rename from rust/plugin_runtime/src/plugins/applications/x11.rs rename to rust/plugin_runtime/src/plugins/applications/linux/x11.rs index 461c684..a716161 100644 --- a/rust/plugin_runtime/src/plugins/applications/x11.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/x11.rs @@ -1,22 +1,160 @@ -use std::collections::HashMap; -use crate::plugins::applications::{JSX11WindowProtocol, JSX11WindowType, JsX11ApplicationEvent}; -use std::convert::Infallible; -use std::str::FromStr; +use crate::plugins::applications::linux::x11; use anyhow::anyhow; +use deno_core::{op2, OpState}; use encoding::{DecoderTrap, Encoding}; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::convert::Infallible; +use std::rc::Rc; +use std::str::FromStr; use tokio::runtime::Handle; -use tokio::sync::mpsc::Sender; +use tokio::sync::mpsc::{Receiver, Sender}; use x11rb::connection::Connection; use x11rb::errors::ConnectionError; use x11rb::properties::{WmClass, WmHints}; -use x11rb::protocol::xproto::{Atom, AtomEnum, ClientMessageEvent, ConfigureWindowAux, InputFocus, MapState, StackMode, Window}; use x11rb::protocol::xproto::ConnectionExt; +use x11rb::protocol::xproto::{Atom, AtomEnum, ClientMessageEvent, ConfigureWindowAux, InputFocus, MapState, StackMode, Window}; use x11rb::protocol::xproto::{ChangeWindowAttributesAux, EventMask}; use x11rb::protocol::Event; use x11rb::rust_connection::RustConnection; +use crate::plugins::applications::{linux, ApplicationContext, DesktopEnvironment}; + +pub struct X11DesktopEnvironment { + receiver: Rc>>, +} + +impl X11DesktopEnvironment { + pub fn new() -> Self { + let (sender, receiver) = tokio::sync::mpsc::channel(100); + + let handle = Handle::current(); + + std::thread::spawn(move || { + if let Err(e) = listen_on_x11_events(handle, sender.clone()) { + tracing::error!("Error while listening on x11 events: {}", e); + } + }); + + Self { + receiver: Rc::new(RefCell::new(receiver)), + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "type")] +enum JsX11ApplicationEvent { + Init { + id: String, + parent_id: String, + override_redirect: bool, + mapped: bool + }, + CreateNotify { + id: String, + parent_id: String, + override_redirect: bool + }, + DestroyNotify { + id: String, + }, + MapNotify { + id: String, + }, + UnmapNotify { + id: String, + }, + ReparentNotify { + id: String, + }, + TitlePropertyNotify { + id: String, + title: String + }, + ClassPropertyNotify { + id: String, + class: String, + instance: String + }, + HintsPropertyNotify { + id: String, + window_group: Option, + }, + ProtocolsPropertyNotify { + id: String, + protocols: Vec, + }, + TransientForPropertyNotify { + id: String, + transient_for: Option, + }, + WindowTypePropertyNotify { + id: String, + window_types: Vec + }, + DesktopFileNamePropertyNotify { + id: String, + desktop_file_name: String + }, +} + +#[derive(Debug, Deserialize, Serialize)] +enum JSX11WindowProtocol { + TakeFocus, + DeleteWindow, +} + +#[derive(Debug, Deserialize, Serialize)] +enum JSX11WindowType { + DropdownMenu, + Dialog, + Menu, + Notification, + Normal, + PopupMenu, + Splash, + Toolbar, + Tooltip, + Utility, +} -pub fn focus_window(window_id: String) -> anyhow::Result<()> { +#[op2(async)] +#[serde] +pub async fn application_x11_pending_event(state: Rc>) -> anyhow::Result { + let receiver = { + let state = state.borrow(); + + let context = state + .borrow::(); + + match &context.desktop { + DesktopEnvironment::Linux(linux::LinuxDesktopEnvironment::X11(X11DesktopEnvironment { receiver })) => receiver.clone(), + _ => Err(anyhow!("Calling application_x11_pending_event on non-x11 platform"))? + } + }; + + let mut receiver = receiver.borrow_mut(); + let event = receiver.recv() + .await + .ok_or_else(|| anyhow!("plugin event stream was suddenly closed"))?; + + tracing::trace!("Received application event {:?}", event); + + Ok(event) +} + + +#[op2(fast)] +pub fn linux_x11_focus_window(#[string] x11_window_id: String) -> anyhow::Result<()> { + + focus_window(x11_window_id)?; + + Ok(()) +} + +fn focus_window(window_id: String) -> anyhow::Result<()> { // https://github.com/freedesktop-unofficial-mirror/xcb__util-wm/blob/24eb17df2e1245885e72c9d4bbb0a0f69f0700f2/ewmh/xcb_ewmh.h.m4#L1268 // this basically reimplementation of xcb_ewmh_request_change_active_window @@ -35,7 +173,7 @@ pub fn focus_window(window_id: String) -> anyhow::Result<()> { let current_active_window = active_window_property .value32() .and_then(|mut iter| iter.next()) - .unwrap_or(0); // no focused window + .unwrap_or(x11rb::NONE); // no focused window // these values are same as rofi let source_indication: u32 = 2; // XCB_EWMH_CLIENT_SOURCE_TYPE_OTHER @@ -70,7 +208,7 @@ fn send_event(tokio_handle: &Handle, sender: &Sender, app let sender = sender.clone(); tokio_handle.spawn(async move { if let Err(e) = sender.send(app_event).await { - tracing::error!("Error while sending x11 connection: {:?}", e); + tracing::error!("Error while sending x11 application event: {:?}", e); } }); } From 40e8d578bac273d888291e8efc450ec56a8ef316 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 31 Dec 2024 19:25:12 +0100 Subject: [PATCH 256/540] Fix focus on action panel for search result going outside of list --- rust/client/src/ui/state/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index a9a5952..d19fd27 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -345,7 +345,7 @@ impl Focus for GlobalState { MainViewState::SearchResultActionPanel { focused_action_item } => { if let Some(search_item) = focused_search_result.get(focus_list) { if search_item.entrypoint_actions.len() != 0 { - focused_action_item.focus_next(search_item.entrypoint_actions.len() + 1) + focused_action_item.focus_next(search_item.entrypoint_actions.len()) .unwrap_or_else(|| Task::none()) } else { Task::none() From 434dd7a25260295ed2448e5fb74fe0b48b0728d9 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 31 Dec 2024 19:29:04 +0100 Subject: [PATCH 257/540] Fix build on non-linux platforms --- rust/plugin_runtime/src/plugins/applications.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/rust/plugin_runtime/src/plugins/applications.rs b/rust/plugin_runtime/src/plugins/applications.rs index b053dd8..849c4d4 100644 --- a/rust/plugin_runtime/src/plugins/applications.rs +++ b/rust/plugin_runtime/src/plugins/applications.rs @@ -94,18 +94,25 @@ pub fn wayland(state: Rc>) -> bool { } pub enum DesktopEnvironment { + #[cfg(target_os = "linux")] Linux(linux::LinuxDesktopEnvironment), + None, } impl DesktopEnvironment { fn new() -> anyhow::Result { - #[cfg(target_os = "linux")] - Ok(Self::Linux(linux::LinuxDesktopEnvironment::new()?)) + let result = Ok(Self::Linux(linux::LinuxDesktopEnvironment::new()?)); + + #[cfg(not(target_os = "linux"))] + let result = None; + + result } fn is_wayland(&self) -> bool { match self { - DesktopEnvironment::Linux(linux) => linux.is_wayland() + DesktopEnvironment::Linux(linux) => linux.is_wayland(), + DesktopEnvironment::None => false } } } From 4f2add81cc0cb780cb37054dfc4fc5ffc64313aa Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 31 Dec 2024 21:04:05 +0100 Subject: [PATCH 258/540] Add cosmic support for window tracking --- Cargo.lock | 37 ++++ rust/plugin_runtime/Cargo.toml | 1 + .../src/plugins/applications.rs | 4 +- .../applications/linux/wayland/cosmic.rs | 177 ++++++++++++++++++ .../plugins/applications/linux/wayland/mod.rs | 11 +- .../plugins/applications/linux/wayland/wlr.rs | 20 +- 6 files changed, 230 insertions(+), 20 deletions(-) create mode 100644 rust/plugin_runtime/src/plugins/applications/linux/wayland/cosmic.rs diff --git a/Cargo.lock b/Cargo.lock index fddcf1c..b6d5ada 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1643,6 +1643,20 @@ dependencies = [ "libc", ] +[[package]] +name = "cosmic-protocols" +version = "0.1.0" +source = "git+https://github.com/pop-os/cosmic-protocols.git#d218c76b58c7a3b20dd5e7943f93fc306a1b81b8" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.5", + "wayland-protocols-wlr 0.3.5", + "wayland-scanner", + "wayland-server", +] + [[package]] name = "cosmic-text" version = "0.12.1" @@ -4104,6 +4118,7 @@ dependencies = [ "bincode 2.0.0-rc.3", "bytes", "cacao", + "cosmic-protocols", "deno_core", "deno_runtime", "encoding", @@ -5706,6 +5721,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + [[package]] name = "ipconfig" version = "0.3.2" @@ -11817,6 +11838,7 @@ dependencies = [ "wayland-backend", "wayland-client", "wayland-scanner", + "wayland-server", ] [[package]] @@ -11869,6 +11891,7 @@ dependencies = [ "wayland-client", "wayland-protocols 0.32.5", "wayland-scanner", + "wayland-server", ] [[package]] @@ -11882,6 +11905,20 @@ dependencies = [ "quote", ] +[[package]] +name = "wayland-server" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89532cc712a2adb119eb4d09694b402576052254d0bb284f82ac1c47fb786ad" +dependencies = [ + "bitflags 2.6.0", + "downcast-rs", + "io-lifetimes", + "rustix", + "wayland-backend", + "wayland-scanner", +] + [[package]] name = "wayland-sys" version = "0.31.5" diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml index a927ae4..834f0c0 100644 --- a/rust/plugin_runtime/Cargo.toml +++ b/rust/plugin_runtime/Cargo.toml @@ -43,6 +43,7 @@ encoding = "0.2" freedesktop_entry_parser = "1.3" freedesktop-icons = "0.2" wayland-protocols-wlr = { version = "0.3.5", features = ["client"] } +cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols.git" } wayland-client = "0.31.7" smithay-client-toolkit = "0.19.2" diff --git a/rust/plugin_runtime/src/plugins/applications.rs b/rust/plugin_runtime/src/plugins/applications.rs index 849c4d4..3052152 100644 --- a/rust/plugin_runtime/src/plugins/applications.rs +++ b/rust/plugin_runtime/src/plugins/applications.rs @@ -101,16 +101,18 @@ pub enum DesktopEnvironment { impl DesktopEnvironment { fn new() -> anyhow::Result { + #[cfg(target_os = "linux")] let result = Ok(Self::Linux(linux::LinuxDesktopEnvironment::new()?)); #[cfg(not(target_os = "linux"))] - let result = None; + let result = Ok(Self::None); result } fn is_wayland(&self) -> bool { match self { + #[cfg(target_os = "linux")] DesktopEnvironment::Linux(linux) => linux.is_wayland(), DesktopEnvironment::None => false } diff --git a/rust/plugin_runtime/src/plugins/applications/linux/wayland/cosmic.rs b/rust/plugin_runtime/src/plugins/applications/linux/wayland/cosmic.rs new file mode 100644 index 0000000..20b885a --- /dev/null +++ b/rust/plugin_runtime/src/plugins/applications/linux/wayland/cosmic.rs @@ -0,0 +1,177 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use anyhow::anyhow; +use smithay_client_toolkit::reexports::calloop::channel::Sender; +use smithay_client_toolkit::seat::SeatState; +use tokio::runtime::Handle; +use crate::plugins::applications::linux::wayland::{send_event, JsWaylandApplicationEvent, WaylandState, WaylandStateInner}; +use wayland_client::globals::GlobalList; +use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle}; +use wayland_client::backend::ObjectId; +use wayland_client::protocol::wl_seat::WlSeat; +use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1; +use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_info_v1; +use cosmic_protocols::toplevel_management::v1::client::zcosmic_toplevel_manager_v1; + +pub struct CosmicWaylandState { + uuid_to_obj_id: HashMap, + obj_id_to_uuid: HashMap, + toplevels: HashMap, + management: zcosmic_toplevel_manager_v1::ZcosmicToplevelManagerV1, +} + +impl CosmicWaylandState { + pub fn new(globals: &GlobalList, queue_handle: &QueueHandle) -> anyhow::Result { + let management = globals + .bind::( + &queue_handle, + 3..=3, + (), + )?; + + Ok(Self { + management, + uuid_to_obj_id: HashMap::new(), + obj_id_to_uuid: HashMap::new(), + toplevels: HashMap::new(), + }) + } + + pub fn focus_window(&self, window_uuid: String, seat_state: &SeatState) -> anyhow::Result<()> { + let obj_id = self.uuid_to_obj_id + .get(&window_uuid) + .ok_or(anyhow!("Unable to find object id for window uuid: {}", window_uuid))?; + + let toplevel = self.toplevels + .get(&obj_id) + .ok_or(anyhow!("Unable to find object id for window uuid: {}", window_uuid))?; + + match seat_state.seats().next() { + Some(seat) => self.management.activate(&toplevel, &seat), + None => Err(anyhow!("no wayland seats found"))? + }; + + Ok(()) + } +} + +impl Dispatch for WaylandState { + fn event( + _state: &mut Self, + _proxy: &zcosmic_toplevel_manager_v1::ZcosmicToplevelManagerV1, + _event: ::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle + ) { + } +} + +impl Dispatch for WaylandState { + fn event( + state: &mut Self, + _proxy: &zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1, + event: ::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + match event { + zcosmic_toplevel_info_v1::Event::Toplevel { toplevel } => { + match &mut state.inner { + WaylandStateInner::Cosmic(inner) => { + let window_id = uuid::Uuid::new_v4().to_string(); + + inner.uuid_to_obj_id.insert(window_id.clone(), toplevel.id()); + inner.obj_id_to_uuid.insert(toplevel.id(), window_id.clone()); + inner.toplevels.insert(toplevel.id(), toplevel); + + send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowOpened { + window_id, + }); + } + _ => {} + } + } + _ => {} + } + } + + event_created_child!(WaylandState, zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1, [ + zcosmic_toplevel_info_v1::EVT_TOPLEVEL_OPCODE => (zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, ()), + ]); +} + +impl Dispatch for WaylandState { + fn event( + state: &mut Self, + proxy: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, + event: ::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + match event { + zcosmic_toplevel_handle_v1::Event::Title { title } => { + match &state.inner { + WaylandStateInner::Cosmic(inner) => { + match inner.obj_id_to_uuid.get(&proxy.id()) { + Some(window_id) => { + send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowTitleChanged { + window_id: window_id.clone(), + title, + }); + } + None => { + tracing::warn!("Received event for cosmic wayland toplevel that doesn't exist in state"); + } + } + } + _ => {} + } + } + zcosmic_toplevel_handle_v1::Event::AppId { app_id } => { + match &state.inner { + WaylandStateInner::Cosmic(inner) => { + match inner.obj_id_to_uuid.get(&proxy.id()) { + Some(window_id) => { + send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowAppIdChanged { + window_id: window_id.clone(), + app_id, + }); + } + None => { + tracing::warn!("Received event for cosmic wayland toplevel that doesn't exist in state"); + } + } + } + _ => {} + } + } + zcosmic_toplevel_handle_v1::Event::Closed => { + match &mut state.inner { + WaylandStateInner::Cosmic(inner) => { + + inner.toplevels.remove(&proxy.id()); + match inner.obj_id_to_uuid.remove(&proxy.id()) { + Some(window_id) => { + inner.uuid_to_obj_id.remove(&window_id); + + send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowClosed { + window_id: window_id.clone(), + }); + } + None => { + tracing::warn!("Received event for cosmic wayland toplevel that doesn't exist in state"); + } + } + } + _ => {} + } + } + _ => {} + } + } +} \ No newline at end of file diff --git a/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs b/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs index 552f345..1288ea9 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs @@ -16,7 +16,8 @@ use wayland_client::protocol::wl_registry; use wayland_client::protocol::wl_seat::WlSeat; use crate::plugins::applications::{linux, ApplicationContext, DesktopEnvironment}; -pub mod wlr; +mod wlr; +mod cosmic; pub struct WaylandDesktopEnvironment { activate_sender: calloop::channel::Sender, @@ -101,7 +102,7 @@ impl WaylandState { pub enum WaylandStateInner { Wlr(wlr::WlrWaylandState), - Cosmic, + Cosmic(cosmic::CosmicWaylandState), None } @@ -183,7 +184,11 @@ fn activation_handler(event: Event, _metadata: &mut (), state: &mut Wayl tracing::error!("Unable to focus wayland window: {:?}", err); }; } - WaylandStateInner::Cosmic => {} + WaylandStateInner::Cosmic(cosmic) => { + if let Err(err) = cosmic.focus_window(window_uuid, &state.seat_state) { + tracing::error!("Unable to focus wayland window: {:?}", err); + }; + } WaylandStateInner::None => { tracing::error!("Calling focus window when there is no supported wayland protocols available"); } diff --git a/rust/plugin_runtime/src/plugins/applications/linux/wayland/wlr.rs b/rust/plugin_runtime/src/plugins/applications/linux/wayland/wlr.rs index 287cd70..6ad9349 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/wayland/wlr.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/wayland/wlr.rs @@ -76,10 +76,7 @@ impl Dispatch { - todo!() - } - WaylandStateInner::None => {} + _ => {} } } _ => {} @@ -116,10 +113,7 @@ impl Dispatch } } } - WaylandStateInner::Cosmic => { - todo!() - } - WaylandStateInner::None => {} + _ => {} } } zwlr_foreign_toplevel_handle_v1::Event::AppId { app_id } => { @@ -137,10 +131,7 @@ impl Dispatch } } } - WaylandStateInner::Cosmic => { - todo!() - } - WaylandStateInner::None => {} + _ => {} } } zwlr_foreign_toplevel_handle_v1::Event::Closed => { @@ -161,10 +152,7 @@ impl Dispatch } } } - WaylandStateInner::Cosmic => { - todo!() - } - WaylandStateInner::None => {} + _ => {} } } _ => {} From b406da159840b22e8dbc9df1c1b5191f8a03393c Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 1 Jan 2025 11:52:41 +0100 Subject: [PATCH 259/540] Implement applications plugin on windows --- Cargo.lock | 15 ++ bundled_plugins/gauntlet/gauntlet.toml | 3 +- bundled_plugins/gauntlet/src/applications.tsx | 22 ++ .../gauntlet/src/window/shared.tsx | 1 - .../gauntlet/src/window/wayland.ts | 3 +- bundled_plugins/gauntlet/src/window/x11.ts | 3 +- js/bridge_build/src/index.ts | 6 + js/core/rollup.config.ts | 1 + js/core/src/internal-all.ts | 2 - js/core/src/internal-linux.ts | 2 + js/core/src/internal-windows.ts | 5 + js/typings/index.d.ts | 21 +- rust/plugin_runtime/Cargo.toml | 5 + rust/plugin_runtime/src/deno.rs | 7 +- .../src/plugins/applications.rs | 13 +- .../src/plugins/applications/windows.rs | 208 ++++++++++++++++++ 16 files changed, 303 insertions(+), 14 deletions(-) create mode 100644 js/core/src/internal-windows.ts create mode 100644 rust/plugin_runtime/src/plugins/applications/windows.rs diff --git a/Cargo.lock b/Cargo.lock index b6d5ada..7e001ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4138,6 +4138,7 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "once_cell", + "open", "plist", "regex", "resvg", @@ -4152,6 +4153,8 @@ dependencies = [ "wayland-client", "wayland-protocols-wlr 0.3.5", "which 7.0.1", + "windows", + "windows-icons", "x11rb", ] @@ -12294,6 +12297,18 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-icons" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd18e61837c404baa46146d3ea12241073ee7b4457d5ad73751d0e8f49dad9a" +dependencies = [ + "base64 0.22.1", + "image 0.25.5", + "winapi", + "windows", +] + [[package]] name = "windows-implement" version = "0.58.0" diff --git a/bundled_plugins/gauntlet/gauntlet.toml b/bundled_plugins/gauntlet/gauntlet.toml index 1f86d0d..74d8d72 100644 --- a/bundled_plugins/gauntlet/gauntlet.toml +++ b/bundled_plugins/gauntlet/gauntlet.toml @@ -31,7 +31,8 @@ clipboard = ["write"] read = [ # technically only uses locations defined by XDG Desktop Entry Specification, but # the spec allows for customization via XDG_DATA_DIRS and XDG_DATA_HOME env vars so it can be any path - "/" + "/", + "C:\\" ] [[supported_system]] diff --git a/bundled_plugins/gauntlet/src/applications.tsx b/bundled_plugins/gauntlet/src/applications.tsx index 3092df4..9073502 100644 --- a/bundled_plugins/gauntlet/src/applications.tsx +++ b/bundled_plugins/gauntlet/src/applications.tsx @@ -18,6 +18,7 @@ import { import { applicationAccessories, applicationActions, OpenWindowData } from "./window/shared"; import { applicationEventLoopX11, focusX11Window } from "./window/x11"; import { applicationEventLoopWayland, focusWaylandWindow } from "./window/wayland"; +import { windows_app_from_path, windows_application_dirs, windows_open_application } from "gauntlet:bridge/internal-windows"; export default async function Applications({ add, remove, get, getAll }: GeneratorProps): Promise void)> { const openWindows: Record = {}; @@ -179,6 +180,27 @@ export default async function Applications({ add, remove, get, getAll }: Generat { exts: ["app"], maxDepth: 2 } ); } + case "windows": { + return await genericGenerator( + windows_application_dirs(), + path => windows_app_from_path(path), + (_id, data) => ({ + name: data.name, + actions: [ + { + label: "Open application", + run: () => { + windows_open_application(data.path) + }, + } + ], + icon: data.icon, + }), + add, + remove, + { exts: ["lnk", "exe"], maxDepth: 2 } + ); + } } } diff --git a/bundled_plugins/gauntlet/src/window/shared.tsx b/bundled_plugins/gauntlet/src/window/shared.tsx index e5e0c8d..6f5f9b0 100644 --- a/bundled_plugins/gauntlet/src/window/shared.tsx +++ b/bundled_plugins/gauntlet/src/window/shared.tsx @@ -1,6 +1,5 @@ import { GeneratedCommand, GeneratedCommandAccessory, GeneratedCommandAction } from "@project-gauntlet/api/helpers"; import { List } from "@project-gauntlet/api/components"; -import { X11WindowData } from "./x11"; import { linux_open_application } from "gauntlet:bridge/internal-linux"; export type OpenWindowData = { diff --git a/bundled_plugins/gauntlet/src/window/wayland.ts b/bundled_plugins/gauntlet/src/window/wayland.ts index 9a04ef9..6251689 100644 --- a/bundled_plugins/gauntlet/src/window/wayland.ts +++ b/bundled_plugins/gauntlet/src/window/wayland.ts @@ -1,7 +1,6 @@ import { addOpenWindow, deleteOpenWindow, openLinuxApplication, OpenWindowData } from "./shared"; import { GeneratedCommand } from "@project-gauntlet/api/helpers"; -import { application_wayland_pending_event } from "gauntlet:bridge/internal-all"; -import { linux_wayland_focus_window } from "gauntlet:bridge/internal-linux"; +import { linux_wayland_focus_window, application_wayland_pending_event } from "gauntlet:bridge/internal-linux"; export function focusWaylandWindow(windowId: string) { diff --git a/bundled_plugins/gauntlet/src/window/x11.ts b/bundled_plugins/gauntlet/src/window/x11.ts index 17127e8..16e0b06 100644 --- a/bundled_plugins/gauntlet/src/window/x11.ts +++ b/bundled_plugins/gauntlet/src/window/x11.ts @@ -1,7 +1,6 @@ import { GeneratedCommand } from "@project-gauntlet/api/helpers"; -import { application_x11_pending_event } from "gauntlet:bridge/internal-all"; import { addOpenWindow, deleteOpenWindow, openLinuxApplication, OpenWindowData } from "./shared"; -import { linux_x11_focus_window } from "gauntlet:bridge/internal-linux"; +import { application_x11_pending_event, linux_x11_focus_window } from "gauntlet:bridge/internal-linux"; export type X11WindowData = { // x11 window id diff --git a/js/bridge_build/src/index.ts b/js/bridge_build/src/index.ts index b4ff4c7..111bd0d 100644 --- a/js/bridge_build/src/index.ts +++ b/js/bridge_build/src/index.ts @@ -174,6 +174,7 @@ const reactJsxRuntimeExports = collectExports(`../react/dist/dev/react-jsx-runti const internalAllExports = collectExports(`../core/dist/internal-all.js`); const internalLinuxExports = collectExports(`../core/dist/internal-linux.js`); const internalMacosExports = collectExports(`../core/dist/internal-macos.js`); +const internalWindowsExports = collectExports(`../core/dist/internal-windows.js`); generate( `${outDir}/bridge-bootstrap.js`, @@ -197,6 +198,10 @@ generate(`${outDir}/bridge-internal-macos-bootstrap.js`, generateInternal({ "GauntletInternalMacos": { importUrl: "ext:gauntlet/internal-macos.js", exports: internalMacosExports } })) +generate(`${outDir}/bridge-internal-windows-bootstrap.js`, generateInternal({ + "GauntletInternalWindows": { importUrl: "ext:gauntlet/internal-windows.js", exports: internalWindowsExports } +})) + generate(`${outDir}/bridge-components.js`, generateExternal("GauntletComponents", componentExports)) generate(`${outDir}/bridge-helpers.js`, generateExternal("GauntletHelpers", helpersExports)) @@ -208,5 +213,6 @@ generate(`${outDir}/bridge-react-jsx-runtime.js`, generateExternal("GauntletReac generate(`${outDir}/bridge-internal-all.js`, generateExternal("GauntletInternalAll", internalAllExports)) generate(`${outDir}/bridge-internal-linux.js`, generateExternal("GauntletInternalLinux", internalLinuxExports)) generate(`${outDir}/bridge-internal-macos.js`, generateExternal("GauntletInternalMacos", internalMacosExports)) +generate(`${outDir}/bridge-internal-windows.js`, generateExternal("GauntletInternalWindows", internalWindowsExports)) diff --git a/js/core/rollup.config.ts b/js/core/rollup.config.ts index 9680de6..760a198 100644 --- a/js/core/rollup.config.ts +++ b/js/core/rollup.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ 'src/internal-all.ts', 'src/internal-linux.ts', 'src/internal-macos.ts', + 'src/internal-windows.ts', ], output: [ { diff --git a/js/core/src/internal-all.ts b/js/core/src/internal-all.ts index c55c90a..fac9402 100644 --- a/js/core/src/internal-all.ts +++ b/js/core/src/internal-all.ts @@ -3,6 +3,4 @@ export { open_settings, current_os, wayland, - application_x11_pending_event, - application_wayland_pending_event, } from "ext:core/ops"; diff --git a/js/core/src/internal-linux.ts b/js/core/src/internal-linux.ts index ef6cda3..503605f 100644 --- a/js/core/src/internal-linux.ts +++ b/js/core/src/internal-linux.ts @@ -4,4 +4,6 @@ export { linux_open_application, linux_x11_focus_window, linux_wayland_focus_window, + application_x11_pending_event, + application_wayland_pending_event, } from "ext:core/ops"; diff --git a/js/core/src/internal-windows.ts b/js/core/src/internal-windows.ts new file mode 100644 index 0000000..77f40d5 --- /dev/null +++ b/js/core/src/internal-windows.ts @@ -0,0 +1,5 @@ +export { + windows_app_from_path, + windows_application_dirs, + windows_open_application +} from "ext:core/ops"; diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index f38c96d..2f5b410 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -27,6 +27,12 @@ type MacOSDesktopApplicationData = { icon: ArrayBuffer | undefined, } +type WindowsDesktopApplicationData = { + name: string + path: string, + icon: ArrayBuffer | undefined, +} + type MacOSDesktopSettingsPre13Data = { name: string path: string, @@ -145,8 +151,6 @@ declare module "gauntlet:bridge/internal-all" { function run_numbat(input: string): { left: string, right: string } function current_os(): string function wayland(): boolean - function application_x11_pending_event(): Promise - function application_wayland_pending_event(): Promise } declare module "gauntlet:bridge/internal-linux" { @@ -155,7 +159,8 @@ declare module "gauntlet:bridge/internal-linux" { function linux_wayland_focus_window(window_id: string): void function linux_application_dirs(): string[] function linux_app_from_path(path: string): Promise> - + function application_x11_pending_event(): Promise + function application_wayland_pending_event(): Promise } declare module "gauntlet:bridge/internal-macos" { @@ -172,6 +177,12 @@ declare module "gauntlet:bridge/internal-macos" { function macos_open_application(app_path: String): void } +declare module "gauntlet:bridge/internal-windows" { + function windows_application_dirs(): string[] + function windows_open_application(path: string): void + function windows_app_from_path(path: string): Promise> +} + declare module "ext:core/ops" { function open_settings(): void function run_numbat(input: string): { left: string, right: string } @@ -199,6 +210,10 @@ declare module "ext:core/ops" { function macos_app_from_arbitrary_path(path: string): Promise> function macos_open_application(app_path: String): void + function windows_application_dirs(): string[] + function windows_open_application(path: string): void + function windows_app_from_path(path: string): Promise> + function op_log_trace(target: string, message: string): void; function op_log_debug(target: string, message: string): void; function op_log_info(target: string, message: string): void; diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml index 834f0c0..44aae6b 100644 --- a/rust/plugin_runtime/Cargo.toml +++ b/rust/plugin_runtime/Cargo.toml @@ -33,6 +33,7 @@ resvg = { version = "0.44.0", default-features = false} numbat = "1.14.0" which = "7.0.1" uuid = "1.11.0" +open = "5" [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] libc = "0.2" @@ -55,5 +56,9 @@ objc2-app-kit = { version = "0.2.2", features = ["NSWorkspace", "NSImage", "NSIm objc2-foundation = { version = "0.2.2", features = ["NSString"] } objc2 = "0.5.2" +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.58.0", features = ["Win32_Storage_FileSystem", "Win32_UI_WindowsAndMessaging", "Win32_UI_Shell", "Win32_UI_Controls"] } +windows-icons = "0.1.0" + [features] scenario_runner = [] diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index 923eed5..73503d1 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -57,7 +57,7 @@ impl CustomModuleLoader { } } -const MODULES: [(&str, &str); 10] = [ +const MODULES: [(&str, &str); 11] = [ ("gauntlet:init", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/core/dist/init.js"))), ("gauntlet:bridge/components", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-components.js"))), ("gauntlet:bridge/hooks", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-hooks.js"))), @@ -68,6 +68,7 @@ const MODULES: [(&str, &str); 10] = [ ("gauntlet:bridge/internal-all", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-all.js"))), ("gauntlet:bridge/internal-linux", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-linux.js"))), ("gauntlet:bridge/internal-macos", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-macos.js"))), + ("gauntlet:bridge/internal-windows", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-windows.js"))), ]; impl ModuleLoader for CustomModuleLoader { @@ -97,6 +98,7 @@ impl ModuleLoader for CustomModuleLoader { ("gauntlet:bridge/internal-all", _) => "gauntlet:bridge/internal-all", ("gauntlet:bridge/internal-linux", _) => "gauntlet:bridge/internal-linux", ("gauntlet:bridge/internal-macos", _) => "gauntlet:bridge/internal-macos", + ("gauntlet:bridge/internal-windows", _) => "gauntlet:bridge/internal-windows", ("react", _) => "gauntlet:bridge/react", ("react/jsx-runtime", _) => "gauntlet:bridge/react-jsx-runtime", ("@project-gauntlet/api/components", _) => "gauntlet:bridge/components", @@ -396,6 +398,9 @@ pub async fn start_js_runtime( #[cfg(target_os = "linux")] extensions.push(crate::plugins::applications::gauntlet_internal_linux::init_ops_and_esm()); + + #[cfg(target_os = "windows")] + extensions.push(crate::plugins::applications::gauntlet_internal_windows::init_ops_and_esm()); } let mut worker = MainWorker::bootstrap_from_options( diff --git a/rust/plugin_runtime/src/plugins/applications.rs b/rust/plugin_runtime/src/plugins/applications.rs index 3052152..9630d96 100644 --- a/rust/plugin_runtime/src/plugins/applications.rs +++ b/rust/plugin_runtime/src/plugins/applications.rs @@ -20,6 +20,13 @@ pub use linux::gauntlet_internal_linux; #[cfg(target_os = "macos")] mod macos; +#[cfg(target_os = "windows")] +mod windows; + +#[cfg(target_os = "windows")] +pub use windows::gauntlet_internal_windows; + + #[derive(Debug, Serialize)] #[serde(tag = "type")] pub enum DesktopPathAction { @@ -51,10 +58,12 @@ pub struct DesktopApplication { icon: Option>, } -#[cfg(all(not(target_os = "linux"), not(target_os = "macos")))] +#[cfg(target_os = "windows")] #[derive(Debug, Serialize)] pub struct DesktopApplication { - + name: String, + path: String, + icon: Option>, } #[cfg(target_os = "macos")] diff --git a/rust/plugin_runtime/src/plugins/applications/windows.rs b/rust/plugin_runtime/src/plugins/applications/windows.rs new file mode 100644 index 0000000..cde3645 --- /dev/null +++ b/rust/plugin_runtime/src/plugins/applications/windows.rs @@ -0,0 +1,208 @@ +use std::{mem, ptr}; +use std::io::Cursor; +use std::mem::MaybeUninit; +use crate::plugins::applications::{resize_icon, DesktopApplication, DesktopPathAction}; +use deno_core::op2; +use std::path::PathBuf; +use anyhow::{anyhow, Context}; +use image::RgbaImage; +use windows::core::{GUID, HSTRING, PWSTR}; +use windows::Win32::Foundation::{HANDLE, HWND}; +use windows::Win32::Graphics::Gdi; +use windows::Win32::Graphics::Gdi::HDC; +use windows::Win32::Storage::FileSystem; +use windows::Win32::UI::{Controls, Shell, WindowsAndMessaging}; +use windows::Win32::UI::Controls::HIMAGELIST; + +deno_core::extension!( + gauntlet_internal_windows, + ops = [ + windows_application_dirs, + windows_open_application, + windows_app_from_path, + ], + esm_entry_point = "ext:gauntlet/internal-windows/bootstrap.js", + esm = [ + "ext:gauntlet/internal-windows/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-windows-bootstrap.js", + "ext:gauntlet/internal-windows.js" = "../../js/core/dist/internal-windows.js", + ] +); + + +#[op2] +#[serde] +fn windows_application_dirs() -> Vec { + vec![ + known_folder(&Shell::FOLDERID_Desktop), + known_folder(&Shell::FOLDERID_PublicDesktop), + known_folder(&Shell::FOLDERID_StartMenu), + known_folder(&Shell::FOLDERID_CommonStartMenu), + ] + .into_iter() + .flatten() + .collect() +} + +#[op2] +#[serde] +fn windows_open_application(#[string] file_path: String) -> anyhow::Result<()> { + open::that_detached(file_path)?; + + Ok(()) +} + +#[op2(async)] +#[serde] +async fn windows_app_from_path(#[string] file_path: String) -> anyhow::Result> { + if PathBuf::from(&file_path).exists() { + let name = extract_name(&file_path)?; + let icon = extract_icon(&file_path) + .inspect_err(|err| tracing::error!("Unable to extract icon for {}: {:?}", file_path, err)) + .ok(); + + Ok(Some(DesktopPathAction::Add { + id: file_path.clone(), + data: DesktopApplication { + name, + path: file_path, + icon, + }, + })) + } else { + Ok(Some(DesktopPathAction::Remove { + id: file_path, + })) + } +} + +fn extract_icon(file_path: &str) -> anyhow::Result> { + unsafe { + let mut shfileinfow = Shell::SHFILEINFOW::default(); + + let hresult = Shell::SHGetFileInfoW( + &HSTRING::from(file_path), + FileSystem::FILE_ATTRIBUTE_NORMAL, + Some(&mut shfileinfow), + size_of::() as u32, + Shell::SHGFI_SYSICONINDEX, + ); + + if hresult == 0 || shfileinfow.iIcon == 0 { + return Err(anyhow!("SHGetFileInfoW failed: {}", hresult)); + } + + let image_list: Controls::IImageList = Shell::SHGetImageList(Shell::SHIL_JUMBO as i32)?; + let icon = image_list.GetIcon(shfileinfow.iIcon, Controls::ILD_TRANSPARENT.0)?; + + let mut icon_info = WindowsAndMessaging::ICONINFO::default(); + WindowsAndMessaging::GetIconInfo(icon, &mut icon_info) + .context("Unable to GetIconInfo")?; + + let _ = Gdi::DeleteObject(icon_info.hbmMask); + + let mut bitmap = Gdi::BITMAP::default(); + let result = Gdi::GetObjectW( + icon_info.hbmColor, + size_of::() as i32, + Some(&mut bitmap as *mut _ as *mut _), + ); + if result != size_of::() as i32 { + Err(anyhow!("Error running GetObjectW: {}", result))?; + } + + let size = (bitmap.bmWidthBytes * bitmap.bmHeight) as usize; + let mut bits: Vec = vec![0; size]; + + let dc = Gdi::GetDC(HWND(ptr::null_mut())); + if dc == HDC(ptr::null_mut()) { + Err(anyhow!("Error running GetDC"))?; + } + + let mut bitmap_info = Gdi::BITMAPINFO { + bmiHeader: Gdi::BITMAPINFOHEADER { + biSize: size_of::() as u32, + biWidth: bitmap.bmWidth, + biHeight: -bitmap.bmHeight, + biPlanes: 1, + biBitCount: 32, + biCompression: Gdi::BI_RGB.0, + biSizeImage: 0, + biXPelsPerMeter: 0, + biYPelsPerMeter: 0, + biClrUsed: 0, + biClrImportant: 0, + }, + bmiColors: [Default::default()], + }; + + let result = Gdi::GetDIBits( + dc, + icon_info.hbmColor, + 0, + bitmap.bmHeight as u32, + Some(bits.as_mut_ptr() as *mut _), + &mut bitmap_info, + Gdi::DIB_RGB_COLORS, + ); + if result != bitmap.bmHeight { + Err(anyhow!("Error running GetDIBits: {}", result))?; + } + + let result = Gdi::ReleaseDC(HWND(ptr::null_mut()), dc); + if result != 1 { + Err(anyhow!("Error running ReleaseDC: {}", result))?; + } + + let _ = Gdi::DeleteObject(icon_info.hbmColor); + + let image_buffer = RgbaImage::from_fn(bitmap.bmWidth as u32, bitmap.bmHeight as u32, |x, y| { + let idx= y as usize * bitmap.bmWidth as usize + x as usize; + let [b, g, r, a] = bits[idx].to_le_bytes(); + [r, g, b, a].into() + }); + + WindowsAndMessaging::DestroyIcon(icon)?; + + let rgba_image = image::DynamicImage::ImageRgba8(image_buffer); + + let mut result = Cursor::new(vec![]); + + rgba_image.write_to(&mut result, image::ImageFormat::Png) + .expect("should be able to convert to png"); + + let data = result.into_inner(); + + let data = resize_icon(data)?; + + Ok(data) + } +} + + +fn extract_name(file_path: &str) -> anyhow::Result { + let mut file_info = Shell::SHFILEINFOW::default(); + + unsafe { + Shell::SHGetFileInfoW( + &HSTRING::from(file_path), + FileSystem::FILE_ATTRIBUTE_NORMAL, + Some(&mut file_info), + size_of::() as u32, + Shell::SHGFI_DISPLAYNAME, + ); + } + + let strlen = file_info + .szDisplayName + .iter() + .position(|x| *x == 0) + .unwrap(); + + Ok(String::from_utf16(&file_info.szDisplayName[..strlen])?) +} + +pub fn known_folder(folder_id: &GUID) -> anyhow::Result { + let result: PWSTR = unsafe { Shell::SHGetKnownFolderPath(folder_id, Shell::KF_FLAG_CREATE, HANDLE::default()) }?; + let result_str = unsafe { result.to_string() }?; + Ok(result_str) +} From 2e062705c756dbc21f144e1c794b27f4dbe8e0a7 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 1 Jan 2025 12:05:20 +0100 Subject: [PATCH 260/540] Remove not used dependency --- rust/plugin_runtime/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml index 44aae6b..9e208fc 100644 --- a/rust/plugin_runtime/Cargo.toml +++ b/rust/plugin_runtime/Cargo.toml @@ -58,7 +58,6 @@ objc2 = "0.5.2" [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.58.0", features = ["Win32_Storage_FileSystem", "Win32_UI_WindowsAndMessaging", "Win32_UI_Shell", "Win32_UI_Controls"] } -windows-icons = "0.1.0" [features] scenario_runner = [] From 34a5c5e1683af4fd2494a7c81534a28e2393c2e4 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 4 Jan 2025 22:00:39 +0100 Subject: [PATCH 261/540] MacOS light and dark themes. Rework simplified theme --- Cargo.lock | 33 +- rust/client/src/ui/mod.rs | 21 +- rust/client/src/ui/theme/container.rs | 12 +- rust/client/src/ui/theme/date_picker.rs | 2 +- rust/client/src/ui/theme/mod.rs | 482 +++++++++++++++--------- 5 files changed, 344 insertions(+), 206 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e001ac..d09c730 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4154,7 +4154,6 @@ dependencies = [ "wayland-protocols-wlr 0.3.5", "which 7.0.1", "windows", - "windows-icons", "x11rb", ] @@ -5172,7 +5171,7 @@ dependencies = [ [[package]] name = "iced" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" dependencies = [ "iced_core", "iced_futures", @@ -5200,7 +5199,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" dependencies = [ "bitflags 2.6.0", "bytes", @@ -5227,7 +5226,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" dependencies = [ "futures", "iced_core", @@ -5241,7 +5240,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5293,7 +5292,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -5305,7 +5304,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" dependencies = [ "bytes", "iced_core", @@ -5326,7 +5325,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" dependencies = [ "bytemuck", "cosmic-text", @@ -5341,7 +5340,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5360,7 +5359,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" dependencies = [ "iced_renderer", "iced_runtime", @@ -5375,7 +5374,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#cfb83792200398e88f47a2cfacb3477f2bd41a4a" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" dependencies = [ "iced_futures", "iced_graphics", @@ -12297,18 +12296,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-icons" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd18e61837c404baa46146d3ea12241073ee7b4457d5ad73751d0e8f49dad9a" -dependencies = [ - "base64 0.22.1", - "image 0.25.5", - "winapi", - "windows", -] - [[package]] name = "windows-implement" version = "0.58.0" diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 7dba7c1..af89248 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -233,6 +233,7 @@ impl TryInto for AppMsg const WINDOW_WIDTH: f32 = 750.0; const WINDOW_HEIGHT: f32 = 450.0; +#[cfg(not(target_os = "macos"))] fn window_settings() -> window::Settings { window::Settings { size: Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), @@ -240,9 +241,27 @@ fn window_settings() -> window::Settings { resizable: false, decorations: false, transparent: true, - #[cfg(target_os = "macos")] + closeable: false, + minimizable: false, + ..Default::default() + } +} + +#[cfg(target_os = "macos")] +fn window_settings() -> window::Settings { + window::Settings { + size: Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), + position: Position::Centered, + resizable: false, + decorations: true, + transparent: false, + closeable: false, + minimizable: false, platform_specific: window::settings::PlatformSpecific { window_kind: window::settings::WindowKind::Popup, + fullsize_content_view: true, + title_hidden: true, + titlebar_transparent: true, ..Default::default() }, ..Default::default() diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 3bc487f..2a8bcf4 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -1,4 +1,4 @@ -use iced::{Border, Color, Length, Renderer}; +use iced::{Border, Color, Length, Renderer, Shadow, Vector}; use iced::widget::{Container, container}; use iced::widget::container::Style; use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; @@ -88,7 +88,7 @@ impl container::Catalog for GauntletComplexTheme { match class { ContainerStyleInner::Transparent => Default::default(), ContainerStyleInner::ActionPanel => { - let root_theme = &self.root; + let root_theme = &self.popup; let panel_theme = &self.action_panel; let background_color = &panel_theme.background_color; @@ -100,7 +100,11 @@ impl container::Catalog for GauntletComplexTheme { width: root_theme.border_width, color: root_theme.border_color.to_iced(), }, - shadow: Default::default(), + shadow: Shadow { + color: Color::from_rgba8(0, 0, 0, 0.50), + offset: Vector::new(0.0, 5.0), + blur_radius: 25.0, + }, } } ContainerStyleInner::ActionShortcutModifier => { @@ -167,7 +171,7 @@ impl container::Catalog for GauntletComplexTheme { } } ContainerStyleInner::Tooltip => { - let theme = &self.root; + let theme = &self.popup; let tooltip_theme = &self.tooltip; let background_color = &tooltip_theme.background_color; diff --git a/rust/client/src/ui/theme/date_picker.rs b/rust/client/src/ui/theme/date_picker.rs index 398cd88..66903bf 100644 --- a/rust/client/src/ui/theme/date_picker.rs +++ b/rust/client/src/ui/theme/date_picker.rs @@ -32,7 +32,7 @@ impl Catalog for GauntletComplexTheme { fn active(theme: &GauntletComplexTheme) -> Style { - let root_theme = &theme.root; + let root_theme = &theme.popup; let theme = &theme.form_input_date_picker; Style { diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 46d7a1c..0df214b 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -24,26 +24,50 @@ mod loading_bar; pub type Element<'a, Message> = iced::Element<'a, Message, GauntletComplexTheme>; -const CURRENT_SIMPLE_THEME_VERSION: u64 = 4; -const CURRENT_COMPLEX_THEME_VERSION: u64 = 4; +const CURRENT_SIMPLE_THEME_VERSION: u64 = 5; +const CURRENT_COMPLEX_THEME_VERSION: u64 = 5; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum GauntletSimpleThemeMode { + #[serde(rename = "light")] + Light, + #[serde(rename = "dark")] + Dark +} + +pub type GauntletSimpleThemeColorPalette = [ThemeColor; 4]; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GauntletSimpleThemeWindow { + border: GauntletSimpleThemeWindowBorder, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GauntletSimpleThemeWindowBorder { + radius: f32, + width: f32, + color: ThemeColor, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GauntletSimpleThemeContent { + border: GauntletSimpleThemeContentBorder, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GauntletSimpleThemeContentBorder { + radius: f32, +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GauntletSimpleTheme { version: u64, - background_darkest_color: ThemeColor, - background_darker_color: ThemeColor, - background_lighter_color: ThemeColor, - background_lightest_color: ThemeColor, - text_lightest_color: ThemeColor, - text_lighter_color: ThemeColor, - text_darker_color: ThemeColor, - text_darkest_color: ThemeColor, - primary_darker_color: ThemeColor, - primary_lighter_color: ThemeColor, - root_border_radius: f32, - root_border_width: f32, - root_border_color: ThemeColor, - content_border_radius: f32, + mode: GauntletSimpleThemeMode, + // value of tint/tones/shades/whatever you have, from lower to higher + background: GauntletSimpleThemeColorPalette, + text: GauntletSimpleThemeColorPalette, + window: GauntletSimpleThemeWindow, + content: GauntletSimpleThemeContent, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -51,6 +75,7 @@ pub struct GauntletComplexTheme { version: u64, text: ThemeColor, root: ThemeRoot, + popup: ThemeRoot, action: ThemeButton, action_panel: ThemePaddingBackgroundColor, action_panel_title: ThemePaddingOnly, @@ -191,7 +216,6 @@ fn parse_json_theme(theme_file: PathBuf, theme_ } } -// TODO add border on focus, lighter background on hover // TODO padding on button is padding, not margin, a lot of margins missing? impl GauntletComplexTheme { @@ -211,57 +235,142 @@ impl GauntletComplexTheme { theme } + // TODO legacy pub fn default_simple_theme() -> GauntletSimpleTheme { GauntletSimpleTheme { version: CURRENT_SIMPLE_THEME_VERSION, - background_lightest_color: BACKGROUND_LIGHTEST, - background_lighter_color: BACKGROUND_LIGHTER, - background_darker_color: BACKGROUND_DARKER, - background_darkest_color: BACKGROUND_DARKEST, - text_lightest_color: TEXT_LIGHTEST, - text_lighter_color: TEXT_LIGHTER, - text_darker_color: TEXT_DARKER, - text_darkest_color: TEXT_DARKEST, - primary_darker_color: PRIMARY, - primary_lighter_color: PRIMARY_HOVERED, - root_border_radius: 10.0, - root_border_width: 1.0, - root_border_color: BACKGROUND_LIGHTER, - content_border_radius: BUTTON_BORDER_RADIUS, + mode: GauntletSimpleThemeMode::Dark, + background: [ + ThemeColor::new(0x626974, 0.3), + ThemeColor::new(0x48505B, 0.5), + ThemeColor::new(0x333a42, 1.0), + ThemeColor::new(0x2C323A, 1.0) + ], + text: [ + ThemeColor::new(0xDDDFE1, 1.0), + ThemeColor::new(0x9AA0A6, 1.0), + ThemeColor::new(0x6B7785, 1.0), + ThemeColor::new(0x1D242C, 1.0), + ], + window: GauntletSimpleThemeWindow { + border: GauntletSimpleThemeWindowBorder { + radius: 10.0, + width: 1.0, + color: ThemeColor::new(0x48505B, 0.5) + }, + }, + content: GauntletSimpleThemeContent { + border: GauntletSimpleThemeContentBorder { + radius: 4.0, + }, + }, } } + // TODO auto detect macos dark mode + // pub fn default_simple_theme() -> GauntletSimpleTheme { + // GauntletSimpleTheme { + // version: CURRENT_SIMPLE_THEME_VERSION, + // mode: GauntletSimpleThemeMode::Dark, + // background: [ + // ThemeColor::from_rgba8(100, 100, 100, 0.5), + // ThemeColor::from_rgba8(55, 55, 55, 1.0), + // ThemeColor::from_rgba8(45, 45, 45, 1.0), + // ThemeColor::from_rgba8(36, 36, 36, 1.0), + // ], + // text: [ + // ThemeColor::from_rgba8(229,229,229, 1.0), + // ThemeColor::from_rgba8(200, 200, 200, 1.0), + // ThemeColor::from_rgba8(150, 150, 150, 1.0), + // ThemeColor::from_rgba8(50, 50, 50, 1.0), + // ], + // window: GauntletSimpleThemeWindow { + // border: GauntletSimpleThemeWindowBorder { + // radius: 8.0, + // width: 1.0, + // color: ThemeColor::from_rgba8(100, 100, 100, 1.0), + // }, + // }, + // content: GauntletSimpleThemeContent { + // border: GauntletSimpleThemeContentBorder { + // radius: 4.0, + // }, + // }, + // } + // } + + // TODO auto detect macos light mode + // pub fn default_simple_theme() -> GauntletSimpleTheme { + // GauntletSimpleTheme { + // version: CURRENT_SIMPLE_THEME_VERSION, + // mode: GauntletSimpleThemeMode::Light, + // background: [ + // ThemeColor::from_rgba8(0, 0, 0, 0.2), + // ThemeColor::from_rgba8(200, 200, 200, 1.0), + // ThemeColor::from_rgba8(210, 210, 210, 1.0), + // ThemeColor::from_rgba8(234, 234, 234, 1.0), + // ], + // text: [ + // ThemeColor::from_rgba8(0, 0, 0, 1.0), + // ThemeColor::from_rgba8(0, 0, 0, 0.847), + // ThemeColor::from_rgba8(0, 0, 0, 0.498), + // ThemeColor::from_rgba8(0, 0, 0, 0.259), + // ], + // window: GauntletSimpleThemeWindow { + // border: GauntletSimpleThemeWindowBorder { + // radius: 8.0, + // width: 1.0, + // color: ThemeColor::from_rgba8(185, 185, 185, 1.0), + // }, + // }, + // content: GauntletSimpleThemeContent { + // border: GauntletSimpleThemeContentBorder { + // radius: 4.0, + // }, + // }, + // } + // } + pub fn default_theme(simple_theme: GauntletSimpleTheme) -> GauntletComplexTheme { let GauntletSimpleTheme { version: _, - background_darkest_color, - background_darker_color, - background_lighter_color, - background_lightest_color, - text_lightest_color, - text_lighter_color, - text_darker_color, - text_darkest_color, - primary_darker_color, - primary_lighter_color, - root_border_width, - root_border_radius, - root_border_color, - content_border_radius, + mode, + background, + text, + window, + content } = simple_theme; + let [background_100, background_200, background_300, background_400] = background; + let [text_100, text_200, text_300, _text_400] = text; + GauntletComplexTheme { version: CURRENT_COMPLEX_THEME_VERSION, - text: text_lightest_color, + text: text_100, root: ThemeRoot { - background_color: background_darkest_color, - border_radius: root_border_radius, - border_width: root_border_width, - border_color: root_border_color, + background_color: background_400, + #[cfg(not(target_os = "macos"))] + border_radius: window.border.radius, + #[cfg(not(target_os = "macos"))] + border_width: window.border.width, + #[cfg(not(target_os = "macos"))] + border_color: window.border.color, + #[cfg(target_os = "macos")] + border_radius: 0.0, + #[cfg(target_os = "macos")] + border_width: 0.0, + #[cfg(target_os = "macos")] + border_color: TRANSPARENT, + }, + popup: ThemeRoot { + background_color: background_400, + border_radius: window.border.radius, + border_width: window.border.width, + border_color: window.border.color, }, action_panel: ThemePaddingBackgroundColor { padding: padding_all(8.0), - background_color: background_darker_color, + background_color: background_400, }, action_panel_title: ThemePaddingOnly { padding: padding(2.0, 8.0, 4.0, 8.0), @@ -269,11 +378,11 @@ impl GauntletComplexTheme { action: ThemeButton { padding: padding_all(8.0), background_color: TRANSPARENT, - background_color_focused: background_lightest_color, - background_color_hovered: background_lighter_color, - text_color: text_lightest_color, - text_color_hovered: text_lightest_color, - border_radius: content_border_radius, + background_color_focused: background_100, + background_color_hovered: background_300, + text_color: text_100, + text_color_hovered: text_100, + border_radius: content.border.radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -283,8 +392,8 @@ impl GauntletComplexTheme { action_shortcut_modifier: ThemeActionShortcutModifier { padding: padding_axis(0.0, 8.0), spacing: 8.0, - background_color: background_lightest_color, - border_radius: content_border_radius, + background_color: background_100, + border_radius: content.border.radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -296,18 +405,27 @@ impl GauntletComplexTheme { }, metadata_tag_item_button: ThemeButton { padding: padding_axis(2.0, 8.0), - background_color: primary_darker_color, - background_color_focused: primary_lighter_color, - background_color_hovered: primary_lighter_color, - text_color: text_darkest_color, - text_color_hovered: text_darkest_color, - border_radius: content_border_radius, + background_color: match mode { + GauntletSimpleThemeMode::Light => background_300, + GauntletSimpleThemeMode::Dark => background_200 + }, + background_color_focused: match mode { + GauntletSimpleThemeMode::Light => background_200, + GauntletSimpleThemeMode::Dark => background_100 + }, + background_color_hovered: match mode { + GauntletSimpleThemeMode::Light => background_200, + GauntletSimpleThemeMode::Dark => background_100 + }, + text_color: text_100, + text_color_hovered: text_100, + border_radius: content.border.radius, border_width: 0.0, border_color: TRANSPARENT, }, metadata_item_label: ThemePaddingTextColorSize { padding: padding_all(0.0), - text_color: text_darker_color, + text_color: text_300, text_size: 14.0, }, metadata_item_value: ThemePaddingOnly { @@ -318,7 +436,7 @@ impl GauntletComplexTheme { }, list_item_subtitle: ThemePaddingTextColor { padding: padding_all(4.0), - text_color: text_lighter_color, + text_color: text_200, }, list_item_title: ThemePaddingOnly { padding: padding_all(4.0), @@ -331,22 +449,22 @@ impl GauntletComplexTheme { }, content_image: ThemeImage { padding: padding_all(0.0), - border_radius: content_border_radius, + border_radius: content.border.radius, }, inline: ThemePaddingOnly { padding: padding_axis(0.0, 8.0), }, inline_name: ThemePaddingTextColor { padding: padding_all(8.0), - text_color: text_lighter_color, + text_color: text_200, }, inline_separator: ThemeTextColor { - text_color: text_lighter_color, + text_color: text_200, }, inline_inner: ThemeInline { padding: padding_all(8.0), - background_color: background_lighter_color, - border_radius: content_border_radius, + background_color: background_200, + border_radius: content.border.radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -359,29 +477,29 @@ impl GauntletComplexTheme { }, grid_item: ThemeButton { padding: padding_all(8.0), - background_color: background_lighter_color, - background_color_focused: background_darker_color, - background_color_hovered: background_lightest_color, - text_color: text_lightest_color, - text_color_hovered: text_lightest_color, - border_radius: content_border_radius, + background_color: background_200, + background_color_focused: background_300, + background_color_hovered: background_100, + text_color: text_100, + text_color_hovered: text_100, + border_radius: content.border.radius, border_width: 0.0, border_color: TRANSPARENT, }, grid_item_title: ThemePaddingTextColor { padding: padding_axis(4.0, 0.0), - text_color: text_lightest_color, + text_color: text_100, }, grid_item_subtitle: ThemeTextColor { - text_color: text_lighter_color, + text_color: text_200, }, content_horizontal_break: ThemePaddingOnly { padding: padding_axis(8.0, 0.0), }, content_code_block_text: ThemeCode { padding: padding_axis(4.0, 8.0), - background_color: background_lighter_color, - border_radius: content_border_radius, + background_color: background_200, + border_radius: content.border.radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -394,47 +512,56 @@ impl GauntletComplexTheme { }, root_top_panel_button: ThemeButton { padding: padding_axis(3.0, 5.0), - background_color: background_lighter_color, - background_color_focused: background_lightest_color, - background_color_hovered: background_lightest_color, - text_color: text_lightest_color, - text_color_hovered: text_lightest_color, - border_radius: content_border_radius, + background_color: match mode { + GauntletSimpleThemeMode::Light => background_300, + GauntletSimpleThemeMode::Dark => background_200 + }, + background_color_focused: match mode { + GauntletSimpleThemeMode::Light => background_200, + GauntletSimpleThemeMode::Dark => background_100 + }, + background_color_hovered: match mode { + GauntletSimpleThemeMode::Light => background_200, + GauntletSimpleThemeMode::Dark => background_100 + }, + text_color: text_100, + text_color_hovered: text_100, + border_radius: content.border.radius, border_width: 0.0, border_color: TRANSPARENT, }, root_bottom_panel: ThemeBottomPanel { padding: padding_axis(6.0, 8.0), - background_color: background_darker_color, + background_color: background_300, spacing: 8.0 }, root_bottom_panel_action_toggle_button: ThemeButton { padding: padding_axis(3.0, 5.0), background_color: TRANSPARENT, - background_color_focused: background_lighter_color, - background_color_hovered: background_lighter_color, - text_color: text_lightest_color, - text_color_hovered: text_lightest_color, - border_radius: content_border_radius, + background_color_focused: background_200, + background_color_hovered: background_200, + text_color: text_100, + text_color_hovered: text_100, + border_radius: content.border.radius, border_width: 0.0, border_color: TRANSPARENT, }, root_bottom_panel_action_toggle_text: ThemePaddingTextColor { padding: padding(0.0, 8.0, 0.0, 4.0), - text_color: text_lighter_color + text_color: text_200 }, root_bottom_panel_primary_action_text: ThemePaddingTextColor { padding: padding(0.0, 8.0, 0.0, 4.0), - text_color: text_lightest_color + text_color: text_100 }, list_item: ThemeButton { padding: padding_all(5.0), background_color: TRANSPARENT, - background_color_focused: background_lighter_color, - background_color_hovered: background_darker_color, - text_color: text_lightest_color, - text_color_hovered: text_lightest_color, - border_radius: content_border_radius, + background_color_focused: background_200, + background_color_hovered: background_300, + text_color: text_100, + text_color_hovered: text_100, + border_radius: content.border.radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -477,28 +604,34 @@ impl GauntletComplexTheme { }, list_section_title: ThemePaddingTextColorSpacing { padding: padding(12.0, 8.0, 4.0, 8.0), - text_color: text_lighter_color, + text_color: text_200, spacing: 8.0, }, list_section_subtitle: ThemeTextColor { - text_color: text_darker_color + text_color: text_300 }, grid_section_title: ThemePaddingTextColorSpacing { padding: padding(12.0, 0.0, 4.0, 0.0), - text_color: text_lighter_color, + text_color: text_200, spacing: 8.0, }, grid_section_subtitle: ThemeTextColor { - text_color: text_darker_color + text_color: text_300 }, main_list_item: ThemeButton { padding: padding_all(5.0), background_color: TRANSPARENT, - background_color_focused: background_lighter_color, - background_color_hovered: background_darker_color, - text_color: text_lightest_color, - text_color_hovered: text_lightest_color, - border_radius: content_border_radius, + background_color_focused: match mode { + GauntletSimpleThemeMode::Light => background_300, + GauntletSimpleThemeMode::Dark => background_200 + }, + background_color_hovered: match mode { + GauntletSimpleThemeMode::Light => background_200, + GauntletSimpleThemeMode::Dark => background_300 + }, + text_color: text_100, + text_color_hovered: text_100, + border_radius: content.border.radius, border_width: 0.0, border_color: TRANSPARENT, }, @@ -507,7 +640,7 @@ impl GauntletComplexTheme { }, main_list_item_sub_text: ThemePaddingTextColor { padding: padding_axis(4.0, 12.0), - text_color: text_darker_color, + text_color: text_300, }, main_list_item_icon: ThemePaddingOnly { padding: padding(0.0, 7.0, 0.0, 5.0), @@ -531,97 +664,97 @@ impl GauntletComplexTheme { padding: padding_all(12.0), }, metadata_link: ThemeLink { - text_color: text_lightest_color, - text_color_hovered: text_lighter_color, + text_color: text_100, + text_color_hovered: text_200, }, empty_view_subtitle: ThemeTextColor { - text_color: text_darker_color, + text_color: text_300, }, form_input_date_picker: ThemeDatePicker { - background_color: background_darkest_color, - text_color: text_lightest_color, - text_color_selected: text_darker_color, - text_color_hovered: text_darker_color, - text_attenuated_color: text_darker_color, - day_background_color: background_lighter_color, - day_background_color_selected: background_lighter_color, - day_background_color_hovered: background_lighter_color, + background_color: background_400, + text_color: text_100, + text_color_selected: text_300, + text_color_hovered: text_300, + text_attenuated_color: text_300, + day_background_color: background_200, + day_background_color_selected: background_200, + day_background_color_hovered: background_200, }, form_input_date_picker_buttons: ThemeButton { padding: padding_all(8.0), - background_color: primary_darker_color, - background_color_focused: primary_lighter_color, - background_color_hovered: primary_lighter_color, - text_color: text_darkest_color, - text_color_hovered: text_darkest_color, - border_radius: content_border_radius, + background_color: background_200, + background_color_focused: background_100, + background_color_hovered: background_100, + text_color: text_100, + text_color_hovered: text_100, + border_radius: content.border.radius, border_width: 0.0, border_color: TRANSPARENT, }, form_input_checkbox: ThemeCheckbox { - background_color_checked: primary_darker_color, - background_color_unchecked: background_darkest_color, - background_color_checked_hovered: primary_lighter_color, - background_color_unchecked_hovered: background_lighter_color, - border_radius: content_border_radius, - border_width: root_border_width, - border_color: primary_darker_color, - icon_color: background_darkest_color, + background_color_checked: text_200, + background_color_unchecked: TRANSPARENT, + background_color_checked_hovered: text_100, + background_color_unchecked_hovered: background_200, + border_radius: content.border.radius, + border_width: window.border.width, + border_color: background_200, + icon_color: background_400, }, form_input_select: ThemeSelect { - background_color: primary_darker_color, - background_color_hovered: primary_lighter_color, - text_color: text_darkest_color, - text_color_hovered: text_darkest_color, - border_radius: content_border_radius, - border_width: root_border_width, - border_color: background_lighter_color, + background_color: background_200, + background_color_hovered: background_100, + text_color: text_200, + text_color_hovered: text_100, + border_radius: content.border.radius, + border_width: window.border.width, + border_color: background_200, }, form_input_select_menu: ThemeSelectMenu { - background_color: background_darkest_color, - background_color_selected: background_lighter_color, - text_color: text_lightest_color, - text_color_selected: text_lightest_color, - border_radius: content_border_radius, - border_width: root_border_width, - border_color: background_lighter_color, + background_color: background_400, + background_color_selected: background_200, + text_color: text_100, + text_color_selected: text_100, + border_radius: content.border.radius, + border_width: window.border.width, + border_color: background_200, }, form_input_text_field: ThemeTextField { background_color: TRANSPARENT, - background_color_hovered: background_lighter_color, - text_color: text_lightest_color, - text_color_placeholder: text_darker_color, - selection_color: background_lighter_color, - border_radius: content_border_radius, - border_width: root_border_width, - border_color: background_lighter_color, - border_color_hovered: background_lighter_color, + background_color_hovered: background_200, + text_color: text_100, + text_color_placeholder: text_300, + selection_color: background_200, + border_radius: content.border.radius, + border_width: window.border.width, + border_color: background_200, + border_color_hovered: background_200, }, separator: ThemeSeparator { - color: background_lighter_color + color: background_200 }, scrollbar: ThemeScrollbar { - color: primary_darker_color, - border_radius: content_border_radius, + color: background_200, + border_radius: content.border.radius, border_width: 0.0, border_color: TRANSPARENT, }, tooltip: ThemeTooltip { padding: 8.0, - background_color: background_darker_color, + background_color: background_300, }, loading_bar: ThemeLoadingBar { - loading_bar_color: primary_darker_color, - background_color: background_lighter_color, + loading_bar_color: text_200, + background_color: background_200, }, text_accessory: ThemePaddingTextColorSpacing { padding: padding(4.0, 4.0, 4.0, 16.0), - text_color: text_lighter_color, + text_color: text_200, spacing: 8.0, }, icon_accessory: ThemeIconAccessory { padding: padding(4.0, 4.0, 4.0, 16.0), - icon_color: text_lighter_color, + icon_color: text_200, }, hud: ThemeRoot { background_color: ThemeColor::new(0x1E1E1E, 0.7), @@ -650,18 +783,6 @@ const NOT_INTENDED_TO_BE_USED: ThemeColor = ThemeColor::new(0xAF5BFF, 1.0); // keep colors more or less in sync with settings ui const TRANSPARENT: ThemeColor = ThemeColor::new(0x000000, 0.0); -const BACKGROUND_LIGHTEST: ThemeColor = ThemeColor::new(0x626974, 0.3); -const BACKGROUND_LIGHTER: ThemeColor = ThemeColor::new(0x48505B, 0.5); -const BACKGROUND_DARKER: ThemeColor = ThemeColor::new(0x333a42, 1.0); -const BACKGROUND_DARKEST: ThemeColor = ThemeColor::new(0x2C323A, 1.0); -const TEXT_LIGHTEST: ThemeColor = ThemeColor::new(0xDDDFE1, 1.0); -const TEXT_LIGHTER: ThemeColor = ThemeColor::new(0x9AA0A6, 1.0); -const TEXT_DARKER: ThemeColor = ThemeColor::new(0x6B7785, 1.0); -const TEXT_DARKEST: ThemeColor = ThemeColor::new(0x1D242C, 1.0); -const PRIMARY: ThemeColor = ThemeColor::new(0xC79F60, 1.0); -const PRIMARY_HOVERED: ThemeColor = ThemeColor::new(0xD7B37A, 1.0); - -const BUTTON_BORDER_RADIUS: f32 = 4.0; const fn padding(top: f32, right: f32, bottom: f32, left: f32) -> ThemePadding { ThemePadding::Each { @@ -985,6 +1106,13 @@ impl ThemeColor { Self { r, g, b, a } } + fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Self { + let color = Color::from_rgba8(r, g, b, a); + let [r, g, b, _] = color.into_rgba8(); + + Self { r, g, b, a } + } + pub fn to_iced(&self) -> Color { Color::from_rgba8(self.r, self.g, self.b, self.a) } From 532cd3edab58294b45b159d46b9d0149418ab407 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 5 Jan 2025 15:35:46 +0100 Subject: [PATCH 262/540] Use single window and hide/unhide it, instead of recreating window each time This is a workaround for https://github.com/iced-rs/iced/issues/2719 --- rust/client/src/ui/mod.rs | 143 ++++++++++++++------------------------ 1 file changed, 51 insertions(+), 92 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index af89248..14637dd 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -11,7 +11,7 @@ use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; use iced::widget::text::Shaping; use iced::widget::text_input::focus; use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, Space}; -use iced::window::{Level, Position, Screenshot}; +use iced::window::{Level, Mode, Position, Screenshot}; use iced::{event, executor, font, futures, keyboard, stream, window, Alignment, Event, Font, Length, Padding, Pixels, Renderer, Settings, Size, Subscription, Task}; use std::collections::HashMap; use std::fs; @@ -65,7 +65,7 @@ pub struct AppModel { global_hotkey_manager: Arc>, current_hotkey: Arc>>, frontend_receiver: Arc>>, - main_window_id: Option, + main_window_id: window::Id, focused: bool, wayland: bool, #[cfg(any(target_os = "macos", target_os = "windows"))] @@ -234,12 +234,13 @@ const WINDOW_WIDTH: f32 = 750.0; const WINDOW_HEIGHT: f32 = 450.0; #[cfg(not(target_os = "macos"))] -fn window_settings() -> window::Settings { +fn window_settings(visible: bool) -> window::Settings { window::Settings { size: Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), position: Position::Centered, resizable: false, decorations: false, + visible, transparent: true, closeable: false, minimizable: false, @@ -248,12 +249,13 @@ fn window_settings() -> window::Settings { } #[cfg(target_os = "macos")] -fn window_settings() -> window::Settings { +fn window_settings(visible: bool) -> window::Settings { window::Settings { size: Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), position: Position::Centered, resizable: false, decorations: true, + visible, transparent: false, closeable: false, minimizable: false, @@ -283,29 +285,18 @@ fn layer_shell_settings() -> iced_layershell::reexport::NewLayerShellSettings { } } -fn open_main_window_non_wayland() -> (window::Id, Task) { - let (main_window_id, open_task) = window::open(window_settings()); +fn open_main_window_non_wayland(minimized: bool) -> (window::Id, Task) { + let (main_window_id, open_task) = window::open(window_settings(!minimized)); - let mut tasks = vec![]; - - tasks.push( + (main_window_id, Task::batch([ open_task.map(|_| AppMsg::Noop), - ); - - tasks.push( window::gain_focus(main_window_id), - ); - - tasks.push( window::change_level(main_window_id, Level::AlwaysOnTop), - ); - - (main_window_id, Task::batch(tasks)) + ])) } #[cfg(target_os = "linux")] -fn open_main_window_wayland() -> (window::Id, Task) { - let id = window::Id::unique(); +fn open_main_window_wayland(id: window::Id) -> (window::Id, Task) { let settings = layer_shell_settings(); (id, Task::done(AppMsg::LayerShell(layer_shell::LayerShellAppMsg::NewLayerShell { id, settings }))) @@ -408,24 +399,24 @@ fn new( font::load(BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded), ]; - let main_window_id = if !minimized { - #[cfg(target_os = "linux")] - let (main_window_id, open_task) = if wayland { - open_main_window_wayland() + #[cfg(target_os = "linux")] + let (main_window_id, open_task) = if wayland { + let id = window::Id::unique(); + + if minimized { + (id, Task::none()) } else { - open_main_window_non_wayland() - }; - - #[cfg(not(target_os = "linux"))] - let (main_window_id, open_task) = open_main_window_non_wayland(); - - tasks.push(open_task); - - Some(main_window_id) + open_main_window_wayland(id) + } } else { - None + open_main_window_non_wayland(minimized) }; + #[cfg(not(target_os = "linux"))] + let (main_window_id, open_task) = open_main_window_non_wayland(minimized); + + tasks.push(open_task); + let global_state = if cfg!(feature = "scenario_runner") { let gen_in = std::env::var("GAUNTLET_SCREENSHOT_GEN_IN") .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_IN"); @@ -538,17 +529,10 @@ fn new( } fn title(state: &AppModel, window: window::Id) -> String { - match state.main_window_id { - Some(main_window_id) => { - if window == main_window_id { - "Gauntlet".to_owned() - } else { - "Gauntlet HUD".to_owned() - } - } - None => { - "Gauntlet HUD".to_owned() - } + if window == state.main_window_id { + "Gauntlet".to_owned() + } else { + "Gauntlet HUD".to_owned() } } @@ -789,11 +773,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } AppMsg::IcedEvent(window_id, Event::Keyboard(event)) => { - let Some(main_window_id) = state.main_window_id else { - return Task::none() - }; - - if window_id != main_window_id { + if window_id != state.main_window_id { return Task::none() } @@ -973,22 +953,14 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } AppMsg::IcedEvent(window_id, Event::Window(window::Event::Focused)) => { - let Some(main_window_id) = state.main_window_id else { - return Task::none() - }; - - if window_id != main_window_id { + if window_id != state.main_window_id { return Task::none() } state.on_focused() } AppMsg::IcedEvent(window_id, Event::Window(window::Event::Unfocused)) => { - let Some(main_window_id) = state.main_window_id else { - return Task::none() - }; - - if window_id != main_window_id { + if window_id != state.main_window_id { return Task::none() } @@ -1072,10 +1044,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { fs::create_dir_all(Path::new(&save_path).parent().expect("no parent?")) .expect("unable to create scenario out directories"); - let window_id = state.main_window_id - .expect("window id should be present when making screenshot"); - - window::screenshot(window_id) + window::screenshot(state.main_window_id) .map(move |screenshot| AppMsg::ScreenshotDone { save_path: save_path.clone(), screenshot, @@ -1387,17 +1356,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } fn view(state: &AppModel, window: window::Id) -> Element<'_, AppMsg> { - match state.main_window_id { - None => { - view_hud(state) - } - Some(main_window_id) => { - if window != main_window_id { - view_hud(state) - } else { - view_main(state) - } - } + if window != state.main_window_id { + view_hud(state) + } else { + view_main(state) } } @@ -1983,10 +1945,6 @@ impl AppModel { } fn hide_window(&mut self) -> Task { - let Some(main_window_id) = self.main_window_id.take() else { - return Task::none() - }; - self.focused = false; let mut commands = vec![]; @@ -1994,17 +1952,17 @@ impl AppModel { #[cfg(target_os = "linux")] if self.wayland { commands.push( - Task::done(AppMsg::LayerShell(layer_shell::LayerShellAppMsg::RemoveWindow(main_window_id))) + Task::done(AppMsg::LayerShell(layer_shell::LayerShellAppMsg::RemoveWindow(self.main_window_id))) ); } else { commands.push( - window::close(main_window_id) + window::change_mode(self.main_window_id, Mode::Hidden) ); }; #[cfg(not(target_os = "linux"))] commands.push( - window::close(main_window_id) + window::change_mode(self.main_window_id, Mode::Hidden) ); #[cfg(target_os = "macos")] @@ -2031,21 +1989,22 @@ impl AppModel { } fn show_window(&mut self) -> Task { - if let Some(_) = self.main_window_id { - return Task::none() - }; - #[cfg(target_os = "linux")] - let (main_window_id, open_task) = if self.wayland { - open_main_window_wayland() + let open_task = if self.wayland { + let (_, open_task) = open_main_window_wayland(self.main_window_id); + open_task } else { - open_main_window_non_wayland() + Task::batch([ + window::gain_focus(self.main_window_id), + window::change_mode(self.main_window_id, Mode::Windowed) + ]) }; #[cfg(not(target_os = "linux"))] - let (main_window_id, open_task) = open_main_window_non_wayland(); - - self.main_window_id = Some(main_window_id); + let open_task = Task::batch([ + window::gain_focus(self.main_window_id), + window::change_mode(self.main_window_id, Mode::Windowed) + ]); Task::batch([ open_task, From f9e4f6660ea593a8be7ba6f00c06be8044ce220c Mon Sep 17 00:00:00 2001 From: Tristan Schrader Date: Mon, 6 Jan 2025 01:04:44 -0800 Subject: [PATCH 263/540] fix: libxkbcommon needed during linux build on nix the new smithay-client-toolkit dependency requires access to these libraries during build, not just runtime as before --- nix/overlay.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/overlay.nix b/nix/overlay.nix index dbbbfff..24bea30 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -69,7 +69,7 @@ inherit pname src version RUSTY_V8_ARCHIVE; cargoExtraArgs = "--features release"; nativeBuildInputs = [cmake pkg-config protobuf]; - buildInputs = [openssl]; + buildInputs = [openssl] ++ optional isLinux libxkbcommon; # OPENSSL_CONFIG_DIR didn't work for vendored dependencies OPENSSL_NO_VENDOR = true; }; @@ -105,7 +105,7 @@ postFixup = if isLinux then '' - patchelf --add-rpath ${makeLibraryPath [libxkbcommon libGL xorg.libX11 wayland]} $out/bin/gauntlet + patchelf --add-rpath ${makeLibraryPath [libGL xorg.libX11 wayland]} $out/bin/gauntlet wrapProgram $out/bin/gauntlet --suffix PATH : ${makeBinPath [gtk3]} substituteInPlace $out/lib/systemd/user/gauntlet.service --replace /usr/bin/gauntlet $out/bin/gauntlet '' From f0a18f0e2287d77b61b71ab0422834959659299f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:52:25 +0100 Subject: [PATCH 264/540] Rework themes. Add theme setting. Remove complex themes. Remove sample generation commands. Use toml for theme config --- Cargo.lock | 2 + Cargo.toml | 3 +- bundled_themes/legacy.toml | 23 + bundled_themes/macos_dark.toml | 23 + bundled_themes/macos_light.toml | 23 + rust/cli/src/lib.rs | 6 +- rust/client/Cargo.toml | 1 + rust/client/src/lib.rs | 47 -- rust/client/src/ui/mod.rs | 106 ++-- rust/client/src/ui/theme/button.rs | 24 +- rust/client/src/ui/theme/checkbox.rs | 22 +- rust/client/src/ui/theme/container.rs | 42 +- rust/client/src/ui/theme/date_picker.rs | 18 +- rust/client/src/ui/theme/loading_bar.rs | 4 +- rust/client/src/ui/theme/mod.rs | 566 ++++++------------ rust/client/src/ui/theme/pick_list.rs | 22 +- rust/client/src/ui/theme/rule.rs | 6 +- rust/client/src/ui/theme/scrollable.rs | 6 +- rust/client/src/ui/theme/text.rs | 32 +- rust/client/src/ui/theme/text_input.rs | 74 +-- rust/common/src/dirs.rs | 16 +- rust/common/src/model.rs | 95 +++ rust/common/src/rpc/backend_api.rs | 65 +- rust/common/src/rpc/backend_server.rs | 52 +- rust/common/src/rpc/frontend_api.rs | 20 +- rust/management_client/src/ui.rs | 50 +- rust/management_client/src/views/general.rs | 106 +++- rust/management_client/src/views/plugins.rs | 16 +- rust/server/Cargo.toml | 3 +- .../db_migrations/12_settings_theme.sql | 1 + rust/server/src/lib.rs | 12 + rust/server/src/plugins/data_db_repository.rs | 66 +- rust/server/src/plugins/mod.rs | 70 +-- rust/server/src/plugins/settings.rs | 166 +++++ rust/server/src/plugins/theme.rs | 231 +++++++ rust/server/src/rpc.rs | 10 +- schema/backend.proto | 17 + 37 files changed, 1319 insertions(+), 727 deletions(-) create mode 100644 bundled_themes/legacy.toml create mode 100644 bundled_themes/macos_dark.toml create mode 100644 bundled_themes/macos_light.toml create mode 100644 rust/server/db_migrations/12_settings_theme.sql create mode 100644 rust/server/src/plugins/settings.rs create mode 100644 rust/server/src/plugins/theme.rs diff --git a/Cargo.lock b/Cargo.lock index d09c730..40f7f9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4029,6 +4029,7 @@ name = "gauntlet-client" version = "0.0.0" dependencies = [ "anyhow", + "arc-swap", "convert_case", "gauntlet-common", "gauntlet-common-ui", @@ -4176,6 +4177,7 @@ dependencies = [ "anyhow", "arboard", "bytes", + "dark-light", "futures", "gauntlet-client", "gauntlet-common", diff --git a/Cargo.toml b/Cargo.toml index 6083862..fbc19bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ edition = "2021" [workspace.dependencies] # iced #iced = { version = "0.13.99", features = ["tokio", "lazy", "advanced", "image"] } -iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", features = ["tokio", "lazy", "advanced", "image"] } +iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", default-features = false, features = ["webgl", "tokio", "lazy", "advanced", "image"] } #iced_aw = { version = "0.11.99", features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } iced_aw = { git = "https://github.com/project-gauntlet/iced_aw.git", branch = "gauntlet-0.13", default-features = false, features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } #iced_table = "0.13.99" @@ -66,6 +66,7 @@ bytes = { version = "1.6.0" } walkdir = { version = "2.4.0" } typed-path = { version = "0.10.0" } interprocess = { version = "2.2.2", features = ["tokio"] } +toml = "0.8" [dependencies] gauntlet-cli = { path = "rust/cli" } diff --git a/bundled_themes/legacy.toml b/bundled_themes/legacy.toml new file mode 100644 index 0000000..f698c9d --- /dev/null +++ b/bundled_themes/legacy.toml @@ -0,0 +1,23 @@ +mode = "dark" + +background = [ + { color = "#626974", alpha = 0.3 }, + { color = "#48505B", alpha = 0.5 }, + "#333A42", + "#2C323A" +] + +text = [ + "#DDDFE1", + "#9AA0A6", + "#6B7785", + "#1D242C" +] + +[window.border] +radius = 10 +width = 1 +color = { color = "#48505B", alpha = 0.5 } + +[content.border] +radius = 4.0 diff --git a/bundled_themes/macos_dark.toml b/bundled_themes/macos_dark.toml new file mode 100644 index 0000000..52389ae --- /dev/null +++ b/bundled_themes/macos_dark.toml @@ -0,0 +1,23 @@ +mode = "dark" + +background = [ + { color = "#646464", alpha = 0.5 }, + "#373737", + "#2D2D2D", + "#242424", +] + +text = [ + "#E5E5E5", + "#C8C8C8", + "#969696", + "#323232" +] + +[window.border] +radius = 8 +width = 1 +color = { color = "#646464", alpha = 0.5 } + +[content.border] +radius = 4.0 diff --git a/bundled_themes/macos_light.toml b/bundled_themes/macos_light.toml new file mode 100644 index 0000000..8dc72d3 --- /dev/null +++ b/bundled_themes/macos_light.toml @@ -0,0 +1,23 @@ +mode = "light" + +background = [ + { color = "#000000", alpha = 0.2 }, + "#C8C8C8", + "#D2D2D2", + "#EAEAEA", +] + +text = [ + { color = "#000000", alpha = 1.0 }, + { color = "#000000", alpha = 0.847 }, + { color = "#000000", alpha = 0.498 }, + { color = "#000000", alpha = 0.259 }, +] + +[window.border] +radius = 8 +width = 1 +color = "#B9B9B9" + +[content.border] +radius = 4.0 diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs index 11b00fd..7290531 100644 --- a/rust/cli/src/lib.rs +++ b/rust/cli/src/lib.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Context}; use clap::Parser; -use gauntlet_client::{generate_complex_theme_sample, generate_simple_theme_sample, open_window}; +use gauntlet_client::open_window; use gauntlet_management_client::start_management_client; use gauntlet_server::start; @@ -17,8 +17,6 @@ struct Cli { enum Commands { Open, Settings, - GenerateSampleComplexTheme, - GenerateSampleSimpleTheme, } pub fn init() { @@ -47,8 +45,6 @@ pub fn init() { match command { Commands::Open => open_window(), Commands::Settings => start_management_client(), - Commands::GenerateSampleComplexTheme => generate_complex_theme_sample().expect("Unable to generate complex theme sample"), - Commands::GenerateSampleSimpleTheme => generate_simple_theme_sample().expect("Unable to generate simple theme sample") }; } } diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index a20338c..53b072d 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -24,6 +24,7 @@ once_cell.workspace = true # other global-hotkey = "0.6.3" +arc-swap = "1.7.1" [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] tray-icon = { version = "0.19.2", default-features = false } diff --git a/rust/client/src/lib.rs b/rust/client/src/lib.rs index 601ab25..08a740e 100644 --- a/rust/client/src/lib.rs +++ b/rust/client/src/lib.rs @@ -60,50 +60,3 @@ pub fn open_settings_window() { }) } -pub fn generate_complex_theme_sample() -> anyhow::Result<()> { - let dirs = Dirs::new(); - - let sample_complex_theme_file = dirs.sample_complex_theme_file(); - let complex_theme_file = dirs.complex_theme_file(); - - let theme_complex = GauntletComplexTheme::default_theme(GauntletComplexTheme::default_simple_theme()); - - let string = serde_json::to_string_pretty(&theme_complex)?; - - let sample_theme_parent = sample_complex_theme_file - .parent() - .expect("no parent?"); - - std::fs::create_dir_all(sample_theme_parent)?; - - std::fs::write(&sample_complex_theme_file, string)?; - - println!("Created sample using default complex theme at {:?}", sample_complex_theme_file); - println!("Make changes and rename file to {:?}", complex_theme_file.file_name().unwrap()); - - Ok(()) -} - -pub fn generate_simple_theme_sample() -> anyhow::Result<()> { - let dirs = Dirs::new(); - - let sample_simple_theme_file = dirs.sample_simple_theme_color_file(); - let simple_theme_file = dirs.theme_simple_file(); - - let theme = GauntletComplexTheme::default_simple_theme(); - - let string = serde_json::to_string_pretty(&theme)?; - - let sample_theme_parent = sample_simple_theme_file - .parent() - .expect("no parent?"); - - std::fs::create_dir_all(sample_theme_parent)?; - - std::fs::write(&sample_simple_theme_file, string)?; - - println!("Created sample using default simple theme at {:?}", sample_simple_theme_file); - println!("Make changes and rename file to {:?}", simple_theme_file.file_name().unwrap()); - - Ok(()) -} \ No newline at end of file diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 14637dd..63cff6f 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -24,7 +24,7 @@ use serde::Deserialize; use tokio::sync::{Mutex as TokioMutex, RwLock as TokioRwLock}; use client_context::ClientContext; -use gauntlet_common::model::{BackendRequestData, BackendResponseData, EntrypointId, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointActionType, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; +use gauntlet_common::model::{BackendRequestData, BackendResponseData, EntrypointId, UiTheme, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointActionType, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiSetupData, UiWidgetId}; use gauntlet_common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFrontendApiError}; use gauntlet_common::scenario_convert::{ui_render_location_from_scenario}; use gauntlet_common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; @@ -70,6 +70,7 @@ pub struct AppModel { wayland: bool, #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: tray_icon::TrayIcon, + theme: GauntletComplexTheme, // ephemeral state prompt: String, @@ -217,6 +218,9 @@ pub enum AppMsg { ClearInlineView { plugin_id: PluginId, }, + SetTheme { + theme: UiTheme + }, } #[cfg(target_os = "linux")] @@ -308,8 +312,6 @@ pub fn run( frontend_receiver: RequestReceiver, backend_sender: RequestSender, ) { - let theme = GauntletComplexTheme::new(); - #[cfg(target_os = "linux")] let result = { let wayland = std::env::var("WAYLAND_DISPLAY") @@ -317,14 +319,14 @@ pub fn run( .is_ok(); if wayland { - run_wayland(minimized, frontend_receiver, backend_sender, theme) + run_wayland(minimized, frontend_receiver, backend_sender) } else { - run_non_wayland(minimized, frontend_receiver, backend_sender, theme) + run_non_wayland(minimized, frontend_receiver, backend_sender) } }; #[cfg(not(target_os = "linux"))] - let result = run_non_wayland(minimized, frontend_receiver, backend_sender, theme); + let result = run_non_wayland(minimized, frontend_receiver, backend_sender); result.expect("Unable to start application") } @@ -333,9 +335,7 @@ fn run_non_wayland( minimized: bool, frontend_receiver: RequestReceiver, backend_sender: RequestSender, - theme: GauntletComplexTheme, ) -> anyhow::Result<()> { - iced::daemon::(title, update, view) .settings(Settings { #[cfg(target_os = "macos")] @@ -346,10 +346,7 @@ fn run_non_wayland( ..Default::default() }) .subscription(subscription) - .theme({ - let theme = theme.clone(); - move |_, _| theme.clone() - }) + .theme(|state, _| state.theme.clone()) .run_with(move || new(frontend_receiver, backend_sender, false, minimized))?; Ok(()) @@ -360,7 +357,6 @@ fn run_wayland( minimized: bool, frontend_receiver: RequestReceiver, backend_sender: RequestSender, - theme: GauntletComplexTheme, ) -> anyhow::Result<()> { iced_layershell::build_pattern::daemon("Gauntlet", update, view, wayland_remove_id_info) .layer_settings(iced_layershell::settings::LayerShellSettings { @@ -371,10 +367,7 @@ fn run_wayland( ..Default::default() }) .subscription(subscription) - .theme({ - let theme = theme.clone(); - move |_| theme.clone() - }) + .theme(|state| state.theme.clone()) .run_with(move || new(frontend_receiver, backend_sender, true, minimized))?; Ok(()) @@ -390,11 +383,25 @@ fn new( wayland: bool, minimized: bool, ) -> (AppModel, Task) { - let backend_api = BackendForFrontendApi::new(backend_sender); + let mut backend_api = BackendForFrontendApi::new(backend_sender); + + let setup_data = futures::executor::block_on(backend_api.setup_data()) + .expect("Unable to setup frontend"); + + let theme = GauntletComplexTheme::new(setup_data.theme); + + GauntletComplexTheme::set_global(theme.clone()); + + let current_hotkey = Arc::new(StdMutex::new(None)); let global_hotkey_manager = GlobalHotKeyManager::new() .expect("unable to create global hot key manager"); + let assignment_result = assign_global_shortcut(&global_hotkey_manager, ¤t_hotkey, setup_data.global_shortcut); + + futures::executor::block_on(backend_api.setup_response(assignment_result.map_err(|err| format!("{:#}", err)).err())) + .expect("Unable to setup frontend"); + let mut tasks = vec![ font::load(BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded), ]; @@ -506,13 +513,14 @@ fn new( // logic backend_api, global_hotkey_manager: Arc::new(StdRwLock::new(global_hotkey_manager)), - current_hotkey: Arc::new(StdMutex::new(None)), + current_hotkey, frontend_receiver: Arc::new(TokioRwLock::new(frontend_receiver)), main_window_id, focused: false, wayland, #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: sys_tray::create_tray(), + theme, // ephemeral state prompt: "".to_string(), @@ -1272,23 +1280,11 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { .read() .expect("lock is poisoned"); - let mut hotkey_guard = state.current_hotkey - .lock() - .expect("lock is poisoned"); - - if let Some(current_hotkey) = *hotkey_guard { - global_hotkey_manager.unregister(current_hotkey)?; - } - - if let Some(shortcut) = shortcut { - let hotkey = convert_physical_shortcut_to_hotkey(shortcut); - - *hotkey_guard = Some(hotkey); - - global_hotkey_manager.register(hotkey)?; - } - - Ok(()) + assign_global_shortcut( + &global_hotkey_manager, + &state.current_hotkey, + shortcut + ) }; // responder is not clone and send, and we need to consume it @@ -1350,6 +1346,13 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { AppMsg::ClearInlineView { plugin_id } => { state.client_context.clear_inline_view(&plugin_id); + Task::none() + } + AppMsg::SetTheme { theme } => { + state.theme = GauntletComplexTheme::new(theme); + + GauntletComplexTheme::update_global(state.theme.clone()); + Task::none() } } @@ -1928,6 +1931,30 @@ fn subscription(state: &AppModel) -> Subscription { ]) } +fn assign_global_shortcut( + global_hotkey_manager: &GlobalHotKeyManager, + current_hotkey: &Arc>>, + shortcut: Option, +) -> anyhow::Result<()> { + + let mut hotkey_guard = current_hotkey + .lock() + .expect("lock is poisoned"); + + if let Some(current_hotkey) = *hotkey_guard { + global_hotkey_manager.unregister(current_hotkey)?; + } + + if let Some(shortcut) = shortcut { + let hotkey = convert_physical_shortcut_to_hotkey(shortcut); + + *hotkey_guard = Some(hotkey); + + global_hotkey_manager.register(hotkey)?; + } + + Ok(()) +} impl AppModel { fn on_focused(&mut self) -> Task { @@ -2316,6 +2343,13 @@ async fn request_loop( show } } + UiRequestData::SetTheme { theme } => { + responder.respond(UiResponseData::Nothing); + + AppMsg::SetTheme { + theme, + } + } } }; diff --git a/rust/client/src/ui/theme/button.rs b/rust/client/src/ui/theme/button.rs index 7b90afc..7bd776d 100644 --- a/rust/client/src/ui/theme/button.rs +++ b/rust/client/src/ui/theme/button.rs @@ -1,8 +1,8 @@ use button::Style; -use iced::{Border, Padding, Renderer}; +use iced::{Border, Color, Padding, Renderer}; use iced::widget::{button, Button}; use iced::widget::button::Status; -use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, NOT_INTENDED_TO_BE_USED, padding_all, ThemableWidget, TRANSPARENT}; +use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, NOT_INTENDED_TO_BE_USED, padding_all, ThemableWidget}; #[derive(Debug, Clone, Copy)] pub enum ButtonStyle { @@ -125,14 +125,14 @@ impl ButtonStyle { } ButtonStyle::MetadataLink => { let theme = &theme.metadata_link; - (None, None, None, &theme.text_color, &theme.text_color_hovered, &0.0, &1.0, &TRANSPARENT) + (None, None, None, &theme.text_color, &theme.text_color_hovered, &0.0, &1.0, &Color::TRANSPARENT) } ButtonStyle::MetadataTagItem => { let theme = &theme.metadata_tag_item_button; (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) } ButtonStyle::ShouldNotBeUsed => { - (Some(&NOT_INTENDED_TO_BE_USED), Some(&NOT_INTENDED_TO_BE_USED), Some(&NOT_INTENDED_TO_BE_USED), &NOT_INTENDED_TO_BE_USED, &NOT_INTENDED_TO_BE_USED, &0.0, &1.0, &TRANSPARENT) + (Some(&NOT_INTENDED_TO_BE_USED), Some(&NOT_INTENDED_TO_BE_USED), Some(&NOT_INTENDED_TO_BE_USED), &NOT_INTENDED_TO_BE_USED, &NOT_INTENDED_TO_BE_USED, &0.0, &1.0, &Color::TRANSPARENT) } ButtonStyle::DatePicker => { let theme = &theme.form_input_date_picker_buttons; @@ -141,10 +141,10 @@ impl ButtonStyle { }; let active = Style { - background: background_color.map(|color| color.to_iced().into()), - text_color: text_color.to_iced(), + background: background_color.map(|color| color.clone().into()), + text_color: text_color.clone(), border: Border { - color: border_color.to_iced(), + color: border_color.clone(), width: (*border_width).into(), radius: (*border_radius).into(), }, @@ -155,21 +155,21 @@ impl ButtonStyle { Status::Active => active, Status::Pressed => { Style { - background: background_color_pressed.map(|color| color.to_iced().into()), - text_color: text_color_hover.to_iced(), + background: background_color_pressed.map(|color| color.clone().into()), + text_color: text_color_hover.clone(), ..active } } Status::Hovered => { Style { - background: background_color_hover.map(|color| color.to_iced().into()), - text_color: text_color_hover.to_iced(), + background: background_color_hover.map(|color| color.clone().into()), + text_color: text_color_hover.clone(), ..active } } Status::Disabled => { Style { - background: Some(NOT_INTENDED_TO_BE_USED.to_iced().into()), + background: Some(NOT_INTENDED_TO_BE_USED.into()), ..active } } diff --git a/rust/client/src/ui/theme/checkbox.rs b/rust/client/src/ui/theme/checkbox.rs index 3b77b91..10d57a0 100644 --- a/rust/client/src/ui/theme/checkbox.rs +++ b/rust/client/src/ui/theme/checkbox.rs @@ -27,18 +27,18 @@ fn active(theme: &GauntletComplexTheme, is_checked: bool) -> Style { let theme = &theme.form_input_checkbox; let background = if is_checked { - theme.background_color_checked.to_iced().into() + theme.background_color_checked.into() } else { - theme.background_color_unchecked.to_iced().into() + theme.background_color_unchecked.into() }; Style { background, - icon_color: theme.icon_color.to_iced(), + icon_color: theme.icon_color, border: Border { radius: theme.border_radius.into(), width: theme.border_width, - color: theme.border_color.to_iced().into(), + color: theme.border_color.into(), }, text_color: None, } @@ -48,18 +48,18 @@ fn hovered(theme: &GauntletComplexTheme, is_checked: bool) -> Style { let theme = &theme.form_input_checkbox; let background = if is_checked { - theme.background_color_checked_hovered.to_iced().into() + theme.background_color_checked_hovered.into() } else { - theme.background_color_unchecked_hovered.to_iced().into() + theme.background_color_unchecked_hovered.into() }; Style { background, - icon_color: theme.icon_color.to_iced(), + icon_color: theme.icon_color, border: Border { radius: theme.border_radius.into(), width: theme.border_width, - color: theme.border_color.to_iced().into(), + color: theme.border_color.into(), }, text_color: None, } @@ -67,12 +67,12 @@ fn hovered(theme: &GauntletComplexTheme, is_checked: bool) -> Style { fn disabled(_is_checked: bool) -> Style { Style { - background: NOT_INTENDED_TO_BE_USED.to_iced().into(), - icon_color: NOT_INTENDED_TO_BE_USED.to_iced(), + background: NOT_INTENDED_TO_BE_USED.into(), + icon_color: NOT_INTENDED_TO_BE_USED, border: Border { radius: 2.0.into(), width: 1.0, - color: NOT_INTENDED_TO_BE_USED.to_iced(), + color: NOT_INTENDED_TO_BE_USED, }, text_color: None, } diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 2a8bcf4..341be64 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -94,11 +94,11 @@ impl container::Catalog for GauntletComplexTheme { Style { text_color: None, - background: Some(background_color.to_iced().into()), + background: Some(background_color.clone().into()), border: Border { radius: root_theme.border_radius.into(), width: root_theme.border_width, - color: root_theme.border_color.to_iced(), + color: root_theme.border_color, }, shadow: Shadow { color: Color::from_rgba8(0, 0, 0, 0.50), @@ -115,11 +115,11 @@ impl container::Catalog for GauntletComplexTheme { Style { text_color: None, - background: Some(background_color.to_iced().into()), + background: Some(background_color.clone().into()), border: Border { radius: theme.border_radius.into(), width: theme.border_width, - color: border_color.to_iced().into(), + color: border_color.clone().into(), }, shadow: Default::default(), } @@ -131,11 +131,11 @@ impl container::Catalog for GauntletComplexTheme { Style { text_color: None, - background: Some(background_color.to_iced().into()), + background: Some(background_color.clone().into()), border: Border { radius: theme.border_radius.into(), width: theme.border_width, - color: border_color.to_iced().into(), + color: border_color.clone().into(), }, shadow: Default::default(), } @@ -146,11 +146,11 @@ impl container::Catalog for GauntletComplexTheme { Style { text_color: None, - background: Some(background_color.to_iced().into()), + background: Some(background_color.clone().into()), border: Border { - radius: theme.border_radius.into(), + radius: theme.border_radius.clone().into(), width: theme.border_width, - color: theme.border_color.to_iced(), + color: theme.border_color, }, shadow: Default::default(), } @@ -161,11 +161,11 @@ impl container::Catalog for GauntletComplexTheme { Style { text_color: None, - background: Some(background_color.to_iced().into()), + background: Some(background_color.clone().into()), border: Border { - radius: theme.border_radius.into(), + radius: theme.border_radius.clone().into(), width: theme.border_width, - color: theme.border_color.to_iced(), + color: theme.border_color, }, shadow: Default::default(), } @@ -177,11 +177,11 @@ impl container::Catalog for GauntletComplexTheme { Style { text_color: None, - background: Some(background_color.to_iced().into()), + background: Some(background_color.clone().into()), border: Border { - radius: theme.border_radius.into(), + radius: theme.border_radius.clone().into(), width: theme.border_width, - color: theme.border_color.to_iced(), + color: theme.border_color, }, shadow: Default::default(), } @@ -205,11 +205,11 @@ impl container::Catalog for GauntletComplexTheme { let panel_theme = &self.root_bottom_panel; Style { - background: Some(panel_theme.background_color.to_iced().into()), + background: Some(panel_theme.background_color.into()), border: Border { radius: gauntlet_common_ui::radius(0.0, 0.0, root_theme.border_radius, root_theme.border_radius), width: root_theme.border_width, - color: root_theme.border_color.to_iced(), + color: root_theme.border_color, }, ..Style::default() } @@ -218,11 +218,11 @@ impl container::Catalog for GauntletComplexTheme { let theme = &self.inline_inner; Style { - background: Some(theme.background_color.to_iced().into()), + background: Some(theme.background_color.into()), border: Border { radius: theme.border_radius.into(), width: theme.border_width, - color: theme.border_color.to_iced(), + color: theme.border_color, }, ..Style::default() } @@ -233,11 +233,11 @@ impl container::Catalog for GauntletComplexTheme { Style { text_color: None, - background: Some(background_color.to_iced().into()), + background: Some(background_color.clone().into()), border: Border { radius: theme.border_radius.into(), width: theme.border_width, - color: theme.border_color.to_iced(), + color: theme.border_color, }, shadow: Default::default(), } diff --git a/rust/client/src/ui/theme/date_picker.rs b/rust/client/src/ui/theme/date_picker.rs index 66903bf..5f21b0a 100644 --- a/rust/client/src/ui/theme/date_picker.rs +++ b/rust/client/src/ui/theme/date_picker.rs @@ -36,13 +36,13 @@ fn active(theme: &GauntletComplexTheme) -> Style { let theme = &theme.form_input_date_picker; Style { - background: theme.background_color.to_iced().into(), + background: theme.background_color.into(), border_radius: root_theme.border_radius, border_width: root_theme.border_width, - border_color: root_theme.border_color.to_iced(), - text_color: theme.text_color.to_iced(), - text_attenuated_color: theme.text_attenuated_color.to_iced(), - day_background: theme.day_background_color.to_iced().into(), + border_color: root_theme.border_color, + text_color: theme.text_color, + text_attenuated_color: theme.text_attenuated_color, + day_background: theme.day_background_color.into(), } } @@ -50,8 +50,8 @@ fn selected(theme: &GauntletComplexTheme) -> Style { let form_theme = &theme.form_input_date_picker; Style { - day_background: form_theme.day_background_color_selected.to_iced().into(), - text_color: form_theme.text_color_selected.to_iced(), + day_background: form_theme.day_background_color_selected.into(), + text_color: form_theme.text_color_selected, ..active(theme) } } @@ -60,8 +60,8 @@ fn hovered(theme: &GauntletComplexTheme) -> Style { let form_theme = &theme.form_input_date_picker; Style { - day_background: form_theme.day_background_color_hovered.to_iced().into(), - text_color: form_theme.text_color_hovered.to_iced(), + day_background: form_theme.day_background_color_hovered.into(), + text_color: form_theme.text_color_hovered, ..active(theme) } } diff --git a/rust/client/src/ui/theme/loading_bar.rs b/rust/client/src/ui/theme/loading_bar.rs index 7696322..3fb85c1 100644 --- a/rust/client/src/ui/theme/loading_bar.rs +++ b/rust/client/src/ui/theme/loading_bar.rs @@ -18,8 +18,8 @@ impl loading_bar::Catalog for GauntletComplexTheme { fn style(&self, _class: &Self::Class<'_>) -> Style { Style { - background_color: self.loading_bar.background_color.to_iced(), - loading_bar_color: self.loading_bar.loading_bar_color.to_iced(), + background_color: self.loading_bar.background_color, + loading_bar_color: self.loading_bar.loading_bar_color, } } } diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 0df214b..e377c51 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -1,10 +1,8 @@ -use std::io::ErrorKind; -use std::path::PathBuf; -use iced::{application, Color, Padding}; +use arc_swap::{ArcSwap, Guard}; +use gauntlet_common::model::{UiTheme, UiThemeColor, UiThemeMode}; use iced::application::DefaultStyle; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use gauntlet_common::dirs::Dirs; +use iced::{application, Color, Padding}; +use std::sync::Arc; pub mod button; pub mod text_input; @@ -24,56 +22,9 @@ mod loading_bar; pub type Element<'a, Message> = iced::Element<'a, Message, GauntletComplexTheme>; -const CURRENT_SIMPLE_THEME_VERSION: u64 = 5; -const CURRENT_COMPLEX_THEME_VERSION: u64 = 5; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum GauntletSimpleThemeMode { - #[serde(rename = "light")] - Light, - #[serde(rename = "dark")] - Dark -} - -pub type GauntletSimpleThemeColorPalette = [ThemeColor; 4]; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GauntletSimpleThemeWindow { - border: GauntletSimpleThemeWindowBorder, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GauntletSimpleThemeWindowBorder { - radius: f32, - width: f32, - color: ThemeColor, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GauntletSimpleThemeContent { - border: GauntletSimpleThemeContentBorder, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GauntletSimpleThemeContentBorder { - radius: f32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GauntletSimpleTheme { - version: u64, - mode: GauntletSimpleThemeMode, - // value of tint/tones/shades/whatever you have, from lower to higher - background: GauntletSimpleThemeColorPalette, - text: GauntletSimpleThemeColorPalette, - window: GauntletSimpleThemeWindow, - content: GauntletSimpleThemeContent, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct GauntletComplexTheme { - version: u64, - text: ThemeColor, + text: Color, root: ThemeRoot, popup: ThemeRoot, action: ThemeButton, @@ -160,180 +111,19 @@ impl Default for GauntletComplexTheme { } } -fn parse_json_theme(theme_file: PathBuf, theme_name: &str) -> Option { - match std::fs::read_to_string(theme_file) { - Ok(value) => { - let result = serde_json::from_str::(&value); - - match result { - Ok(value) => { - match value.get("version") { - Some(serde_json::Value::Number(number)) => { - match number.as_u64() { - None => { - tracing::warn!("Version of read {} file is invalid", theme_name); - None - } - Some(CURRENT_SIMPLE_THEME_VERSION) => { - match serde_json::from_value::(value) { - Ok(value) => Some(value), - Err(err) => { - tracing::warn!("Unable to parse {} file: {}", theme_name, err); - None - } - } - } - Some(_) => { - tracing::warn!("Version of read {} file doesn't match expected, theme: {}, expected: {}", theme_name, number, CURRENT_SIMPLE_THEME_VERSION); - None - } - } - } - _ => { - tracing::warn!("Version of read {} file is not a number", theme_name); - None - } - } - } - Err(err) => { - tracing::warn!("Unable to parse {} file: {}", theme_name, err); - None - } - } - } - Err(err) => { - match err.kind() { - ErrorKind::NotFound => { - tracing::debug!("No {} file was found", theme_name); - None - } - err @ _ => { - tracing::warn!("Unable to read {} file: {}", theme_name, err); - None - } - } - } - } -} - // TODO padding on button is padding, not margin, a lot of margins missing? impl GauntletComplexTheme { - pub fn new() -> Self { - let dirs = Dirs::new(); - - let theme = parse_json_theme(dirs.complex_theme_file(), "complex theme") - .unwrap_or_else(|| { - let simple_theme = parse_json_theme(dirs.theme_simple_file(), "simple theme") - .unwrap_or_else(|| GauntletComplexTheme::default_simple_theme()); - - GauntletComplexTheme::default_theme(simple_theme) - }); - - init_theme(theme.clone()); - - theme + pub fn set_global(theme: GauntletComplexTheme) { + init_theme(theme); } - // TODO legacy - pub fn default_simple_theme() -> GauntletSimpleTheme { - GauntletSimpleTheme { - version: CURRENT_SIMPLE_THEME_VERSION, - mode: GauntletSimpleThemeMode::Dark, - background: [ - ThemeColor::new(0x626974, 0.3), - ThemeColor::new(0x48505B, 0.5), - ThemeColor::new(0x333a42, 1.0), - ThemeColor::new(0x2C323A, 1.0) - ], - text: [ - ThemeColor::new(0xDDDFE1, 1.0), - ThemeColor::new(0x9AA0A6, 1.0), - ThemeColor::new(0x6B7785, 1.0), - ThemeColor::new(0x1D242C, 1.0), - ], - window: GauntletSimpleThemeWindow { - border: GauntletSimpleThemeWindowBorder { - radius: 10.0, - width: 1.0, - color: ThemeColor::new(0x48505B, 0.5) - }, - }, - content: GauntletSimpleThemeContent { - border: GauntletSimpleThemeContentBorder { - radius: 4.0, - }, - }, - } + pub fn update_global(theme: GauntletComplexTheme) { + set_theme(theme); } - // TODO auto detect macos dark mode - // pub fn default_simple_theme() -> GauntletSimpleTheme { - // GauntletSimpleTheme { - // version: CURRENT_SIMPLE_THEME_VERSION, - // mode: GauntletSimpleThemeMode::Dark, - // background: [ - // ThemeColor::from_rgba8(100, 100, 100, 0.5), - // ThemeColor::from_rgba8(55, 55, 55, 1.0), - // ThemeColor::from_rgba8(45, 45, 45, 1.0), - // ThemeColor::from_rgba8(36, 36, 36, 1.0), - // ], - // text: [ - // ThemeColor::from_rgba8(229,229,229, 1.0), - // ThemeColor::from_rgba8(200, 200, 200, 1.0), - // ThemeColor::from_rgba8(150, 150, 150, 1.0), - // ThemeColor::from_rgba8(50, 50, 50, 1.0), - // ], - // window: GauntletSimpleThemeWindow { - // border: GauntletSimpleThemeWindowBorder { - // radius: 8.0, - // width: 1.0, - // color: ThemeColor::from_rgba8(100, 100, 100, 1.0), - // }, - // }, - // content: GauntletSimpleThemeContent { - // border: GauntletSimpleThemeContentBorder { - // radius: 4.0, - // }, - // }, - // } - // } - - // TODO auto detect macos light mode - // pub fn default_simple_theme() -> GauntletSimpleTheme { - // GauntletSimpleTheme { - // version: CURRENT_SIMPLE_THEME_VERSION, - // mode: GauntletSimpleThemeMode::Light, - // background: [ - // ThemeColor::from_rgba8(0, 0, 0, 0.2), - // ThemeColor::from_rgba8(200, 200, 200, 1.0), - // ThemeColor::from_rgba8(210, 210, 210, 1.0), - // ThemeColor::from_rgba8(234, 234, 234, 1.0), - // ], - // text: [ - // ThemeColor::from_rgba8(0, 0, 0, 1.0), - // ThemeColor::from_rgba8(0, 0, 0, 0.847), - // ThemeColor::from_rgba8(0, 0, 0, 0.498), - // ThemeColor::from_rgba8(0, 0, 0, 0.259), - // ], - // window: GauntletSimpleThemeWindow { - // border: GauntletSimpleThemeWindowBorder { - // radius: 8.0, - // width: 1.0, - // color: ThemeColor::from_rgba8(185, 185, 185, 1.0), - // }, - // }, - // content: GauntletSimpleThemeContent { - // border: GauntletSimpleThemeContentBorder { - // radius: 4.0, - // }, - // }, - // } - // } - - pub fn default_theme(simple_theme: GauntletSimpleTheme) -> GauntletComplexTheme { - let GauntletSimpleTheme { - version: _, + pub fn new(simple_theme: UiTheme) -> GauntletComplexTheme { + let UiTheme { mode, background, text, @@ -342,10 +132,26 @@ impl GauntletComplexTheme { } = simple_theme; let [background_100, background_200, background_300, background_400] = background; - let [text_100, text_200, text_300, _text_400] = text; + let [text_100, text_200, text_300, text_400] = text; + + fn to_iced(ui_color: &UiThemeColor) -> Color { + Color::from_rgba(ui_color.r, ui_color.g, ui_color.b, ui_color.a) + } + + let [background_100, background_200, background_300, background_400] = [ + to_iced(&background_100), + to_iced(&background_200), + to_iced(&background_300), + to_iced(&background_400) + ]; + let [text_100, text_200, text_300, _text_400] = [ + to_iced(&text_100), + to_iced(&text_200), + to_iced(&text_300), + to_iced(&text_400) + ]; GauntletComplexTheme { - version: CURRENT_COMPLEX_THEME_VERSION, text: text_100, root: ThemeRoot { background_color: background_400, @@ -354,19 +160,19 @@ impl GauntletComplexTheme { #[cfg(not(target_os = "macos"))] border_width: window.border.width, #[cfg(not(target_os = "macos"))] - border_color: window.border.color, + border_color: to_iced(&window.border.color), #[cfg(target_os = "macos")] border_radius: 0.0, #[cfg(target_os = "macos")] border_width: 0.0, #[cfg(target_os = "macos")] - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, popup: ThemeRoot { background_color: background_400, border_radius: window.border.radius, border_width: window.border.width, - border_color: window.border.color, + border_color: to_iced(&window.border.color), }, action_panel: ThemePaddingBackgroundColor { padding: padding_all(8.0), @@ -377,14 +183,14 @@ impl GauntletComplexTheme { }, action: ThemeButton { padding: padding_all(8.0), - background_color: TRANSPARENT, + background_color: Color::TRANSPARENT, background_color_focused: background_100, background_color_hovered: background_300, text_color: text_100, text_color_hovered: text_100, border_radius: content.border.radius, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, action_shortcut: ThemePaddingOnly { padding: padding_all(0.0) @@ -395,7 +201,7 @@ impl GauntletComplexTheme { background_color: background_100, border_radius: content.border.radius, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, form_input: ThemePaddingOnly { padding: padding_all(8.0) @@ -406,22 +212,22 @@ impl GauntletComplexTheme { metadata_tag_item_button: ThemeButton { padding: padding_axis(2.0, 8.0), background_color: match mode { - GauntletSimpleThemeMode::Light => background_300, - GauntletSimpleThemeMode::Dark => background_200 + UiThemeMode::Light => background_300, + UiThemeMode::Dark => background_200 }, background_color_focused: match mode { - GauntletSimpleThemeMode::Light => background_200, - GauntletSimpleThemeMode::Dark => background_100 + UiThemeMode::Light => background_200, + UiThemeMode::Dark => background_100 }, background_color_hovered: match mode { - GauntletSimpleThemeMode::Light => background_200, - GauntletSimpleThemeMode::Dark => background_100 + UiThemeMode::Light => background_200, + UiThemeMode::Dark => background_100 }, text_color: text_100, text_color_hovered: text_100, border_radius: content.border.radius, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, metadata_item_label: ThemePaddingTextColorSize { padding: padding_all(0.0), @@ -466,7 +272,7 @@ impl GauntletComplexTheme { background_color: background_200, border_radius: content.border.radius, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, empty_view_image: ThemePaddingSize { padding: padding_all(8.0), @@ -484,7 +290,7 @@ impl GauntletComplexTheme { text_color_hovered: text_100, border_radius: content.border.radius, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, grid_item_title: ThemePaddingTextColor { padding: padding_axis(4.0, 0.0), @@ -501,7 +307,7 @@ impl GauntletComplexTheme { background_color: background_200, border_radius: content.border.radius, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, metadata_separator: ThemePaddingOnly { padding: padding_axis(8.0, 0.0), @@ -513,22 +319,22 @@ impl GauntletComplexTheme { root_top_panel_button: ThemeButton { padding: padding_axis(3.0, 5.0), background_color: match mode { - GauntletSimpleThemeMode::Light => background_300, - GauntletSimpleThemeMode::Dark => background_200 + UiThemeMode::Light => background_300, + UiThemeMode::Dark => background_200 }, background_color_focused: match mode { - GauntletSimpleThemeMode::Light => background_200, - GauntletSimpleThemeMode::Dark => background_100 + UiThemeMode::Light => background_200, + UiThemeMode::Dark => background_100 }, background_color_hovered: match mode { - GauntletSimpleThemeMode::Light => background_200, - GauntletSimpleThemeMode::Dark => background_100 + UiThemeMode::Light => background_200, + UiThemeMode::Dark => background_100 }, text_color: text_100, text_color_hovered: text_100, border_radius: content.border.radius, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, root_bottom_panel: ThemeBottomPanel { padding: padding_axis(6.0, 8.0), @@ -537,14 +343,14 @@ impl GauntletComplexTheme { }, root_bottom_panel_action_toggle_button: ThemeButton { padding: padding_axis(3.0, 5.0), - background_color: TRANSPARENT, + background_color: Color::TRANSPARENT, background_color_focused: background_200, background_color_hovered: background_200, text_color: text_100, text_color_hovered: text_100, border_radius: content.border.radius, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, root_bottom_panel_action_toggle_text: ThemePaddingTextColor { padding: padding(0.0, 8.0, 0.0, 4.0), @@ -556,14 +362,14 @@ impl GauntletComplexTheme { }, list_item: ThemeButton { padding: padding_all(5.0), - background_color: TRANSPARENT, + background_color: Color::TRANSPARENT, background_color_focused: background_200, background_color_hovered: background_300, text_color: text_100, text_color_hovered: text_100, border_radius: content.border.radius, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, list_item_icon: ThemePaddingOnly { padding: padding_axis(0.0, 4.0) @@ -620,20 +426,20 @@ impl GauntletComplexTheme { }, main_list_item: ThemeButton { padding: padding_all(5.0), - background_color: TRANSPARENT, + background_color: Color::TRANSPARENT, background_color_focused: match mode { - GauntletSimpleThemeMode::Light => background_300, - GauntletSimpleThemeMode::Dark => background_200 + UiThemeMode::Light => background_300, + UiThemeMode::Dark => background_200 }, background_color_hovered: match mode { - GauntletSimpleThemeMode::Light => background_200, - GauntletSimpleThemeMode::Dark => background_300 + UiThemeMode::Light => background_200, + UiThemeMode::Dark => background_300 }, text_color: text_100, text_color_hovered: text_100, border_radius: content.border.radius, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, main_list_item_text: ThemePaddingOnly { padding: padding_all(4.0), @@ -689,11 +495,11 @@ impl GauntletComplexTheme { text_color_hovered: text_100, border_radius: content.border.radius, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, form_input_checkbox: ThemeCheckbox { background_color_checked: text_200, - background_color_unchecked: TRANSPARENT, + background_color_unchecked: Color::TRANSPARENT, background_color_checked_hovered: text_100, background_color_unchecked_hovered: background_200, border_radius: content.border.radius, @@ -720,7 +526,7 @@ impl GauntletComplexTheme { border_color: background_200, }, form_input_text_field: ThemeTextField { - background_color: TRANSPARENT, + background_color: Color::TRANSPARENT, background_color_hovered: background_200, text_color: text_100, text_color_placeholder: text_300, @@ -737,7 +543,7 @@ impl GauntletComplexTheme { color: background_200, border_radius: content.border.radius, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, tooltip: ThemeTooltip { padding: 8.0, @@ -757,10 +563,10 @@ impl GauntletComplexTheme { icon_color: text_200, }, hud: ThemeRoot { - background_color: ThemeColor::new(0x1E1E1E, 0.7), + background_color: Color::from_rgba8(30,30,30, 0.7), border_radius: 30.0, border_width: 0.0, - border_color: TRANSPARENT, + border_color: Color::TRANSPARENT, }, hud_content: ThemePaddingOnly { padding: padding_axis(8.0, 16.0), @@ -770,19 +576,23 @@ impl GauntletComplexTheme { } fn init_theme(theme: GauntletComplexTheme) { - THEME.set(theme).expect("already set"); + THEME.set(ArcSwap::new(Arc::new(theme))).expect("already set"); } -fn get_theme() -> &'static GauntletComplexTheme { - &THEME.get().expect("theme global var was not set") +fn set_theme(theme: GauntletComplexTheme) { + THEME.get().expect("theme global var was not set").store(Arc::new(theme)) } -static THEME: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); +fn get_theme() -> Guard> { + THEME + .get() + .expect("theme global var was not set") + .load() +} -const NOT_INTENDED_TO_BE_USED: ThemeColor = ThemeColor::new(0xAF5BFF, 1.0); +static THEME: once_cell::sync::OnceCell> = once_cell::sync::OnceCell::new(); -// keep colors more or less in sync with settings ui -const TRANSPARENT: ThemeColor = ThemeColor::new(0x000000, 0.0); +const NOT_INTENDED_TO_BE_USED: Color = Color::from_rgba(175.0 / 255.0, 91.0 / 255.0, 255.0 / 255.0, 1.0); const fn padding(top: f32, right: f32, bottom: f32, left: f32) -> ThemePadding { ThemePadding::Each { @@ -806,240 +616,239 @@ const fn padding_axis(vertical: f32, horizontal: f32) -> ThemePadding { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeButton { padding: ThemePadding, - background_color: ThemeColor, - background_color_focused: ThemeColor, - background_color_hovered: ThemeColor, - text_color: ThemeColor, - text_color_hovered: ThemeColor, + background_color: Color, + background_color_focused: Color, + background_color_hovered: Color, + text_color: Color, + text_color_hovered: Color, border_radius: f32, border_width: f32, - border_color: ThemeColor, + border_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeCheckbox { - background_color_checked: ThemeColor, - background_color_unchecked: ThemeColor, + background_color_checked: Color, + background_color_unchecked: Color, - background_color_checked_hovered: ThemeColor, - background_color_unchecked_hovered: ThemeColor, + background_color_checked_hovered: Color, + background_color_unchecked_hovered: Color, border_radius: f32, border_width: f32, - border_color: ThemeColor, + border_color: Color, - icon_color: ThemeColor + icon_color: Color } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeSelect { - background_color: ThemeColor, - background_color_hovered: ThemeColor, + background_color: Color, + background_color_hovered: Color, - text_color: ThemeColor, - text_color_hovered: ThemeColor, + text_color: Color, + text_color_hovered: Color, border_radius: f32, border_width: f32, - border_color: ThemeColor, + border_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeSelectMenu { - background_color: ThemeColor, - background_color_selected: ThemeColor, + background_color: Color, + background_color_selected: Color, - text_color: ThemeColor, - text_color_selected: ThemeColor, + text_color: Color, + text_color_selected: Color, border_radius: f32, border_width: f32, - border_color: ThemeColor, + border_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeTextField { - background_color: ThemeColor, - background_color_hovered: ThemeColor, + background_color: Color, + background_color_hovered: Color, - text_color: ThemeColor, - text_color_placeholder: ThemeColor, + text_color: Color, + text_color_placeholder: Color, - selection_color: ThemeColor, + selection_color: Color, border_radius: f32, border_width: f32, - border_color: ThemeColor, - border_color_hovered: ThemeColor, + border_color: Color, + border_color_hovered: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeSeparator { - color: ThemeColor, + color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeScrollbar { - color: ThemeColor, + color: Color, border_radius: f32, border_width: f32, - border_color: ThemeColor, + border_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeRoot { - background_color: ThemeColor, + background_color: Color, border_radius: f32, border_width: f32, - border_color: ThemeColor, + border_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeActionShortcutModifier { padding: ThemePadding, spacing: f32, - background_color: ThemeColor, + background_color: Color, border_radius: f32, border_width: f32, - border_color: ThemeColor, + border_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeLoadingBar { - loading_bar_color: ThemeColor, - background_color: ThemeColor, + loading_bar_color: Color, + background_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeLink { - text_color: ThemeColor, - text_color_hovered: ThemeColor, + text_color: Color, + text_color_hovered: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeCode { padding: ThemePadding, - background_color: ThemeColor, + background_color: Color, border_radius: f32, border_width: f32, - border_color: ThemeColor, + border_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeInline { padding: ThemePadding, - background_color: ThemeColor, + background_color: Color, border_radius: f32, border_width: f32, - border_color: ThemeColor, + border_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeDatePicker { - background_color: ThemeColor, + background_color: Color, - text_color: ThemeColor, - text_color_selected: ThemeColor, - text_color_hovered: ThemeColor, + text_color: Color, + text_color_selected: Color, + text_color_hovered: Color, - text_attenuated_color: ThemeColor, + text_attenuated_color: Color, - day_background_color: ThemeColor, - day_background_color_selected: ThemeColor, - day_background_color_hovered: ThemeColor + day_background_color: Color, + day_background_color_selected: Color, + day_background_color_hovered: Color } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemePaddingTextColor { padding: ThemePadding, - text_color: ThemeColor, + text_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemePaddingTextColorSize { padding: ThemePadding, - text_color: ThemeColor, + text_color: Color, text_size: f32, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemePaddingBackgroundColor { padding: ThemePadding, - background_color: ThemeColor, + background_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeBottomPanel { padding: ThemePadding, - background_color: ThemeColor, + background_color: Color, spacing: f32, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeTooltip { padding: f32, // TODO for some reason padding on tooltip is a single number in iced-rs - background_color: ThemeColor, + background_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemePaddingOnly { padding: ThemePadding, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeImage { padding: ThemePadding, border_radius: f32, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeTextColor { - text_color: ThemeColor, + text_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemePaddingSize { padding: ThemePadding, size: ExternalThemeSize, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ExternalThemeGrid { padding: ThemePadding, spacing: f32, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemeIconAccessory { padding: ThemePadding, - icon_color: ThemeColor, + icon_color: Color, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemePaddingTextColorSpacing { padding: ThemePadding, - text_color: ThemeColor, + text_color: Color, spacing: f32, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ThemePaddingSpacing { padding: ThemePadding, spacing: f32, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ExternalThemeSize { width: f32, height: f32, } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] +#[derive(Debug, Clone)] pub enum ThemePadding { Each { top: f32, @@ -1087,37 +896,6 @@ impl ThemePadding { } } - -#[derive(Clone, Debug, Copy, Serialize, Deserialize)] -pub struct ThemeColor { - r: u8, - g: u8, - b: u8, - a: f32, -} - -impl ThemeColor { - #[allow(unused_parens)] - const fn new(hex: u32, a: f32) -> Self { - let r = ((hex & 0xff0000) >> 16) as u8; - let g = ((hex & 0xff00) >> 8) as u8; - let b = (hex & 0xff) as u8; - - Self { r, g, b, a } - } - - fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Self { - let color = Color::from_rgba8(r, g, b, a); - let [r, g, b, _] = color.into_rgba8(); - - Self { r, g, b, a } - } - - pub fn to_iced(&self) -> Color { - Color::from_rgba8(self.r, self.g, self.b, self.a) - } -} - pub trait ThemableWidget<'a, Message> { type Kind; @@ -1130,7 +908,7 @@ impl DefaultStyle for GauntletComplexTheme { application::Appearance { background_color: Color::TRANSPARENT, - text_color: theme.text.to_iced(), + text_color: theme.text, } } } @@ -1142,10 +920,8 @@ impl iced_layershell::DefaultStyle for GauntletComplexTheme { iced_layershell::Appearance { background_color: Color::TRANSPARENT, - text_color: theme.text.to_iced(), + text_color: theme.text, } } } - - diff --git a/rust/client/src/ui/theme/pick_list.rs b/rust/client/src/ui/theme/pick_list.rs index 7ad0da3..09be609 100644 --- a/rust/client/src/ui/theme/pick_list.rs +++ b/rust/client/src/ui/theme/pick_list.rs @@ -29,24 +29,24 @@ impl pick_list::Catalog for GauntletComplexTheme { let theme = &theme.form_input_select; let background_color = match status { - Status::Active | Status::Opened => theme.background_color.to_iced(), - Status::Hovered => theme.background_color_hovered.to_iced(), + Status::Active | Status::Opened => theme.background_color, + Status::Hovered => theme.background_color_hovered, }; let text_color = match status { - Status::Active | Status::Opened => theme.text_color.to_iced(), - Status::Hovered => theme.text_color_hovered.to_iced(), + Status::Active | Status::Opened => theme.text_color, + Status::Hovered => theme.text_color_hovered, }; pick_list::Style { text_color, background: background_color.into(), - placeholder_color: NOT_INTENDED_TO_BE_USED.to_iced(), + placeholder_color: NOT_INTENDED_TO_BE_USED, handle_color: text_color, border: Border { radius: theme.border_radius.into(), width: theme.border_width, - color: theme.border_color.to_iced().into(), + color: theme.border_color.into(), }, } } @@ -63,15 +63,15 @@ impl overlay::menu::Catalog for GauntletComplexTheme { let theme = &self.form_input_select_menu; // TODO consider using root style overlay::menu::Style { - text_color: theme.text_color.to_iced(), - background: theme.background_color.to_iced().into(), + text_color: theme.text_color, + background: theme.background_color.into(), border: Border { radius: theme.border_radius.into(), width: theme.border_width, - color: theme.border_color.to_iced().into(), + color: theme.border_color.into(), }, - selected_text_color: theme.text_color_selected.to_iced(), - selected_background: theme.background_color_selected.to_iced().into(), + selected_text_color: theme.text_color_selected, + selected_background: theme.background_color_selected.into(), } } } diff --git a/rust/client/src/ui/theme/rule.rs b/rust/client/src/ui/theme/rule.rs index 61095e1..117d548 100644 --- a/rust/client/src/ui/theme/rule.rs +++ b/rust/client/src/ui/theme/rule.rs @@ -20,7 +20,7 @@ impl rule::Catalog for GauntletComplexTheme { match class { RuleStyle::Default => { Style { - color: self.separator.color.to_iced(), + color: self.separator.color, width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Full, @@ -28,7 +28,7 @@ impl rule::Catalog for GauntletComplexTheme { } RuleStyle::ActionPanel => { Style { - color: self.separator.color.to_iced(), + color: self.separator.color, width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Percent(96.0), @@ -36,7 +36,7 @@ impl rule::Catalog for GauntletComplexTheme { } RuleStyle::PrimaryActionSeparator => { Style { - color: self.separator.color.to_iced(), + color: self.separator.color, width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Percent(70.0), diff --git a/rust/client/src/ui/theme/scrollable.rs b/rust/client/src/ui/theme/scrollable.rs index d55a622..ec84ee4 100644 --- a/rust/client/src/ui/theme/scrollable.rs +++ b/rust/client/src/ui/theme/scrollable.rs @@ -21,7 +21,7 @@ impl scrollable::Catalog for GauntletComplexTheme { scroller: scrollable::Scroller { color: Color::TRANSPARENT, border: Border { - color: theme.border_color.to_iced(), + color: theme.border_color, width: theme.border_width.into(), radius: theme.border_radius.into(), }, @@ -41,7 +41,7 @@ impl scrollable::Catalog for GauntletComplexTheme { } => { let hovered_scrollbar = scrollable::Rail { scroller: scrollable::Scroller { - color: theme.color.to_iced(), + color: theme.color, ..scrollbar.scroller }, ..scrollbar @@ -68,7 +68,7 @@ impl scrollable::Catalog for GauntletComplexTheme { } => { let dragged_scrollbar = scrollable::Rail { scroller: scrollable::Scroller { - color: theme.color.to_iced(), + color: theme.color, ..scrollbar.scroller }, ..scrollbar diff --git a/rust/client/src/ui/theme/text.rs b/rust/client/src/ui/theme/text.rs index 3760984..2e390d8 100644 --- a/rust/client/src/ui/theme/text.rs +++ b/rust/client/src/ui/theme/text.rs @@ -62,52 +62,52 @@ impl text::Catalog for GauntletComplexTheme { match class { TextStyle::Default => Default::default(), TextStyle::EmptyViewSubtitle => Style { - color: Some(self.empty_view_subtitle.text_color.to_iced()), + color: Some(self.empty_view_subtitle.text_color), }, TextStyle::ListItemSubtitle => Style { - color: Some(self.list_item_subtitle.text_color.to_iced()), + color: Some(self.list_item_subtitle.text_color), }, TextStyle::ListSectionTitle => Style { - color: Some(self.list_section_title.text_color.to_iced()), + color: Some(self.list_section_title.text_color), }, TextStyle::ListSectionSubtitle => Style { - color: Some(self.list_section_subtitle.text_color.to_iced()), + color: Some(self.list_section_subtitle.text_color), }, TextStyle::GridSectionTitle => Style { - color: Some(self.grid_section_title.text_color.to_iced()), + color: Some(self.grid_section_title.text_color), }, TextStyle::GridSectionSubtitle => Style{ - color: Some(self.grid_section_subtitle.text_color.to_iced()), + color: Some(self.grid_section_subtitle.text_color), }, TextStyle::MainListItemSubtext => Style { - color: Some(self.main_list_item_sub_text.text_color.to_iced()), + color: Some(self.main_list_item_sub_text.text_color), }, TextStyle::MetadataItemLabel => Style { - color: Some(self.metadata_item_label.text_color.to_iced()), + color: Some(self.metadata_item_label.text_color), }, TextStyle::TextAccessory => Style { - color: Some(self.text_accessory.text_color.to_iced()), + color: Some(self.text_accessory.text_color), }, TextStyle::IconAccessory => Style { - color: Some(self.icon_accessory.icon_color.to_iced()), + color: Some(self.icon_accessory.icon_color), }, TextStyle::GridItemTitle => Style { - color: Some(self.grid_item_title.text_color.to_iced()), + color: Some(self.grid_item_title.text_color), }, TextStyle::GridItemSubTitle => Style { - color: Some(self.grid_item_subtitle.text_color.to_iced()), + color: Some(self.grid_item_subtitle.text_color), }, TextStyle::InlineName => Style { - color: Some(self.inline_name.text_color.to_iced()), + color: Some(self.inline_name.text_color), }, TextStyle::InlineSeparator => Style { - color: Some(self.inline_separator.text_color.to_iced()), + color: Some(self.inline_separator.text_color), }, TextStyle::RootBottomPanelPrimaryActionText => Style { - color: Some(self.root_bottom_panel_primary_action_text.text_color.to_iced()), + color: Some(self.root_bottom_panel_primary_action_text.text_color), }, TextStyle::RootBottomPanelActionToggleText => Style { - color: Some(self.root_bottom_panel_action_toggle_text.text_color.to_iced()), + color: Some(self.root_bottom_panel_action_toggle_text.text_color), } } } diff --git a/rust/client/src/ui/theme/text_input.rs b/rust/client/src/ui/theme/text_input.rs index ae0f6e4..75ea3b5 100644 --- a/rust/client/src/ui/theme/text_input.rs +++ b/rust/client/src/ui/theme/text_input.rs @@ -33,31 +33,31 @@ fn active(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { match style { TextInputStyle::ShouldNotBeUsed => { Style { - background: NOT_INTENDED_TO_BE_USED.to_iced().into(), + background: NOT_INTENDED_TO_BE_USED.into(), border: Border { - color: NOT_INTENDED_TO_BE_USED.to_iced().into(), + color: NOT_INTENDED_TO_BE_USED.into(), ..Border::default() }, - icon: NOT_INTENDED_TO_BE_USED.to_iced(), - placeholder: NOT_INTENDED_TO_BE_USED.to_iced(), - value: NOT_INTENDED_TO_BE_USED.to_iced(), - selection: NOT_INTENDED_TO_BE_USED.to_iced(), + icon: NOT_INTENDED_TO_BE_USED, + placeholder: NOT_INTENDED_TO_BE_USED, + value: NOT_INTENDED_TO_BE_USED, + selection: NOT_INTENDED_TO_BE_USED, } }, TextInputStyle::FormInput => { let theme = &theme.form_input_text_field; Style { - background: theme.background_color.to_iced().into(), + background: theme.background_color.into(), border: Border { radius: theme.border_radius.into(), width: theme.border_width, - color: theme.border_color.to_iced().into(), + color: theme.border_color.into(), }, - icon: NOT_INTENDED_TO_BE_USED.to_iced(), - placeholder: theme.text_color_placeholder.to_iced(), - value: theme.text_color.to_iced(), - selection: theme.selection_color.to_iced(), + icon: NOT_INTENDED_TO_BE_USED, + placeholder: theme.text_color_placeholder, + value: theme.text_color, + selection: theme.selection_color, } }, TextInputStyle::MainSearch | TextInputStyle::PluginSearchBar => { @@ -67,10 +67,10 @@ fn active(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { color: Color::TRANSPARENT, ..Border::default() }, - icon: NOT_INTENDED_TO_BE_USED.to_iced(), - placeholder: theme.form_input_text_field.text_color_placeholder.to_iced(), // TODO fix - value: theme.form_input_text_field.text_color.to_iced(), // TODO fix - selection: theme.form_input_text_field.selection_color.to_iced(), // TODO fix + icon: NOT_INTENDED_TO_BE_USED, + placeholder: theme.form_input_text_field.text_color_placeholder, // TODO fix + value: theme.form_input_text_field.text_color, // TODO fix + selection: theme.form_input_text_field.selection_color, // TODO fix } }, } @@ -80,31 +80,31 @@ fn focused(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { match style { TextInputStyle::ShouldNotBeUsed => { Style { - background: NOT_INTENDED_TO_BE_USED.to_iced().into(), + background: NOT_INTENDED_TO_BE_USED.into(), border: Border { - color: NOT_INTENDED_TO_BE_USED.to_iced().into(), + color: NOT_INTENDED_TO_BE_USED.into(), ..Border::default() }, - icon: NOT_INTENDED_TO_BE_USED.to_iced(), - placeholder: NOT_INTENDED_TO_BE_USED.to_iced(), - value: NOT_INTENDED_TO_BE_USED.to_iced(), - selection: NOT_INTENDED_TO_BE_USED.to_iced(), + icon: NOT_INTENDED_TO_BE_USED, + placeholder: NOT_INTENDED_TO_BE_USED, + value: NOT_INTENDED_TO_BE_USED, + selection: NOT_INTENDED_TO_BE_USED, } }, TextInputStyle::FormInput => { let theme = &theme.form_input_text_field; Style { - background: theme.background_color_hovered.to_iced().into(), + background: theme.background_color_hovered.into(), border: Border { radius: theme.border_radius.into(), width: theme.border_width, - color: theme.border_color_hovered.to_iced().into(), + color: theme.border_color_hovered.into(), }, - icon: NOT_INTENDED_TO_BE_USED.to_iced(), - placeholder: theme.text_color_placeholder.to_iced(), - value: theme.text_color.to_iced(), - selection: theme.selection_color.to_iced(), + icon: NOT_INTENDED_TO_BE_USED, + placeholder: theme.text_color_placeholder, + value: theme.text_color, + selection: theme.selection_color, } }, TextInputStyle::MainSearch | TextInputStyle::PluginSearchBar => { @@ -114,10 +114,10 @@ fn focused(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { color: Color::TRANSPARENT, ..Border::default() }, - icon: NOT_INTENDED_TO_BE_USED.to_iced(), - placeholder: theme.form_input_text_field.text_color_placeholder.to_iced(), // TODO fix - value: theme.form_input_text_field.text_color.to_iced(), // TODO fix - selection: theme.form_input_text_field.selection_color.to_iced(), // TODO fix + icon: NOT_INTENDED_TO_BE_USED, + placeholder: theme.form_input_text_field.text_color_placeholder, // TODO fix + value: theme.form_input_text_field.text_color, // TODO fix + selection: theme.form_input_text_field.selection_color, // TODO fix } }, } @@ -125,16 +125,16 @@ fn focused(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { fn disabled() -> Style { Style { - background: NOT_INTENDED_TO_BE_USED.to_iced().into(), + background: NOT_INTENDED_TO_BE_USED.into(), border: Border { radius: 2.0.into(), width: 1.0, color: Color::TRANSPARENT, }, - icon: NOT_INTENDED_TO_BE_USED.to_iced(), - placeholder: NOT_INTENDED_TO_BE_USED.to_iced(), - value: NOT_INTENDED_TO_BE_USED.to_iced(), - selection: NOT_INTENDED_TO_BE_USED.to_iced(), + icon: NOT_INTENDED_TO_BE_USED, + placeholder: NOT_INTENDED_TO_BE_USED, + value: NOT_INTENDED_TO_BE_USED, + selection: NOT_INTENDED_TO_BE_USED, } } diff --git a/rust/common/src/dirs.rs b/rust/common/src/dirs.rs index 51c15fe..f0562ef 100644 --- a/rust/common/src/dirs.rs +++ b/rust/common/src/dirs.rs @@ -50,20 +50,8 @@ impl Dirs { self.config_dir().join("config.toml") } - pub fn complex_theme_file(&self) -> PathBuf { - self.config_dir().join("complex-theme.json") - } - - pub fn sample_complex_theme_file(&self) -> PathBuf { - self.config_dir().join("complex-theme.sample.json") - } - - pub fn theme_simple_file(&self) -> PathBuf { - self.config_dir().join("simple-theme.json") - } - - pub fn sample_simple_theme_color_file(&self) -> PathBuf { - self.config_dir().join("simple-theme.sample.json") + pub fn theme_file(&self) -> PathBuf { + self.config_dir().join("theme.toml") } pub fn config_dir(&self) -> PathBuf { diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index fed28de..c9c5ed1 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::fmt::Display; use std::path::PathBuf; use std::sync::Arc; @@ -149,6 +150,64 @@ pub enum SearchResultEntrypointType { Generated, } +#[derive(Debug, Clone)] +pub enum UiThemeMode { + Light, + Dark +} + +#[derive(Debug, Clone)] +pub struct UiThemeColor { + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, +} + +pub type UiThemeColorPalette = [UiThemeColor; 4]; + +#[derive(Debug, Clone)] +pub struct UiThemeWindow { + pub border: UiThemeWindowBorder, +} + +#[derive(Debug, Clone)] +pub struct UiThemeWindowBorder { + pub radius: f32, + pub width: f32, + pub color: UiThemeColor, +} + +#[derive(Debug, Clone)] +pub struct UiThemeContent { + pub border: UiThemeContentBorder, +} + +#[derive(Debug, Clone)] +pub struct UiThemeContentBorder { + pub radius: f32, +} + +#[derive(Debug, Clone)] +pub struct UiTheme { + pub mode: UiThemeMode, + pub background: UiThemeColorPalette, + pub text: UiThemeColorPalette, + pub window: UiThemeWindow, + pub content: UiThemeContent, +} + +#[derive(Debug)] +pub struct UiSetupData { + pub theme: UiTheme, + pub global_shortcut: Option, +} + +#[derive(Debug)] +pub struct UiSetupResponse { + pub global_shortcut_error: Option, +} + #[derive(Debug)] pub enum UiResponseData { Nothing, @@ -194,11 +253,17 @@ pub enum UiRequestData { SetGlobalShortcut { shortcut: Option }, + SetTheme { + theme: UiTheme + }, } #[derive(Debug)] pub enum BackendResponseData { Nothing, + SetupData { + data: UiSetupData + }, Search { results: Vec }, @@ -212,6 +277,7 @@ pub enum BackendResponseData { #[derive(Debug)] pub enum BackendRequestData { + Setup, Search { text: String, render_inline_view: bool @@ -258,6 +324,9 @@ pub enum BackendRequestData { entrypoint_id: Option }, InlineViewShortcuts, + SetupResponse { + global_shortcut_error: Option + }, } #[derive(Debug, Clone)] @@ -589,6 +658,32 @@ pub enum SettingsEntrypointType { EntrypointGenerator, } +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum SettingsTheme { + AutoDetect, + ThemeFile, + Config, + // Custom, TODO specify file path or drag and drop via settings ui + MacOSLight, + MacOSDark, + Legacy +} + +impl Display for SettingsTheme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let label = match self { + SettingsTheme::AutoDetect => "Auto-detect", + SettingsTheme::ThemeFile => "Theme file present", + SettingsTheme::Config => "Config setting present", + SettingsTheme::MacOSLight => "macOS Light", + SettingsTheme::MacOSDark => "macOS Dark", + SettingsTheme::Legacy => "Legacy", + }; + + write!(f, "{}", label) + } +} + #[derive(Debug, Clone)] pub enum PluginPreferenceUserData { Number { diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index 869827b..eff0da7 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -5,8 +5,8 @@ use tonic::transport::Channel; use gauntlet_utils::channel::{RequestError, RequestSender}; -use crate::model::{BackendRequestData, BackendResponseData, DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, UiPropertyValue, UiWidgetId}; -use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadStatus, RpcDownloadStatusRequest, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcPingRequest, RpcPluginsRequest, RpcRemovePluginRequest, RpcSaveLocalPluginRequest, RpcSetEntrypointStateRequest, RpcSetGlobalShortcutRequest, RpcSetPluginStateRequest, RpcSetPreferenceValueRequest, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowWindowRequest}; +use crate::model::{BackendRequestData, BackendResponseData, DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, SettingsTheme, UiPropertyValue, UiSetupData, UiWidgetId}; +use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadStatus, RpcDownloadStatusRequest, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetThemeRequest, RpcPingRequest, RpcPluginsRequest, RpcRemovePluginRequest, RpcSaveLocalPluginRequest, RpcSetEntrypointStateRequest, RpcSetGlobalShortcutRequest, RpcSetPluginStateRequest, RpcSetPreferenceValueRequest, RpcSetThemeRequest, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowWindowRequest}; use crate::rpc::grpc::rpc_backend_client::RpcBackendClient; use crate::rpc::grpc_convert::{plugin_preference_from_rpc, plugin_preference_user_data_from_rpc, plugin_preference_user_data_to_rpc}; @@ -41,6 +41,28 @@ impl BackendForFrontendApi { } } + pub async fn setup_data(&mut self) -> Result { + let request = BackendRequestData::Setup; + + let BackendResponseData::SetupData { data } = self.backend_sender.send_receive(request).await? else { + unreachable!() + }; + + Ok(data) + } + + pub async fn setup_response(&mut self, global_shortcut_error: Option) -> Result<(), BackendForFrontendApiError> { + let request = BackendRequestData::SetupResponse { + global_shortcut_error + }; + + let BackendResponseData::Nothing = self.backend_sender.send_receive(request).await? else { + unreachable!() + }; + + Ok(()) + } + pub async fn search(&mut self, text: String, render_inline_view: bool) -> Result, BackendForFrontendApiError> { let request = BackendRequestData::Search { text, @@ -392,6 +414,45 @@ impl BackendApi { )) } + pub async fn set_theme(&mut self, theme: SettingsTheme) -> Result<(), BackendApiError> { + let theme = match theme { + SettingsTheme::AutoDetect => "AutoDetect", + SettingsTheme::ThemeFile => "ThemeFile", + SettingsTheme::Config => "Config", + SettingsTheme::MacOSLight => "MacOSLight", + SettingsTheme::MacOSDark => "MacOSDark", + SettingsTheme::Legacy => "Legacy", + }; + + let request = RpcSetThemeRequest { + theme: theme.to_string() + }; + + self.client.set_theme(Request::new(request)) + .await?; + + Ok(()) + } + + pub async fn get_theme(&mut self) -> Result { + let response = self.client.get_theme(Request::new(RpcGetThemeRequest::default())) + .await?; + + let theme = response.into_inner().theme; + + let theme = match theme.as_str() { + "AutoDetect" => SettingsTheme::AutoDetect, + "ThemeFile" => SettingsTheme::ThemeFile, + "Config" => SettingsTheme::Config, + "MacOSLight" => SettingsTheme::MacOSLight, + "MacOSDark" => SettingsTheme::MacOSDark, + "Legacy" => SettingsTheme::Legacy, + _ => unreachable!() + }; + + Ok(theme) + } + pub async fn set_preference_value(&mut self, plugin_id: PluginId, entrypoint_id: Option, id: String, user_data: PluginPreferenceUserData) -> Result<(), BackendApiError> { let request = RpcSetPreferenceValueRequest { plugin_id: plugin_id.to_string(), diff --git a/rust/common/src/rpc/backend_server.rs b/rust/common/src/rpc/backend_server.rs index cc9e8a0..f2d45f4 100644 --- a/rust/common/src/rpc/backend_server.rs +++ b/rust/common/src/rpc/backend_server.rs @@ -6,8 +6,8 @@ use tokio::net::TcpStream; use tonic::{Request, Response, Status}; use tonic::transport::Server; -use crate::model::{DownloadStatus, EntrypointId, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SettingsEntrypointType, SettingsPlugin}; -use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadPluginResponse, RpcDownloadStatus, RpcDownloadStatusRequest, RpcDownloadStatusResponse, RpcDownloadStatusValue, RpcEntrypoint, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetGlobalShortcutResponse, RpcPingRequest, RpcPingResponse, RpcPlugin, RpcPluginsRequest, RpcPluginsResponse, RpcRemovePluginRequest, RpcRemovePluginResponse, RpcSaveLocalPluginRequest, RpcSaveLocalPluginResponse, RpcSetEntrypointStateRequest, RpcSetEntrypointStateResponse, RpcSetGlobalShortcutRequest, RpcSetGlobalShortcutResponse, RpcSetPluginStateRequest, RpcSetPluginStateResponse, RpcSetPreferenceValueRequest, RpcSetPreferenceValueResponse, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowSettingsWindowResponse, RpcShowWindowRequest, RpcShowWindowResponse}; +use crate::model::{DownloadStatus, EntrypointId, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SettingsEntrypointType, SettingsPlugin, SettingsTheme}; +use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadPluginResponse, RpcDownloadStatus, RpcDownloadStatusRequest, RpcDownloadStatusResponse, RpcDownloadStatusValue, RpcEntrypoint, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetGlobalShortcutResponse, RpcGetThemeRequest, RpcGetThemeResponse, RpcPingRequest, RpcPingResponse, RpcPlugin, RpcPluginsRequest, RpcPluginsResponse, RpcRemovePluginRequest, RpcRemovePluginResponse, RpcSaveLocalPluginRequest, RpcSaveLocalPluginResponse, RpcSetEntrypointStateRequest, RpcSetEntrypointStateResponse, RpcSetGlobalShortcutRequest, RpcSetGlobalShortcutResponse, RpcSetPluginStateRequest, RpcSetPluginStateResponse, RpcSetPreferenceValueRequest, RpcSetPreferenceValueResponse, RpcSetThemeRequest, RpcSetThemeResponse, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowSettingsWindowResponse, RpcShowWindowRequest, RpcShowWindowResponse}; use crate::rpc::grpc::rpc_backend_server::{RpcBackend, RpcBackendServer}; use crate::rpc::grpc_convert::{plugin_preference_to_rpc, plugin_preference_user_data_from_rpc, plugin_preference_user_data_to_rpc}; @@ -75,6 +75,15 @@ pub trait BackendServer { &self, ) -> anyhow::Result<(Option, Option)>; + async fn set_theme( + &self, + theme: SettingsTheme + ) -> anyhow::Result<()>; + + async fn get_theme( + &self, + ) -> anyhow::Result; + async fn set_preference_value( &self, plugin_id: PluginId, @@ -257,6 +266,45 @@ impl RpcBackend for RpcBackendServerImpl { })) } + async fn set_theme(&self, request: Request) -> Result, Status> { + let theme = request.into_inner().theme; + + let theme = match theme.as_str() { + "AutoDetect" => SettingsTheme::AutoDetect, + "ThemeFile" => SettingsTheme::ThemeFile, + "Config" => SettingsTheme::Config, + "MacOSLight" => SettingsTheme::MacOSLight, + "MacOSDark" => SettingsTheme::MacOSDark, + "Legacy" => SettingsTheme::Legacy, + _ => unreachable!() + }; + + self.server.set_theme(theme) + .await + .map_err(|err| Status::internal(format!("{:#}", err)))?; + + Ok(Response::new(RpcSetThemeResponse::default())) + } + + async fn get_theme(&self, _request: Request) -> Result, Status> { + let theme = self.server.get_theme() + .await + .map_err(|err| Status::internal(format!("{:#}", err)))?; + + let theme = match theme { + SettingsTheme::AutoDetect => "AutoDetect", + SettingsTheme::ThemeFile => "ThemeFile", + SettingsTheme::Config => "Config", + SettingsTheme::MacOSLight => "MacOSLight", + SettingsTheme::MacOSDark => "MacOSDark", + SettingsTheme::Legacy => "Legacy", + }; + + Ok(Response::new(RpcGetThemeResponse { + theme: theme.to_string(), + })) + } + async fn download_plugin(&self, request: Request) -> Result, Status> { let request = request.into_inner(); let plugin_id = request.plugin_id; diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index 150d4d1..f8b4869 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -3,7 +3,7 @@ use anyhow::anyhow; use thiserror::Error; use gauntlet_utils::channel::{RequestError, RequestSender}; -use crate::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; +use crate::model::{EntrypointId, UiTheme, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; #[derive(Error, Debug)] pub enum FrontendApiError { @@ -180,4 +180,22 @@ impl FrontendApi { UiResponseData::Err(err) => Err(err) } } + + pub async fn set_theme( + &self, + theme: UiTheme + ) -> anyhow::Result<()> { + let request = UiRequestData::SetTheme { + theme, + }; + + let data = self.frontend_sender.send_receive(request) + .await + .map_err(|err| anyhow!("error: {:?}", err))?; + + match data { + UiResponseData::Nothing => Ok(()), + UiResponseData::Err(err) => Err(err) + } + } } \ No newline at end of file diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index 306b6fb..42a073a 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -8,7 +8,7 @@ use iced_aw::Spinner; use iced_fonts::{Bootstrap, BOOTSTRAP_FONT, BOOTSTRAP_FONT_BYTES}; use itertools::Itertools; -use gauntlet_common::model::{DownloadStatus, PluginId}; +use gauntlet_common::model::{DownloadStatus, PhysicalShortcut, PluginId, SettingsTheme}; use gauntlet_common::rpc::backend_api::{BackendApi, BackendApiError}; use gauntlet_common_ui::padding; use crate::theme::{Element, GauntletSettingsTheme}; @@ -97,28 +97,26 @@ fn new() -> (ManagementAppModel, Task) { }, Task::batch([ font::load(BOOTSTRAP_FONT_BYTES).map(ManagementAppMsg::FontLoaded), - Task::perform( - async {}, - |()| ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::RequestPluginReload) - ), + Task::done(ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::FetchPlugins)), Task::perform( async { match backend_api { - Some(mut backend_api) => { - let shortcut = backend_api.get_global_shortcut() - .await; - - Some(shortcut) - } + Some(backend_api) => Some(init_data(backend_api).await), None => None } }, |shortcut| { match shortcut { None => ManagementAppMsg::General(ManagementAppGeneralMsgIn::Noop), - Some(shortcut) => { - match shortcut { - Ok((shortcut, error)) => ManagementAppMsg::General(ManagementAppGeneralMsgIn::RefreshShortcut { shortcut, error }), + Some(init) => { + match init { + Ok(init) => { + ManagementAppMsg::General(ManagementAppGeneralMsgIn::InitSetting { + theme: init.theme, + shortcut: init.global_shortcut, + shortcut_error: init.global_shortcut_error + }) + }, Err(err) => ManagementAppMsg::HandleBackendError(err) } } @@ -129,6 +127,26 @@ fn new() -> (ManagementAppModel, Task) { ) } +struct InitSettingsData { + global_shortcut: Option, + global_shortcut_error: Option, + theme: SettingsTheme +} + +async fn init_data(mut backend_api: BackendApi) -> Result { + let (global_shortcut, global_shortcut_error) = backend_api.get_global_shortcut() + .await?; + + let theme = backend_api.get_theme() + .await?; + + Ok(InitSettingsData { + global_shortcut, + global_shortcut_error, + theme + }) +} + fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task { let backend_api = match &state.backend_api { Some(backend_api) => backend_api.clone(), @@ -143,7 +161,7 @@ fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task { - ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::PluginsReloaded(plugins)) + ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::PluginsFetched(plugins)) } ManagementAppPluginMsgOut::Noop => { ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::Noop) @@ -214,7 +232,7 @@ fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task { diff --git a/rust/management_client/src/views/general.rs b/rust/management_client/src/views/general.rs index fbfa96e..a819ca2 100644 --- a/rust/management_client/src/views/general.rs +++ b/rust/management_client/src/views/general.rs @@ -1,18 +1,19 @@ use crate::components::shortcut_selector::ShortcutSelector; use crate::theme::text::TextStyle; use crate::theme::Element; -use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::{PhysicalShortcut, SettingsTheme}; use gauntlet_common::rpc::backend_api::{BackendApi, BackendApiError}; use iced::alignment::Horizontal; use iced::widget::text::Shaping; use iced::widget::tooltip::Position; -use iced::widget::{column, container, row, text, tooltip, value, Space}; +use iced::widget::{column, container, pick_list, row, text, tooltip, value, Space}; use iced::{alignment, Alignment, Length, Padding, Task}; use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; use crate::theme::container::ContainerStyle; pub struct ManagementAppGeneralState { backend_api: Option, + theme: SettingsTheme, current_shortcut: Option, current_shortcut_error: Option, currently_capturing: bool @@ -22,9 +23,11 @@ pub struct ManagementAppGeneralState { pub enum ManagementAppGeneralMsgIn { ShortcutCaptured(Option), CapturingChanged(bool), - RefreshShortcut { + ThemeChanged(SettingsTheme), + InitSetting { + theme: SettingsTheme, shortcut: Option, - error: Option + shortcut_error: Option }, Noop } @@ -39,6 +42,7 @@ impl ManagementAppGeneralState { pub fn new(backend_api: Option) -> Self { Self { backend_api, + theme: SettingsTheme::AutoDetect, current_shortcut: None, current_shortcut_error: None, currently_capturing: false, @@ -69,36 +73,77 @@ impl ManagementAppGeneralState { ManagementAppGeneralMsgIn::Noop => { Task::none() } - ManagementAppGeneralMsgIn::RefreshShortcut { shortcut, error } => { + ManagementAppGeneralMsgIn::InitSetting { theme, shortcut, shortcut_error } => { + self.theme = theme; self.current_shortcut = shortcut; - self.current_shortcut_error = error; + self.current_shortcut_error = shortcut_error; - Task::perform(async move {}, |_| ManagementAppGeneralMsgOut::Noop) + Task::done(ManagementAppGeneralMsgOut::Noop) } ManagementAppGeneralMsgIn::CapturingChanged(capturing) => { self.currently_capturing = capturing; Task::none() } + ManagementAppGeneralMsgIn::ThemeChanged(theme) => { + self.theme = theme.clone(); + + let mut backend_api = backend_api.clone(); + + Task::perform(async move { + backend_api.set_theme(theme) + .await?; + + Ok(()) + }, |result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop)) + + } } } pub fn view(&self) -> Element { - let shortcut_selector: Element<_> = ShortcutSelector::new( + let global_shortcut_selector: Element<_> = ShortcutSelector::new( &self.current_shortcut, move |value| { ManagementAppGeneralMsgIn::ShortcutCaptured(value) }, move |value| { ManagementAppGeneralMsgIn::CapturingChanged(value) }, ).into(); - let field: Element<_> = container(shortcut_selector) + let global_shortcut_field: Element<_> = container(global_shortcut_selector) .width(Length::Fill) .height(Length::Fixed(35.0)) .into(); - let field = self.view_field("Global Shortcut", field.into()); + let global_shortcut_field = self.view_field( + "Global Shortcut", + global_shortcut_field, + Some(self.shortcut_capture_after()) + ); - let content: Element<_> = column(vec![field]) + let theme_items = [ + SettingsTheme::AutoDetect, + SettingsTheme::MacOSLight, + SettingsTheme::MacOSDark, + SettingsTheme::Legacy, + ]; + + let theme_field: Element<_> = pick_list( + theme_items, + Some(self.theme.clone()), + move |item| ManagementAppGeneralMsgIn::ThemeChanged(item), + ).into(); + + let theme_field: Element<_> = container(theme_field) + .width(Length::Fill) + .into(); + + let theme_field = self.view_field( + "Theme", + theme_field, + None + ); + + let content: Element<_> = column(vec![global_shortcut_field, theme_field]) .into(); let content: Element<_> = container(content) @@ -112,7 +157,7 @@ impl ManagementAppGeneralState { content } - fn view_field<'a>(&'a self, label: &'a str, input: Element<'a, ManagementAppGeneralMsgIn>) -> Element<'a, ManagementAppGeneralMsgIn> { + fn view_field<'a>(&'a self, label: &'a str, input: Element<'a, ManagementAppGeneralMsgIn>, after: Option>) -> Element<'a, ManagementAppGeneralMsgIn> { let label: Element<_> = text(label) .shaping(Shaping::Advanced) .align_x(Horizontal::Right) @@ -129,7 +174,27 @@ impl ManagementAppGeneralState { .padding(4) .into(); - let after = if self.currently_capturing { + let after = after.unwrap_or_else(|| { + Space::with_width(Length::FillPortion(3)) + .into() + }); + + let content = vec![ + label, + input_field, + after, + ]; + + let row: Element<_> = row(content) + .align_y(Alignment::Center) + .padding(12) + .into(); + + row + } + + fn shortcut_capture_after(&self) -> Element { + if self.currently_capturing { let hint1: Element<_> = text("Backspace - Unset Shortcut") .width(Length::Fill) .class(TextStyle::Subtitle) @@ -176,20 +241,7 @@ impl ManagementAppGeneralState { Space::with_width(Length::FillPortion(3)) .into() } - }; - - let content = vec![ - label, - input_field, - after, - ]; - - let row: Element<_> = row(content) - .align_y(Alignment::Center) - .padding(12) - .into(); - - row + } } } diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index 4f87258..ba17703 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -23,8 +23,8 @@ mod table; pub enum ManagementAppPluginMsgIn { PluginTableMsg(PluginTableMsgIn), PluginPreferenceMsg(PluginPreferencesMsg), - RequestPluginReload, - PluginsReloaded(HashMap), + FetchPlugins, + PluginsFetched(HashMap), RemovePlugin { plugin_id: PluginId }, @@ -131,7 +131,7 @@ impl ManagementAppPluginsState { ) } PluginTableMsgOut::SelectItem(selected_item) => { - Task::perform(async move { selected_item }, ManagementAppPluginMsgOut::SelectedItem) + Task::done(ManagementAppPluginMsgOut::SelectedItem(selected_item)) } PluginTableMsgOut::ToggleShowEntrypoints { plugin_id } => { let plugins = { @@ -142,7 +142,7 @@ impl ManagementAppPluginsState { plugin_data.plugins.clone() }; - self.apply_plugin_reload(plugins); + self.apply_plugin_fetch(plugins); Task::none() } @@ -170,7 +170,7 @@ impl ManagementAppPluginsState { } } } - ManagementAppPluginMsgIn::RequestPluginReload => { + ManagementAppPluginMsgIn::FetchPlugins => { let mut backend_api = backend_api.clone(); Task::perform( @@ -183,8 +183,8 @@ impl ManagementAppPluginsState { |result| handle_backend_error(result, |plugins| ManagementAppPluginMsgOut::PluginsReloaded(plugins)) ) } - ManagementAppPluginMsgIn::PluginsReloaded(plugins) => { - self.apply_plugin_reload(plugins); + ManagementAppPluginMsgIn::PluginsFetched(plugins) => { + self.apply_plugin_fetch(plugins); Task::none() } @@ -220,7 +220,7 @@ impl ManagementAppPluginsState { } } - fn apply_plugin_reload(&mut self, plugins: HashMap) { + fn apply_plugin_fetch(&mut self, plugins: HashMap) { self.preference_user_data = plugins.iter() .map(|(plugin_id, plugin)| { let mut result = vec![]; diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index dcf4054..f3d7cf1 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -26,9 +26,9 @@ bytes.workspace = true walkdir.workspace = true typed-path.workspace = true interprocess.workspace = true +toml.workspace = true # other -toml = "0.8" tantivy = "0.22" git2 = { version = "0.19", features = ["vendored-libgit2", "vendored-openssl"] } tempfile = "3" @@ -40,6 +40,7 @@ arboard = { version = "3.4", features = ["wayland-data-control"] } url = "2.5" ureq = "2.10" vergen-pretty = "0.3" +dark-light = "1.1.1" [features] release = ["gauntlet-common/release"] diff --git a/rust/server/db_migrations/12_settings_theme.sql b/rust/server/db_migrations/12_settings_theme.sql new file mode 100644 index 0000000..8161d56 --- /dev/null +++ b/rust/server/db_migrations/12_settings_theme.sql @@ -0,0 +1 @@ +ALTER TABLE settings_data ADD COLUMN settings JSON DEFAULT NULL; diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index e9a3600..f3a7bfd 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -174,6 +174,18 @@ async fn run_server(frontend_sender: RequestSender, request_data: BackendRequestData) -> anyhow::Result { let response_data = match request_data { + BackendRequestData::Setup => { + let data = application_manager.setup_data().await?; + + BackendResponseData::SetupData { + data, + } + } + BackendRequestData::SetupResponse { global_shortcut_error } => { + application_manager.setup_response(global_shortcut_error).await?; + + BackendResponseData::Nothing + } BackendRequestData::Search { text, render_inline_view } => { let results = application_manager.search(&text, render_inline_view)?; diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index db08220..380102a 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -11,7 +11,7 @@ use sqlx::sqlite::SqliteConnectOptions; use sqlx::types::Json; use typed_path::TypedPathBuf; use uuid::Uuid; -use gauntlet_common::model::{PhysicalKey, PhysicalShortcut, PluginId}; +use gauntlet_common::model::{UiTheme, PhysicalKey, PhysicalShortcut, PluginId}; use gauntlet_common::dirs::Dirs; use crate::model::ActionShortcutKey; use crate::plugins::frecency::{FrecencyItemStats, FrecencyMetaParams}; @@ -214,9 +214,11 @@ pub struct DbPluginActionUserData { } #[derive(sqlx::FromRow)] -pub struct DbSettingsData { +struct DbSettingsDataContainer { #[sqlx(json)] - pub global_shortcut: DbSettingsGlobalShortcutData, + pub global_shortcut: DbSettingsGlobalShortcutData, // separate field because legacy + // #[sqlx(json)] // https://github.com/launchbadge/sqlx/issues/2849 + pub settings: Option>, } #[derive(Debug, Deserialize, Serialize)] @@ -232,6 +234,22 @@ pub struct DbSettingsGlobalShortcutData { pub error: Option } +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct DbSettings { + // none means auto-detect + pub theme: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum DbTheme { + #[serde(rename = "macos_light")] + MacOSLight, + #[serde(rename = "macos_dark")] + MacOSDark, + #[serde(rename = "legacy")] + Legacy +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub enum DbPluginActionShortcutKind { #[serde(rename = "main")] @@ -245,7 +263,7 @@ pub enum DbPluginActionShortcutKind { pub enum DbPluginPreference { #[serde(rename = "number")] Number { - name: Option, // option for db backwards compatibility, in settings id will be shown + name: Option, // optional for db backwards compatibility, in settings id will be shown default: Option, description: String, }, @@ -317,6 +335,8 @@ pub struct DbPluginEntrypointFrecencyStats { pub num_accesses: i32, } +const SETTINGS_DATA_ID: &str = "settings_data"; // only one row in the table + impl DataDbRepository { pub async fn new(dirs: Dirs) -> anyhow::Result { let data_db_file = dirs.data_db_file()?; @@ -847,8 +867,6 @@ impl DataDbRepository { DO UPDATE SET global_shortcut = ?2 "#; - let id = "settings_data"; // only one row in the table - let shortcut_data = match shortcut { None => { DbSettingsGlobalShortcutData { @@ -875,7 +893,7 @@ impl DataDbRepository { }; sqlx::query(sql) - .bind(id) + .bind(SETTINGS_DATA_ID) .bind(Json(shortcut_data)) .execute(&self.pool) .await?; @@ -885,7 +903,7 @@ impl DataDbRepository { pub async fn get_global_shortcut(&self) -> anyhow::Result, Option)>> { // language=SQLite - let data = sqlx::query_as::<_, DbSettingsData>("SELECT * FROM settings_data") + let data = sqlx::query_as::<_, DbSettingsDataContainer>("SELECT * FROM settings_data") .fetch_optional(&self.pool) .await; @@ -915,6 +933,38 @@ impl DataDbRepository { } } + pub async fn get_settings(&self) -> anyhow::Result { + // language=SQLite + let settings = sqlx::query_as::<_, DbSettingsDataContainer>("SELECT * FROM settings_data") + .fetch_optional(&self.pool) + .await?; + + let theme = settings + .map(|data| data.settings) + .flatten() + .unwrap_or_default(); + + Ok(theme.0) + } + + pub async fn set_settings(&self, value: DbSettings) -> anyhow::Result<()> { + // language=SQLite + let sql = r#" + INSERT INTO settings_data (id, global_shortcut, settings) + VALUES(?1, ?2, ?3) + ON CONFLICT (id) + DO UPDATE SET settings = ?2 + "#; + + sqlx::query(sql) + .bind(SETTINGS_DATA_ID) + .bind(Json(value)) + .execute(&self.pool) + .await?; + + Ok(()) + } + pub async fn set_preference_value(&self, plugin_id: String, entrypoint_id: Option, preference_id: String, value: DbPluginPreferenceUserData) -> anyhow::Result<()> { let mut tx = self.pool.begin().await?; diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 6bc9014..ca62438 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -7,7 +7,7 @@ use anyhow::anyhow; use include_dir::{include_dir, Dir}; use tokio::runtime::Handle; -use gauntlet_common::model::{DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, UiPropertyValue, UiRequestData, UiResponseData, UiWidgetId}; +use gauntlet_common::model::{DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, SettingsTheme, UiPropertyValue, UiRequestData, UiResponseData, UiSetupData, UiWidgetId}; use gauntlet_common::rpc::frontend_api::FrontendApi; use gauntlet_common::{settings_env_data_to_string, SettingsEnvData}; use gauntlet_utils::channel::RequestSender; @@ -21,6 +21,7 @@ use crate::plugins::icon_cache::IconCache; use crate::plugins::js::{start_plugin_runtime, AllPluginCommandData, OnePluginCommandData, PluginCommand, PluginPermissions, PluginPermissionsClipboard, PluginRuntimeData}; use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; +use crate::plugins::settings::Settings; use crate::search::SearchIndex; use crate::SETTINGS_ENV; @@ -35,6 +36,8 @@ pub(super) mod frecency; mod clipboard; mod runtime; mod image_gatherer; +mod settings; +mod theme; static BUNDLED_PLUGINS: [(&str, Dir); 1] = [ ("gauntlet", include_dir!("$CARGO_MANIFEST_DIR/../../bundled_plugins/gauntlet/dist")), @@ -51,6 +54,7 @@ pub struct ApplicationManager { frontend_api: FrontendApi, dirs: Dirs, clipboard: Clipboard, + settings: Settings, } impl ApplicationManager { @@ -64,10 +68,11 @@ impl ApplicationManager { let run_status_holder = RunStatusHolder::new(); let search_index = SearchIndex::create_index(frontend_api.clone())?; let clipboard = Clipboard::new()?; + let settings = Settings::new(dirs.clone(), db_repository.clone(), frontend_api.clone())?; let (command_broadcaster, _) = tokio::sync::broadcast::channel::(100); - let manager = Self { + Ok(Self { config_reader, search_index, command_broadcaster, @@ -77,37 +82,25 @@ impl ApplicationManager { icon_cache, frontend_api, clipboard, + settings, dirs - }; + }) + } - match manager.get_global_shortcut().await? { - None => { - let shortcut = if cfg!(target_os = "windows") { - PhysicalShortcut { - physical_key: PhysicalKey::Space, - modifier_shift: false, - modifier_control: false, - modifier_alt: true, - modifier_meta: false, - } - } else { - PhysicalShortcut { - physical_key: PhysicalKey::Space, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: true, - } - }; + pub async fn setup_data(&self) -> anyhow::Result { + let theme = self.settings.effective_theme().await?; + let global_shortcut = self.settings.effective_global_shortcut().await?; - manager.set_global_shortcut(Some(shortcut)).await? - } - Some((shortcut, _)) => { - manager.set_global_shortcut(shortcut).await? - } - }; + Ok(UiSetupData { + theme, + global_shortcut + }) + } - Ok(manager) + pub async fn setup_response(&self, global_shortcut_error: Option) -> anyhow::Result<()> { + self.settings.set_global_shortcut_error(global_shortcut_error).await?; + + Ok(()) } pub fn clear_all_icon_cache_dir(&self) -> anyhow::Result<()> { @@ -276,18 +269,19 @@ impl ApplicationManager { } pub async fn set_global_shortcut(&self, shortcut: Option) -> anyhow::Result<()> { - let err = self.frontend_api.set_global_shortcut(shortcut.clone()).await; - - let db_err = err.as_ref().map_err(|err| format!("{:#}", err)).err(); - - self.db_repository.set_global_shortcut(shortcut, db_err) - .await?; - - err + self.settings.set_global_shortcut(shortcut).await } pub async fn get_global_shortcut(&self) -> anyhow::Result, Option)>> { - self.db_repository.get_global_shortcut().await + self.settings.global_shortcut().await + } + + pub async fn set_theme(&self, theme: SettingsTheme) -> anyhow::Result<()> { + self.settings.set_theme_setting(theme).await + } + + pub async fn get_theme(&self) -> anyhow::Result { + self.settings.theme_setting().await } pub async fn set_preference_value(&self, plugin_id: PluginId, entrypoint_id: Option, preference_id: String, preference_value: PluginPreferenceUserData) -> anyhow::Result<()> { diff --git a/rust/server/src/plugins/settings.rs b/rust/server/src/plugins/settings.rs new file mode 100644 index 0000000..24d2ee4 --- /dev/null +++ b/rust/server/src/plugins/settings.rs @@ -0,0 +1,166 @@ +use crate::plugins::data_db_repository::{DataDbRepository, DbTheme}; +use crate::plugins::theme::{read_theme_file, BundledThemes}; +use anyhow::anyhow; +use dark_light::Mode; +use gauntlet_common::dirs::Dirs; +use gauntlet_common::model::{PhysicalKey, PhysicalShortcut, SettingsTheme, UiTheme}; +use gauntlet_common::rpc::frontend_api::FrontendApi; +use std::env::consts::OS; + +pub struct Settings { + dirs: Dirs, + repository: DataDbRepository, + frontend_api: FrontendApi, + themes: BundledThemes, +} + +impl Settings { + pub fn new(dirs: Dirs, repository: DataDbRepository, frontend_api: FrontendApi) -> anyhow::Result { + Ok(Self { + dirs, + repository, + frontend_api, + themes: BundledThemes::new()? + }) + } + + pub async fn effective_global_shortcut(&self) -> anyhow::Result> { + match self.global_shortcut().await? { + None => { + if cfg!(target_os = "windows") { + Ok(Some(PhysicalShortcut { + physical_key: PhysicalKey::Space, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + })) + } else { + Ok(Some(PhysicalShortcut { + physical_key: PhysicalKey::Space, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: true, + })) + } + } + Some((shortcut, _)) => Ok(shortcut) + } + } + + pub async fn global_shortcut(&self) -> anyhow::Result, Option)>> { + self.repository.get_global_shortcut().await + } + + pub async fn set_global_shortcut(&self, shortcut: Option) -> anyhow::Result<()> { + let err = self.frontend_api.set_global_shortcut(shortcut.clone()).await; + + let db_err = err.as_ref().map_err(|err| format!("{:#}", err)).err(); + + self.repository.set_global_shortcut(shortcut, db_err) + .await?; + + err + } + + pub async fn set_global_shortcut_error(&self, error: Option) -> anyhow::Result<()> { + match self.repository.get_global_shortcut().await? { + None => {} + Some((shortcut, _)) => { + self.repository.set_global_shortcut(shortcut, error) + .await?; + + } + }; + + Ok(()) + } + + pub async fn effective_theme(&self) -> anyhow::Result { + if let Some(theme) = read_theme_file(self.dirs.theme_file()) { + return Ok(theme); + }; + + // TODO config + + let settings = self.repository + .get_settings() + .await?; + + let theme = match &settings.theme { + None => self.autodetect_theme(), + Some(theme) => match theme { + DbTheme::MacOSLight => self.themes.macos_light_theme.clone(), + DbTheme::MacOSDark => self.themes.macos_dark_theme.clone(), + DbTheme::Legacy => self.themes.legacy_theme.clone(), + } + }; + + Ok(theme) + } + + pub async fn theme_setting(&self) -> anyhow::Result { + if let Some(_) = read_theme_file(self.dirs.theme_file()) { + return Ok(SettingsTheme::ThemeFile); + }; + + // TODO config + + let mut settings = self.repository + .get_settings() + .await?; + + match settings.theme { + None => Ok(SettingsTheme::AutoDetect), + Some(DbTheme::MacOSLight) => Ok(SettingsTheme::MacOSLight), + Some(DbTheme::MacOSDark) => Ok(SettingsTheme::MacOSDark), + Some(DbTheme::Legacy) => Ok(SettingsTheme::Legacy), + } + } + + pub async fn set_theme_setting(&self, theme: SettingsTheme) -> anyhow::Result<()> { + + let mut settings = self.repository + .get_settings() + .await?; + + settings.theme = match theme { + SettingsTheme::AutoDetect => None, + SettingsTheme::MacOSLight => Some(DbTheme::MacOSLight), + SettingsTheme::MacOSDark => Some(DbTheme::MacOSDark), + SettingsTheme::Legacy => Some(DbTheme::Legacy), + // these should not be visible in settings ui + SettingsTheme::Config => Err(anyhow!("Unable to set current theme to config"))?, + SettingsTheme::ThemeFile => Err(anyhow!("Unable to set current theme to a file"))? + }; + + let theme = match &settings.theme { + None => self.autodetect_theme(), + Some(theme) => match theme { + DbTheme::MacOSLight => self.themes.macos_light_theme.clone(), + DbTheme::MacOSDark => self.themes.macos_dark_theme.clone(), + DbTheme::Legacy => self.themes.legacy_theme.clone(), + } + }; + + self.repository.set_settings(settings).await?; + + self.frontend_api.set_theme(theme).await?; + + Ok(()) + } + + fn autodetect_theme(&self) -> UiTheme { + match OS { + "macos" => { + match dark_light::detect() { + Mode::Dark => self.themes.macos_dark_theme.clone(), + Mode::Light => self.themes.macos_light_theme.clone(), + Mode::Default => self.themes.macos_dark_theme.clone() + } + } + _ => self.themes.legacy_theme.clone() + } + } +} diff --git a/rust/server/src/plugins/theme.rs b/rust/server/src/plugins/theme.rs new file mode 100644 index 0000000..0ba430d --- /dev/null +++ b/rust/server/src/plugins/theme.rs @@ -0,0 +1,231 @@ +use std::env::consts::OS; +use std::io::ErrorKind; +use std::path::PathBuf; +use anyhow::{anyhow, Context}; +use dark_light::Mode; +use serde::{Deserialize, Serialize}; +use gauntlet_common::dirs::Dirs; +use gauntlet_common::model::{UiTheme, UiThemeColor, UiThemeContent, UiThemeContentBorder, UiThemeMode, UiThemeWindow, UiThemeWindowBorder}; +use gauntlet_common::rpc::frontend_api::FrontendApi; +use crate::plugins::data_db_repository::DataDbRepository; + +pub struct BundledThemes { + pub legacy_theme: UiTheme, + pub macos_dark_theme: UiTheme, + pub macos_light_theme: UiTheme, +} + +const LEGACY_THEME: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../bundled_themes/legacy.toml")); +const MACOS_DARK_THEME: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../bundled_themes/macos_dark.toml")); +const MACOS_LIGHT_THEME: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../bundled_themes/macos_light.toml")); + +impl BundledThemes { + pub fn new() -> anyhow::Result { + Ok(Self { + legacy_theme: parse_theme(LEGACY_THEME).expect("bundled theme should always be valid"), + macos_dark_theme: parse_theme(MACOS_DARK_THEME).expect("bundled theme should always be valid"), + macos_light_theme: parse_theme(MACOS_LIGHT_THEME).expect("bundled theme should always be valid"), + }) + } +} + +pub fn convert_theme(config_theme: ConfigTheme) -> anyhow::Result { + let [background_100, background_200, background_300, background_400] = config_theme.background; + let [text_100, text_200, text_300, text_400] = config_theme.text; + + Ok(UiTheme { + mode: match config_theme.mode { + ConfigThemeMode::Light => UiThemeMode::Light, + ConfigThemeMode::Dark => UiThemeMode::Dark + }, + background: [ + convert_complex_color(background_100)?, + convert_complex_color(background_200)?, + convert_complex_color(background_300)?, + convert_complex_color(background_400)? + ], + text: [ + convert_complex_color(text_100)?, + convert_complex_color(text_200)?, + convert_complex_color(text_300)?, + convert_complex_color(text_400)?, + ], + window: UiThemeWindow { + border: UiThemeWindowBorder { + radius: config_theme.window.border.radius, + width: config_theme.window.border.width, + color: convert_complex_color(config_theme.window.border.color)?, + }, + }, + content: UiThemeContent { + border: UiThemeContentBorder { + radius: config_theme.content.border.radius, + }, + }, + }) +} + +fn convert_complex_color(color: ConfigThemeColor) -> anyhow::Result { + match color { + ConfigThemeColor::String(value) => convert_color(value, true), + ConfigThemeColor::Object { color, alpha } => { + + if !(0.0..=1.0).contains(&alpha) { + Err(anyhow!("Alpha component must be on [0, 1] range"))?; + } + + let mut color = convert_color(color, false)?; + + color.a = alpha; + + Ok(color) + } + } +} + +fn convert_color(color: String, allow_alpha: bool) -> anyhow::Result { + if !color.starts_with("#") { + Err(anyhow!("Colors have to start with #"))?; + } + + let hex = color.strip_prefix('#').expect("validated just above"); + + let parse_channel = |from: usize, to: usize| -> anyhow::Result { + let num = usize::from_str_radix(&hex[from..=to], 16) + .context("Unable to parse as hex number")?; + let num = num as f32 / 255.0; + + // If we only got half a byte (one letter), expand it into a full byte (two letters) + Ok(if from == to { num + num * 16.0 } else { num }) + }; + + let color = match hex.len() { + 3 => UiThemeColor { + r: parse_channel(0, 0)?, + g: parse_channel(1, 1)?, + b: parse_channel(2, 2)?, + a: 1.0, + }, + 4 => { + if allow_alpha { + UiThemeColor { + r: parse_channel(0, 0)?, + g: parse_channel(1, 1)?, + b: parse_channel(2, 2)?, + a: parse_channel(3, 3)?, + } + } else { + Err(anyhow!("alpha channel is not allowed here"))? + } + }, + 6 => UiThemeColor { + r: parse_channel(0, 1)?, + g: parse_channel(2, 3)?, + b: parse_channel(4, 5)?, + a: 1.0, + }, + 8 => { + if allow_alpha { + UiThemeColor { + r: parse_channel(0, 1)?, + g: parse_channel(2, 3)?, + b: parse_channel(4, 5)?, + a: parse_channel(6, 7)?, + } + } else { + Err(anyhow!("alpha channel is not allowed here"))? + } + }, + _ => Err(anyhow!("invalid length of a color string"))?, + }; + + Ok(color) +} + +pub fn parse_theme(value: &str) -> anyhow::Result { + let value = toml::from_str::(value) + .context("Unable to parse theme file")?; + + match convert_theme(value) { + Ok(value) => Ok(value), + Err(err) => Err(err.context("Unable to parse theme file")) + } +} + +pub fn read_theme_file(theme_file: PathBuf) -> Option { + match std::fs::read_to_string(&theme_file) { + Ok(value) => { + match parse_theme(&value) { + Ok(value) => Some(value), + Err(err) => { + tracing::warn!("Unable to parse theme file: {:?} - {:?}", theme_file, err); + None + } + } + }, + Err(err) => { + match err.kind() { + ErrorKind::NotFound => { + tracing::debug!("No theme file was found"); + None + }, + err @ _ => { + tracing::warn!("Unable to read theme file: {}", err); + None + }, + } + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ConfigThemeMode { + #[serde(rename = "light")] + Light, + #[serde(rename = "dark")] + Dark +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ConfigThemeColor { + String(String), + Object { + color: String, + alpha: f32 + } +} + +pub type ConfigThemeColorPalette = [ConfigThemeColor; 4]; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigThemeWindow { + pub border: ConfigThemeWindowBorder, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigThemeWindowBorder { + pub radius: f32, + pub width: f32, + pub color: ConfigThemeColor, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigThemeContent { + pub border: ConfigThemeContentBorder, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigThemeContentBorder { + pub radius: f32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigTheme { + pub mode: ConfigThemeMode, + // value of tint/tones/shades/whatever you have, from lower to higher + pub background: ConfigThemeColorPalette, + pub text: ConfigThemeColorPalette, + pub window: ConfigThemeWindow, + pub content: ConfigThemeContent, +} diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index fa31ae1..12876a7 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; use gauntlet_common::{settings_env_data_to_string, SettingsEnvData}; -use gauntlet_common::model::{DownloadStatus, EntrypointId, PluginId, PluginPreferenceUserData, SettingsPlugin, UiPropertyValue, SearchResult, UiWidgetId, PhysicalKey, PhysicalShortcut, LocalSaveData}; +use gauntlet_common::model::{DownloadStatus, EntrypointId, PluginId, PluginPreferenceUserData, SettingsPlugin, UiPropertyValue, SearchResult, UiWidgetId, PhysicalKey, PhysicalShortcut, LocalSaveData, SettingsTheme}; use gauntlet_common::rpc::backend_server::BackendServer; use crate::plugins::ApplicationManager; @@ -86,6 +86,14 @@ impl BackendServer for BackendServerImpl { Ok(result) } + async fn set_theme(&self, theme: SettingsTheme) -> anyhow::Result<()> { + self.application_manager.set_theme(theme).await + } + + async fn get_theme(&self) -> anyhow::Result { + self.application_manager.get_theme().await + } + async fn set_preference_value(&self, plugin_id: PluginId, entrypoint_id: Option, preference_id: String, preference_value: PluginPreferenceUserData) -> anyhow::Result<()> { let result = self.application_manager.set_preference_value(plugin_id, entrypoint_id, preference_id, preference_value) .await; diff --git a/schema/backend.proto b/schema/backend.proto index 2aec6a4..89c7f17 100644 --- a/schema/backend.proto +++ b/schema/backend.proto @@ -22,6 +22,9 @@ service RpcBackend { rpc SetGlobalShortcut (RpcSetGlobalShortcutRequest) returns (RpcSetGlobalShortcutResponse); rpc GetGlobalShortcut (RpcGetGlobalShortcutRequest) returns (RpcGetGlobalShortcutResponse); + rpc SetTheme (RpcSetThemeRequest) returns (RpcSetThemeResponse); + rpc GetTheme (RpcGetThemeRequest) returns (RpcGetThemeResponse); + rpc DownloadPlugin (RpcDownloadPluginRequest) returns (RpcDownloadPluginResponse); rpc DownloadStatus (RpcDownloadStatusRequest) returns (RpcDownloadStatusResponse); @@ -91,6 +94,20 @@ message RpcGetGlobalShortcutResponse { optional string error = 2; } +message RpcSetThemeRequest { + string theme = 1; +} + +message RpcSetThemeResponse { +} + +message RpcGetThemeRequest { +} + +message RpcGetThemeResponse { + string theme = 1; +} + message RpcSetPreferenceValueRequest { string plugin_id = 1; string entrypoint_id = 2; From 27e60d4bc52fe3b022217577dfdd8501e2d54ba9 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 6 Jan 2025 19:06:30 +0100 Subject: [PATCH 265/540] Fix build --- Cargo.lock | 51 --------------------------------------------------- Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40f7f9f..0c51ffb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,12 +146,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "aliasable" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" - [[package]] name = "aligned-vec" version = "0.5.0" @@ -5367,7 +5361,6 @@ dependencies = [ "iced_runtime", "num-traits", "once_cell", - "ouroboros", "rustc-hash 2.1.0", "thiserror 1.0.69", "unicode-segmentation", @@ -7478,31 +7471,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ouroboros" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "944fa20996a25aded6b4795c6d63f10014a7a83f8be9828a11860b08c5fc4a67" -dependencies = [ - "aliasable", - "ouroboros_macro", - "static_assertions", -] - -[[package]] -name = "ouroboros_macro" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39b0deead1528fd0e5947a8546a9642a9777c25f6e1e26f34c97b204bbb465bd" -dependencies = [ - "heck 0.4.1", - "itertools 0.12.1", - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.90", -] - [[package]] name = "outref" version = "0.1.0" @@ -8130,19 +8098,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", - "version_check", - "yansi", -] - [[package]] name = "profiling" version = "1.0.16" @@ -12822,12 +12777,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - [[package]] name = "yazi" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index fbc19bc..77675db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ edition = "2021" [workspace.dependencies] # iced #iced = { version = "0.13.99", features = ["tokio", "lazy", "advanced", "image"] } -iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", default-features = false, features = ["webgl", "tokio", "lazy", "advanced", "image"] } +iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", default-features = false, features = ["wgpu", "tokio", "advanced", "image"] } #iced_aw = { version = "0.11.99", features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } iced_aw = { git = "https://github.com/project-gauntlet/iced_aw.git", branch = "gauntlet-0.13", default-features = false, features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } #iced_table = "0.13.99" From a5d69e8d94dfc9e512be740e69e8d93750878d7e Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:39:54 +0100 Subject: [PATCH 266/540] Add "show all opened windows" view endpoint to bundled plugin --- Cargo.toml | 2 +- bundled_plugins/gauntlet/gauntlet.toml | 7 +++ bundled_plugins/gauntlet/src/applications.tsx | 11 ++--- .../gauntlet/src/window/shared.tsx | 15 +++---- .../gauntlet/src/window/wayland.ts | 9 +--- bundled_plugins/gauntlet/src/window/x11.ts | 27 ++++++------ bundled_plugins/gauntlet/src/windows.tsx | 43 +++++++++++++++++++ 7 files changed, 75 insertions(+), 39 deletions(-) create mode 100644 bundled_plugins/gauntlet/src/windows.tsx diff --git a/Cargo.toml b/Cargo.toml index 77675db..86b9f58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ edition = "2021" [workspace.dependencies] # iced -#iced = { version = "0.13.99", features = ["tokio", "lazy", "advanced", "image"] } +#iced = { version = "0.13.99", features = ["wgpu", "tokio", "lazy", "advanced", "image"] } iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", default-features = false, features = ["wgpu", "tokio", "advanced", "image"] } #iced_aw = { version = "0.11.99", features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } iced_aw = { git = "https://github.com/project-gauntlet/iced_aw.git", branch = "gauntlet-0.13", default-features = false, features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } diff --git a/bundled_plugins/gauntlet/gauntlet.toml b/bundled_plugins/gauntlet/gauntlet.toml index 74d8d72..8501ab5 100644 --- a/bundled_plugins/gauntlet/gauntlet.toml +++ b/bundled_plugins/gauntlet/gauntlet.toml @@ -9,6 +9,13 @@ path = 'src/applications.tsx' type = 'entrypoint-generator' description = 'Run installed applications from your system' +[[entrypoint]] +id = 'windows' +name = 'All Open Windows' +path = 'src/windows.tsx' +type = 'view' +description = 'Show all open windows' + [[entrypoint]] id = 'settings' name = 'Gauntlet Settings' diff --git a/bundled_plugins/gauntlet/src/applications.tsx b/bundled_plugins/gauntlet/src/applications.tsx index 9073502..752024c 100644 --- a/bundled_plugins/gauntlet/src/applications.tsx +++ b/bundled_plugins/gauntlet/src/applications.tsx @@ -15,13 +15,12 @@ import { macos_settings_pre_13, macos_system_applications } from "gauntlet:bridge/internal-macos"; -import { applicationAccessories, applicationActions, OpenWindowData } from "./window/shared"; +import { applicationAccessories, applicationActions } from "./window/shared"; import { applicationEventLoopX11, focusX11Window } from "./window/x11"; import { applicationEventLoopWayland, focusWaylandWindow } from "./window/wayland"; import { windows_app_from_path, windows_application_dirs, windows_open_application } from "gauntlet:bridge/internal-windows"; export default async function Applications({ add, remove, get, getAll }: GeneratorProps): Promise void)> { - const openWindows: Record = {}; switch (current_os()) { case "linux": { @@ -38,9 +37,8 @@ export default async function Applications({ add, remove, get, getAll }: Generat linux_open_application(id) }, focusWaylandWindow, - openWindows ), - accessories: applicationAccessories(id, openWindows), + accessories: applicationAccessories(id), icon: data.icon, // TODO lazy icons "__linux__": { startupWmClass: data.startup_wm_class, @@ -56,9 +54,8 @@ export default async function Applications({ add, remove, get, getAll }: Generat linux_open_application(id) }, focusX11Window, - openWindows, ), - accessories: applicationAccessories(id, openWindows), + accessories: applicationAccessories(id), icon: data.icon, // TODO lazy icons "__linux__": { startupWmClass: data.startup_wm_class, @@ -74,7 +71,6 @@ export default async function Applications({ add, remove, get, getAll }: Generat if (wayland()) { try { applicationEventLoopWayland( - openWindows, focusWaylandWindow, add, get, @@ -86,7 +82,6 @@ export default async function Applications({ add, remove, get, getAll }: Generat } else { try { applicationEventLoopX11( - openWindows, focusX11Window, add, get, diff --git a/bundled_plugins/gauntlet/src/window/shared.tsx b/bundled_plugins/gauntlet/src/window/shared.tsx index 6f5f9b0..789a647 100644 --- a/bundled_plugins/gauntlet/src/window/shared.tsx +++ b/bundled_plugins/gauntlet/src/window/shared.tsx @@ -8,11 +8,12 @@ export type OpenWindowData = { appId: string } +export const openWindows: Record = {}; + export function applicationActions( id: string, openApplication: () => void, focusWindow: (windowId: string) => void, - openWindows: Record ): GeneratedCommandAction[] { const appWindows = Object.entries(openWindows) .filter(([_, windowData]) => windowData.appId == id) @@ -68,7 +69,7 @@ export function applicationActions( } } -export function applicationAccessories(id: string, openWindows: Record): GeneratedCommandAccessory[] { +export function applicationAccessories(id: string): GeneratedCommandAccessory[] { const appWindows = Object.entries(openWindows) .filter(([_, windowData]) => windowData.appId == id) @@ -88,7 +89,6 @@ export function addOpenWindow( generatedEntrypoint: GeneratedCommand, windowId: string, windowTitle: string, - openWindows: Record, openApplication: () => void, focusWindow: (windowId: string) => void, add: (id: string, data: GeneratedCommand) => void, @@ -102,14 +102,13 @@ export function addOpenWindow( add(appId, { ...generatedEntrypoint, - actions: applicationActions(appId, openApplication, focusWindow, openWindows), - accessories: applicationAccessories(appId, openWindows) + actions: applicationActions(appId, openApplication, focusWindow), + accessories: applicationAccessories(appId) }) } } export function deleteOpenWindow( - openWindows: Record, windowId: string, openApplication: (appId: string) => (() => void), focusWindow: (windowId: string) => void, @@ -125,8 +124,8 @@ export function deleteOpenWindow( if (generatedEntrypoint) { add(openWindow.appId, { ...generatedEntrypoint, - actions: applicationActions(openWindow.appId, openApplication(openWindow.appId), focusWindow, openWindows), - accessories: applicationAccessories(openWindow.appId, openWindows) + actions: applicationActions(openWindow.appId, openApplication(openWindow.appId), focusWindow), + accessories: applicationAccessories(openWindow.appId) }) } } diff --git a/bundled_plugins/gauntlet/src/window/wayland.ts b/bundled_plugins/gauntlet/src/window/wayland.ts index 6251689..1c4ad93 100644 --- a/bundled_plugins/gauntlet/src/window/wayland.ts +++ b/bundled_plugins/gauntlet/src/window/wayland.ts @@ -1,4 +1,4 @@ -import { addOpenWindow, deleteOpenWindow, openLinuxApplication, OpenWindowData } from "./shared"; +import { addOpenWindow, deleteOpenWindow, openLinuxApplication } from "./shared"; import { GeneratedCommand } from "@project-gauntlet/api/helpers"; import { linux_wayland_focus_window, application_wayland_pending_event } from "gauntlet:bridge/internal-linux"; @@ -8,7 +8,6 @@ export function focusWaylandWindow(windowId: string) { } export function applicationEventLoopWayland( - openWindows: Record, focusWindow: (windowId: string) => void, add: (id: string, data: GeneratedCommand) => void, get: (id: string) => GeneratedCommand | undefined, @@ -33,7 +32,7 @@ export function applicationEventLoopWayland( case "WindowClosed": { delete knownWindows[applicationEvent.window_id] - deleteOpenWindow(openWindows, applicationEvent.window_id, openLinuxApplication, focusWindow, get, add) + deleteOpenWindow(applicationEvent.window_id, openLinuxApplication, focusWindow, get, add) break; } @@ -52,7 +51,6 @@ export function applicationEventLoopWayland( windowId, windowAppId, windowTitle, - openWindows, focusWindow, add, get, @@ -78,7 +76,6 @@ export function applicationEventLoopWayland( windowId, windowAppId, windowTitle, - openWindows, focusWindow, add, get, @@ -97,7 +94,6 @@ function addOpenWindowWayland( windowId: string, windowAppId: string, windowTitle: string, - openWindows: Record, focusWindow: (windowId: string) => void, add: (id: string, data: GeneratedCommand) => void, get: (id: string) => GeneratedCommand | undefined, @@ -130,7 +126,6 @@ function addOpenWindowWayland( generatedEntrypoint, windowId, windowTitle, - openWindows, openLinuxApplication(appId), focusWindow, add, diff --git a/bundled_plugins/gauntlet/src/window/x11.ts b/bundled_plugins/gauntlet/src/window/x11.ts index 16e0b06..4278f99 100644 --- a/bundled_plugins/gauntlet/src/window/x11.ts +++ b/bundled_plugins/gauntlet/src/window/x11.ts @@ -1,5 +1,5 @@ import { GeneratedCommand } from "@project-gauntlet/api/helpers"; -import { addOpenWindow, deleteOpenWindow, openLinuxApplication, OpenWindowData } from "./shared"; +import { addOpenWindow, deleteOpenWindow, openLinuxApplication } from "./shared"; import { application_x11_pending_event, linux_x11_focus_window } from "gauntlet:bridge/internal-linux"; export type X11WindowData = { @@ -36,7 +36,6 @@ export function focusX11Window(windowId: string) { } export function applicationEventLoopX11( - openWindows: Record, focusWindow: (windowId: string) => void, add: (id: string, data: GeneratedCommand) => void, get: (id: string) => GeneratedCommand | undefined, @@ -87,7 +86,7 @@ export function applicationEventLoopX11( case "DestroyNotify": { delete windows[applicationEvent.id] - deleteOpenWindow(openWindows, applicationEvent.id, openLinuxApplication, focusWindow, get, add) + deleteOpenWindow(applicationEvent.id, openLinuxApplication, focusWindow, get, add) break; } @@ -96,7 +95,7 @@ export function applicationEventLoopX11( if (window) { window.mapped = true; - validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -106,7 +105,7 @@ export function applicationEventLoopX11( if (window) { window.mapped = false; - deleteOpenWindow(openWindows, applicationEvent.id, openLinuxApplication, focusWindow, get, add) + deleteOpenWindow(applicationEvent.id, openLinuxApplication, focusWindow, get, add) } break; @@ -117,7 +116,7 @@ export function applicationEventLoopX11( if (window) { window.mapped = true; - validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -127,7 +126,7 @@ export function applicationEventLoopX11( if (window) { window.title = applicationEvent.title; - validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -138,7 +137,7 @@ export function applicationEventLoopX11( window.class = applicationEvent.class; window.instance = applicationEvent.instance; - validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -148,7 +147,7 @@ export function applicationEventLoopX11( if (window) { window.windowGroup = applicationEvent.window_group; - validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -158,7 +157,7 @@ export function applicationEventLoopX11( if (window) { window.protocols = applicationEvent.protocols; - validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -168,7 +167,7 @@ export function applicationEventLoopX11( if (window) { window.transientFor = applicationEvent.transient_for; - validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -178,7 +177,7 @@ export function applicationEventLoopX11( if (window) { window.windowTypes = applicationEvent.window_types; - validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -188,7 +187,7 @@ export function applicationEventLoopX11( if (window) { window.desktopFileName = applicationEvent.desktop_file_name; - validateAndAddOpenWindow(window, openWindows, windows, openLinuxApplication, focusWindow, add, getAll) + validateAndAddOpenWindow(window, windows, openLinuxApplication, focusWindow, add, getAll) } break; @@ -200,7 +199,6 @@ export function applicationEventLoopX11( function validateAndAddOpenWindow( window: X11WindowData, - openWindows: Record, windows: Record, openApplication: (appId: string) => (() => void), focusWindow: (windowId: string) => void, @@ -242,7 +240,6 @@ function validateAndAddOpenWindow( generatedEntrypoint, window.id, window.title, - openWindows, openApplication(appId), focusWindow, add, diff --git a/bundled_plugins/gauntlet/src/windows.tsx b/bundled_plugins/gauntlet/src/windows.tsx new file mode 100644 index 0000000..c521320 --- /dev/null +++ b/bundled_plugins/gauntlet/src/windows.tsx @@ -0,0 +1,43 @@ +import React, { ReactElement } from "react"; +import { List } from "@project-gauntlet/api/components"; +import { openWindows } from "./window/shared"; +import { current_os, wayland } from "gauntlet:bridge/internal-all"; +import { focusWaylandWindow } from "./window/wayland"; +import { focusX11Window } from "./window/x11"; + +export default function Windows(): ReactElement { + switch (current_os()) { + case "linux": { + if (wayland()) { + return ( + focusWaylandWindow(windowId)}/> + ) + } else { + return ( + focusX11Window(windowId)}/> + ) + } + } + default: { + return ( + + + + ) + } + } +} + +function ListOfWindows({ focus }: { focus: (windowId: string) => void }) { + return ( + + { + Object.entries(openWindows) + .map(([_, window]) => ( + { focus(window.id) }}/> + ) + ) + } + + ) +} \ No newline at end of file From 6b0e3fa39bab397e71fa3c9ad172b8b945005ef7 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 6 Jan 2025 22:10:40 +0100 Subject: [PATCH 267/540] Add generator entrypoint name in addition to plugin name to main view search results --- rust/client/src/ui/search_list.rs | 10 ++++++++-- rust/common/src/model.rs | 1 + rust/server/src/plugins/js.rs | 12 ++++++++++++ rust/server/src/search.rs | 4 ++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index ad0b4c6..8419270 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -35,11 +35,17 @@ pub fn search_list<'a>( .width(Length::Fill) .into(); - let sub_text: Element<_> = text(&search_result.plugin_name) + let sub_text = match &search_result.entrypoint_generator_name { + None => &search_result.plugin_name, + Some(entrypoint_generator_name) => &format!("{} - {}", entrypoint_generator_name, &search_result.plugin_name) + }; + + let sub_text: Element<_> = text(sub_text.clone()) .shaping(Shaping::Advanced) .themed(TextStyle::MainListItemSubtext); + let sub_text: Element<_> = container(sub_text) - .themed(ContainerStyle::MainListItemSubText); // FIXME find a way to set padding based on whether the scroll bar is visible + .themed(ContainerStyle::MainListItemSubText); // FIXME find a way to set padding based on whether the scroll bar is visible let mut button_content = vec![]; diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index c9c5ed1..7e8f6a6 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -111,6 +111,7 @@ pub struct SearchResult { pub plugin_name: String, pub entrypoint_id: EntrypointId, pub entrypoint_name: String, + pub entrypoint_generator_name: Option, pub entrypoint_icon: Option, pub entrypoint_type: SearchResultEntrypointType, pub entrypoint_actions: Vec, diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index aa09b43..aa91d6c 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -722,6 +722,11 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { shortcuts.insert(id.clone(), entrypoint_shortcuts); } + let generator_names: HashMap<_, _> = entrypoints.iter() + .filter(|entrypoint| matches!(db_entrypoint_from_str(&entrypoint.entrypoint_type), DbPluginEntrypointType::EntrypointGenerator)) + .map(|entrypoint| (entrypoint.id.clone(), entrypoint.name.clone())) + .collect(); + let mut generated_search_items = generated_commands.into_iter() .map(|item| { let entrypoint_icon_path = match item.entrypoint_icon { @@ -767,6 +772,10 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { }) .collect(); + let entrypoint_generator_name = generator_names + .get(&item.generator_entrypoint_id) + .map(|name| name.to_string()); + Ok(SearchIndexItem { entrypoint_type: SearchResultEntrypointType::Generated, entrypoint_id: EntrypointId::from_string(item.entrypoint_id), @@ -775,6 +784,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { entrypoint_frecency, entrypoint_actions, entrypoint_accessories, + entrypoint_generator_name, }) }) .collect::>>()?; @@ -817,6 +827,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { Ok(Some(SearchIndexItem { entrypoint_type: SearchResultEntrypointType::Command, entrypoint_name: entrypoint.name, + entrypoint_generator_name: None, entrypoint_id, entrypoint_icon_path, entrypoint_frecency, @@ -828,6 +839,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { Ok(Some(SearchIndexItem { entrypoint_type: SearchResultEntrypointType::View, entrypoint_name: entrypoint.name, + entrypoint_generator_name: None, entrypoint_id, entrypoint_icon_path, entrypoint_frecency, diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index 47ee13d..fd6391b 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -25,6 +25,7 @@ pub struct SearchIndex { } struct EntrypointData { + entrypoint_generator_name: Option, entrypoint_type: SearchResultEntrypointType, icon_path: Option, frecency: f64, @@ -47,6 +48,7 @@ enum EntrypointActionType { pub struct SearchIndexItem { pub entrypoint_type: SearchResultEntrypointType, pub entrypoint_name: String, + pub entrypoint_generator_name: Option, pub entrypoint_id: EntrypointId, pub entrypoint_icon_path: Option, pub entrypoint_frecency: f64, @@ -163,6 +165,7 @@ impl SearchIndex { .collect(); let data = EntrypointData { + entrypoint_generator_name: item.entrypoint_generator_name, entrypoint_type: item.entrypoint_type, icon_path: item.entrypoint_icon_path, frecency: item.entrypoint_frecency, @@ -288,6 +291,7 @@ impl SearchIndex { let result_item = SearchResult { entrypoint_type: entrypoint_data.entrypoint_type.clone(), entrypoint_name, + entrypoint_generator_name: entrypoint_data.entrypoint_generator_name.clone(), entrypoint_id, entrypoint_icon: entrypoint_data.icon_path.clone(), plugin_name, From 63e16428236904a8f84c1373e2a3267f11a614ed Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 7 Jan 2025 19:20:22 +0100 Subject: [PATCH 268/540] Prevent from changing theme settings if theme file exist --- rust/management_client/src/views/general.rs | 47 ++++++++++++++++----- rust/server/src/plugins/theme.rs | 2 +- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/rust/management_client/src/views/general.rs b/rust/management_client/src/views/general.rs index a819ca2..084e017 100644 --- a/rust/management_client/src/views/general.rs +++ b/rust/management_client/src/views/general.rs @@ -120,18 +120,43 @@ impl ManagementAppGeneralState { Some(self.shortcut_capture_after()) ); - let theme_items = [ - SettingsTheme::AutoDetect, - SettingsTheme::MacOSLight, - SettingsTheme::MacOSDark, - SettingsTheme::Legacy, - ]; - let theme_field: Element<_> = pick_list( - theme_items, - Some(self.theme.clone()), - move |item| ManagementAppGeneralMsgIn::ThemeChanged(item), - ).into(); + let theme_field = match &self.theme { + SettingsTheme::ThemeFile => { + let theme_field: Element<_> = text("Unable to change because theme config file is present ") + .shaping(Shaping::Advanced) + .align_x(Horizontal::Center) + .width(Length::Fill) + .into(); + + theme_field + } + SettingsTheme::Config => { + let theme_field: Element<_> = text("Unable to change because value is defined in config") + .shaping(Shaping::Advanced) + .align_x(Horizontal::Center) + .width(Length::Fill) + .into(); + + theme_field + } + _ => { + let theme_items = [ + SettingsTheme::AutoDetect, + SettingsTheme::MacOSLight, + SettingsTheme::MacOSDark, + SettingsTheme::Legacy, + ]; + + let theme_field: Element<_> = pick_list( + theme_items, + Some(self.theme.clone()), + move |item| ManagementAppGeneralMsgIn::ThemeChanged(item), + ).into(); + + theme_field + } + }; let theme_field: Element<_> = container(theme_field) .width(Length::Fill) diff --git a/rust/server/src/plugins/theme.rs b/rust/server/src/plugins/theme.rs index 0ba430d..1cae175 100644 --- a/rust/server/src/plugins/theme.rs +++ b/rust/server/src/plugins/theme.rs @@ -158,7 +158,7 @@ pub fn read_theme_file(theme_file: PathBuf) -> Option { match parse_theme(&value) { Ok(value) => Some(value), Err(err) => { - tracing::warn!("Unable to parse theme file: {:?} - {:?}", theme_file, err); + tracing::warn!("Unable to parse theme file: {:?} - {:#}", theme_file, err); None } } From 20fa211d05253784ce630a7dccfdf869a2898189 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:07:49 +0100 Subject: [PATCH 269/540] Tweak styling of action panel --- dev_plugin/src/detail-view.tsx | 2 +- rust/client/src/ui/theme/container.rs | 4 ++++ rust/client/src/ui/theme/mod.rs | 10 ++++++-- rust/client/src/ui/theme/text.rs | 4 ++++ rust/client/src/ui/widget.rs | 33 ++++++++++----------------- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/dev_plugin/src/detail-view.tsx b/dev_plugin/src/detail-view.tsx index fc483f9..f3364c9 100644 --- a/dev_plugin/src/detail-view.tsx +++ b/dev_plugin/src/detail-view.tsx @@ -83,7 +83,7 @@ export default function DetailView(): ReactElement { return ( + { diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 341be64..07600c6 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -6,6 +6,7 @@ use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget} pub enum ContainerStyle { ActionPanel, ActionPanelTitle, + ActionSectionTitle, ActionShortcutModifier, ActionShortcutModifiersInit, // "init" means every item on list except last one ContentCodeBlock, @@ -259,6 +260,9 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau ContainerStyle::ActionPanelTitle => { self.padding(theme.action_panel_title.padding.to_iced()) } + ContainerStyle::ActionSectionTitle => { + self.padding(theme.action_section_title.padding.to_iced()) + } ContainerStyle::ActionShortcutModifier => { self.class(ContainerStyleInner::ActionShortcutModifier) .padding(theme.action_shortcut_modifier.padding.to_iced()) diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index e377c51..b70dab7 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -29,7 +29,8 @@ pub struct GauntletComplexTheme { popup: ThemeRoot, action: ThemeButton, action_panel: ThemePaddingBackgroundColor, - action_panel_title: ThemePaddingOnly, + action_panel_title: ThemePaddingTextColor, + action_section_title: ThemePaddingTextColor, action_shortcut: ThemePaddingOnly, action_shortcut_modifier: ThemeActionShortcutModifier, content_code_block: ThemePaddingOnly, @@ -178,8 +179,13 @@ impl GauntletComplexTheme { padding: padding_all(8.0), background_color: background_400, }, - action_panel_title: ThemePaddingOnly { + action_panel_title: ThemePaddingTextColor { padding: padding(2.0, 8.0, 4.0, 8.0), + text_color: text_300, + }, + action_section_title: ThemePaddingTextColor { + padding: padding(8.0, 8.0, 4.0, 8.0), + text_color: text_300, }, action: ThemeButton { padding: padding_all(8.0), diff --git a/rust/client/src/ui/theme/text.rs b/rust/client/src/ui/theme/text.rs index 2e390d8..1ac925c 100644 --- a/rust/client/src/ui/theme/text.rs +++ b/rust/client/src/ui/theme/text.rs @@ -8,6 +8,7 @@ pub enum TextStyle { #[default] Default, // TODO is this used? + ActionSectionTitle, EmptyViewSubtitle, ListItemSubtitle, ListSectionTitle, @@ -61,6 +62,9 @@ impl text::Catalog for GauntletComplexTheme { fn style(&self, class: &Self::Class<'_>) -> Style { match class { TextStyle::Default => Default::default(), + TextStyle::ActionSectionTitle => Style { + color: Some(self.action_section_title.text_color), + }, TextStyle::EmptyViewSubtitle => Style { color: Some(self.empty_view_subtitle.text_color), }, diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 5f26629..69ae275 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -2178,6 +2178,7 @@ fn convert_action_panel(action_panel: &Option, action_shortcu } fn render_action_panel_items<'a, T: 'a + Clone>( + root: bool, title: Option, items: Vec, action_panel_focus_index: Option, @@ -2193,27 +2194,24 @@ fn render_action_panel_items<'a, T: 'a + Clone>( weight: Weight::Bold, ..Font::DEFAULT }) - .into(); + .themed(TextStyle::ActionSectionTitle); let text = container(text) - .themed(ContainerStyle::ActionPanelTitle); + .themed(if root { ContainerStyle::ActionPanelTitle } else { ContainerStyle::ActionSectionTitle }); columns.push(text) - } + } else { + if !root { + let separator: Element<_> = horizontal_rule(1) + .themed(RuleStyle::ActionPanel); - let mut place_separator = false; + columns.push(separator); + } + } for item in items { match item { ActionPanelItem::Action { label, widget_id, physical_shortcut } => { - if place_separator { - let separator: Element<_> = horizontal_rule(1) - .themed(RuleStyle::ActionPanel); - - columns.push(separator); - - place_separator = false; - } let physical_shortcut = match index_counter.get() { 0 => Some(PhysicalShortcut { // primary @@ -2274,18 +2272,11 @@ fn render_action_panel_items<'a, T: 'a + Clone>( columns.push(content); } ActionPanelItem::ActionSection { title, items } => { - let separator: Element<_> = horizontal_rule(1) - .themed(RuleStyle::ActionPanel); - - columns.push(separator); - - let content = render_action_panel_items(title, items, action_panel_focus_index, on_action_click, index_counter); + let content = render_action_panel_items(false, title, items, action_panel_focus_index, on_action_click, index_counter); for content in content { columns.push(content); } - - place_separator = true; } }; } @@ -2298,7 +2289,7 @@ fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T, ACTION>( on_action_click: F, action_panel_scroll_handle: &ScrollHandle, ) -> Element<'a, T> { - let columns = render_action_panel_items(action_panel.title, action_panel.items, action_panel_scroll_handle.index, &on_action_click, &Cell::new(0)); + let columns = render_action_panel_items(true, action_panel.title, action_panel.items, action_panel_scroll_handle.index, &on_action_click, &Cell::new(0)); let actions: Element<_> = column(columns) .into(); From b2094be5458dec5aa1557fa28795b81f76ad5757 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:21:56 +0100 Subject: [PATCH 270/540] Finish renaming Generated Command to Generated Entrypoint --- bundled_plugins/gauntlet/src/applications.tsx | 6 +- .../gauntlet/src/window/shared.tsx | 14 ++-- .../gauntlet/src/window/wayland.ts | 14 ++-- bundled_plugins/gauntlet/src/window/x11.ts | 12 +-- js/api/src/helpers.ts | 24 +++--- js/core/src/core.tsx | 8 +- js/core/src/entrypoint-generator.ts | 74 +++++++++---------- js/core/src/search-index.ts | 4 +- js/typings/index.d.ts | 14 ++-- rust/client/src/ui/mod.rs | 14 ++-- rust/common/src/model.rs | 2 +- rust/common/src/rpc/backend_api.rs | 4 +- rust/plugin_runtime/src/api.rs | 6 +- rust/plugin_runtime/src/events.rs | 2 +- rust/plugin_runtime/src/model.rs | 2 +- rust/plugin_runtime/src/search.rs | 4 +- rust/server/src/lib.rs | 4 +- rust/server/src/model.rs | 2 +- rust/server/src/plugins/js.rs | 16 ++-- rust/server/src/plugins/mod.rs | 4 +- schema/backend.proto | 4 +- 21 files changed, 117 insertions(+), 117 deletions(-) diff --git a/bundled_plugins/gauntlet/src/applications.tsx b/bundled_plugins/gauntlet/src/applications.tsx index 752024c..03c4910 100644 --- a/bundled_plugins/gauntlet/src/applications.tsx +++ b/bundled_plugins/gauntlet/src/applications.tsx @@ -1,4 +1,4 @@ -import { GeneratedCommand, GeneratorProps } from "@project-gauntlet/api/helpers"; +import { GeneratedEntrypoint, GeneratorProps } from "@project-gauntlet/api/helpers"; import { walk, WalkOptions } from "@std/fs/walk"; import { debounce } from "@std/async/debounce"; import { current_os, wayland } from "gauntlet:bridge/internal-all"; @@ -202,8 +202,8 @@ export default async function Applications({ add, remove, get, getAll }: Generat async function genericGenerator( directoriesToWatch: string[], appFromPath: (path: string) => Promise>, - commandFromApp: (id: string, data: DATA) => GeneratedCommand, - add: (id: string, data: GeneratedCommand) => void, + commandFromApp: (id: string, data: DATA) => GeneratedEntrypoint, + add: (id: string, data: GeneratedEntrypoint) => void, remove: (id: string) => void, walkOpts?: WalkOptions ): Promise<() => void> { diff --git a/bundled_plugins/gauntlet/src/window/shared.tsx b/bundled_plugins/gauntlet/src/window/shared.tsx index 789a647..e1989cd 100644 --- a/bundled_plugins/gauntlet/src/window/shared.tsx +++ b/bundled_plugins/gauntlet/src/window/shared.tsx @@ -1,4 +1,4 @@ -import { GeneratedCommand, GeneratedCommandAccessory, GeneratedCommandAction } from "@project-gauntlet/api/helpers"; +import { GeneratedEntrypoint, GeneratedEntrypointAccessory, GeneratedEntrypointAction } from "@project-gauntlet/api/helpers"; import { List } from "@project-gauntlet/api/components"; import { linux_open_application } from "gauntlet:bridge/internal-linux"; @@ -14,7 +14,7 @@ export function applicationActions( id: string, openApplication: () => void, focusWindow: (windowId: string) => void, -): GeneratedCommandAction[] { +): GeneratedEntrypointAction[] { const appWindows = Object.entries(openWindows) .filter(([_, windowData]) => windowData.appId == id) @@ -69,7 +69,7 @@ export function applicationActions( } } -export function applicationAccessories(id: string): GeneratedCommandAccessory[] { +export function applicationAccessories(id: string): GeneratedEntrypointAccessory[] { const appWindows = Object.entries(openWindows) .filter(([_, windowData]) => windowData.appId == id) @@ -86,12 +86,12 @@ export function applicationAccessories(id: string): GeneratedCommandAccessory[] export function addOpenWindow( appId: string, - generatedEntrypoint: GeneratedCommand, + generatedEntrypoint: GeneratedEntrypoint, windowId: string, windowTitle: string, openApplication: () => void, focusWindow: (windowId: string) => void, - add: (id: string, data: GeneratedCommand) => void, + add: (id: string, data: GeneratedEntrypoint) => void, ) { if (generatedEntrypoint) { openWindows[windowId] = { @@ -112,8 +112,8 @@ export function deleteOpenWindow( windowId: string, openApplication: (appId: string) => (() => void), focusWindow: (windowId: string) => void, - get: (id: string) => GeneratedCommand | undefined, - add: (id: string, data: GeneratedCommand) => void, + get: (id: string) => GeneratedEntrypoint | undefined, + add: (id: string, data: GeneratedEntrypoint) => void, ) { const openWindow = openWindows[windowId]; if (openWindow) { diff --git a/bundled_plugins/gauntlet/src/window/wayland.ts b/bundled_plugins/gauntlet/src/window/wayland.ts index 1c4ad93..4bc8339 100644 --- a/bundled_plugins/gauntlet/src/window/wayland.ts +++ b/bundled_plugins/gauntlet/src/window/wayland.ts @@ -1,5 +1,5 @@ import { addOpenWindow, deleteOpenWindow, openLinuxApplication } from "./shared"; -import { GeneratedCommand } from "@project-gauntlet/api/helpers"; +import { GeneratedEntrypoint } from "@project-gauntlet/api/helpers"; import { linux_wayland_focus_window, application_wayland_pending_event } from "gauntlet:bridge/internal-linux"; @@ -9,9 +9,9 @@ export function focusWaylandWindow(windowId: string) { export function applicationEventLoopWayland( focusWindow: (windowId: string) => void, - add: (id: string, data: GeneratedCommand) => void, - get: (id: string) => GeneratedCommand | undefined, - getAll: () => { [id: string]: GeneratedCommand }, + add: (id: string, data: GeneratedEntrypoint) => void, + get: (id: string) => GeneratedEntrypoint | undefined, + getAll: () => { [id: string]: GeneratedEntrypoint }, ) { const knownWindows: Record = { }; @@ -95,9 +95,9 @@ function addOpenWindowWayland( windowAppId: string, windowTitle: string, focusWindow: (windowId: string) => void, - add: (id: string, data: GeneratedCommand) => void, - get: (id: string) => GeneratedCommand | undefined, - getAll: () => { [id: string]: GeneratedCommand }, + add: (id: string, data: GeneratedEntrypoint) => void, + get: (id: string) => GeneratedEntrypoint | undefined, + getAll: () => { [id: string]: GeneratedEntrypoint }, ) { let appId = windowAppId; let generatedEntrypoint = get(windowAppId); diff --git a/bundled_plugins/gauntlet/src/window/x11.ts b/bundled_plugins/gauntlet/src/window/x11.ts index 4278f99..2b0538d 100644 --- a/bundled_plugins/gauntlet/src/window/x11.ts +++ b/bundled_plugins/gauntlet/src/window/x11.ts @@ -1,4 +1,4 @@ -import { GeneratedCommand } from "@project-gauntlet/api/helpers"; +import { GeneratedEntrypoint } from "@project-gauntlet/api/helpers"; import { addOpenWindow, deleteOpenWindow, openLinuxApplication } from "./shared"; import { application_x11_pending_event, linux_x11_focus_window } from "gauntlet:bridge/internal-linux"; @@ -37,9 +37,9 @@ export function focusX11Window(windowId: string) { export function applicationEventLoopX11( focusWindow: (windowId: string) => void, - add: (id: string, data: GeneratedCommand) => void, - get: (id: string) => GeneratedCommand | undefined, - getAll: () => { [id: string]: GeneratedCommand }, + add: (id: string, data: GeneratedEntrypoint) => void, + get: (id: string) => GeneratedEntrypoint | undefined, + getAll: () => { [id: string]: GeneratedEntrypoint }, ) { const windows: Record = {}; @@ -202,8 +202,8 @@ function validateAndAddOpenWindow( windows: Record, openApplication: (appId: string) => (() => void), focusWindow: (windowId: string) => void, - add: (id: string, data: GeneratedCommand) => void, - getAll: () => { [id: string]: GeneratedCommand }, + add: (id: string, data: GeneratedEntrypoint) => void, + getAll: () => { [id: string]: GeneratedEntrypoint }, ) { if (window.overrideRedirect) { diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index b4a7c22..0a2e5ee 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -33,45 +33,45 @@ export function showHud(display: string): void { return showHudWindow(display) } -export interface GeneratedCommand { +export interface GeneratedEntrypoint { name: string - actions: GeneratedCommandAction[] + actions: GeneratedEntrypointAction[] icon?: ArrayBuffer - accessories?: GeneratedCommandAccessory[] + accessories?: GeneratedEntrypointAccessory[] } -export type GeneratedCommandAction = GeneratedCommandActionRun | GeneratedCommandActionView +export type GeneratedEntrypointAction = GeneratedEntrypointActionRun | GeneratedEntrypointActionView -export interface GeneratedCommandActionRun { +export interface GeneratedEntrypointActionRun { ref?: string label: string run: () => void } -export interface GeneratedCommandActionView { +export interface GeneratedEntrypointActionView { ref?: string label: string view: FC } -export type GeneratedCommandAccessory = GeneratedCommandTextAccessory | GeneratedCommandIconAccessory; +export type GeneratedEntrypointAccessory = GeneratedEntrypointTextAccessory | GeneratedEntrypointIconAccessory; -export interface GeneratedCommandTextAccessory { +export interface GeneratedEntrypointTextAccessory { text: string icon?: string tooltip?: string } -export interface GeneratedCommandIconAccessory { +export interface GeneratedEntrypointIconAccessory { icon: string tooltip?: string } export type GeneratorProps = { - add: (id: string, data: GeneratedCommand) => void, + add: (id: string, data: GeneratedEntrypoint) => void, remove: (id: string) => void, - get: (id: string) => GeneratedCommand | undefined - getAll: () => { [id: string]: GeneratedCommand }, + get: (id: string) => GeneratedEntrypoint | undefined + getAll: () => { [id: string]: GeneratedEntrypoint }, }; export const Clipboard: Clipboard = { diff --git a/js/core/src/core.tsx b/js/core/src/core.tsx index bd8c519..8d88a25 100644 --- a/js/core/src/core.tsx +++ b/js/core/src/core.tsx @@ -1,5 +1,5 @@ import type { FC } from "react"; -import { runEntrypointGenerators, runGeneratedCommand, runGeneratedCommandAction } from "./entrypoint-generator"; +import { runEntrypointGenerators, runGeneratedEntrypoint, runGeneratedEntrypointAction } from "./entrypoint-generator"; import { reloadSearchIndex } from "./search-index"; import { closeView, handleEvent, handlePluginViewKeyboardEvent, renderInlineView, renderView } from "./render"; import { @@ -17,7 +17,7 @@ async function handleKeyboardEvent(event: NotReactsKeyboardEvent) { op_log_trace("plugin_event_handler", `Handling keyboard event: ${Deno.inspect(event)}`); switch (event.origin) { case "MainView": { - runGeneratedCommandAction(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta) + runGeneratedEntrypointAction(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta) break; } case "PluginView": { @@ -104,9 +104,9 @@ export async function runPluginLoop() { } break; } - case "RunGeneratedCommand": { + case "RunGeneratedEntrypoint": { try { - runGeneratedCommand(pluginEvent.entrypointId, pluginEvent.actionIndex) + runGeneratedEntrypoint(pluginEvent.entrypointId, pluginEvent.actionIndex) } catch (e) { console.error("Error occurred when running a generated command", pluginEvent.entrypointId, e) } diff --git a/js/core/src/entrypoint-generator.ts b/js/core/src/entrypoint-generator.ts index e9ee1bf..69fc2a4 100644 --- a/js/core/src/entrypoint-generator.ts +++ b/js/core/src/entrypoint-generator.ts @@ -9,54 +9,54 @@ import { reloadSearchIndex } from "./search-index"; import type { FC } from "react"; import { renderView } from "./render"; -interface GeneratedCommand { // TODO is it possible to import api here +interface GeneratedEntrypoint { // TODO is it possible to import api here name: string - actions: GeneratedCommandAction[] + actions: GeneratedEntrypointAction[] icon?: ArrayBuffer - accessories?: GeneratedCommandAccessory[] + accessories?: GeneratedEntrypointAccessory[] } -type GeneratedCommandAction = GeneratedCommandActionRun | GeneratedCommandActionView +type GeneratedEntrypointAction = GeneratedEntrypointActionRun | GeneratedEntrypointActionView -interface GeneratedCommandActionRun { +interface GeneratedEntrypointActionRun { ref?: string label: string run: () => void } -interface GeneratedCommandActionView { +interface GeneratedEntrypointActionView { ref?: string label: string view: FC } type GeneratorProps = { - add: (id: string, data: GeneratedCommand) => void, + add: (id: string, data: GeneratedEntrypoint) => void, remove: (id: string) => void, - get: (id: string) => GeneratedCommand | undefined - getAll: () => { [id: string]: GeneratedCommand } + get: (id: string) => GeneratedEntrypoint | undefined + getAll: () => { [id: string]: GeneratedEntrypoint } }; type Generator = (props: GeneratorProps) => void | (() => (void | Promise)) | Promise (void | Promise))> -type ProcessedGeneratedCommand = { +type ProcessedGeneratedEntrypoint = { generatorEntrypointId: string, id: string, uuid: string, - command: GeneratedCommand - derivedActions: GeneratedCommandDerivedAction[] + command: GeneratedEntrypoint + derivedActions: GeneratedEntrypointDerivedAction[] }; -type GeneratedCommandDerivedAction = GeneratedCommandDerivedActionRun | GeneratedCommandDerivedActionView +type GeneratedEntrypointDerivedAction = GeneratedEntrypointDerivedActionRun | GeneratedEntrypointDerivedActionView -interface GeneratedCommandDerivedActionRun { +interface GeneratedEntrypointDerivedActionRun { type: "Command" ref?: string label: string run: () => void } -interface GeneratedCommandDerivedActionView { +interface GeneratedEntrypointDerivedActionView { type: "View" ref?: string label: string @@ -64,10 +64,10 @@ interface GeneratedCommandDerivedActionView { } -type ProcessedGeneratedCommands = { [lookupEntrypointId: string]: ProcessedGeneratedCommand }; +type ProcessedGeneratedEntrypoints = { [lookupEntrypointId: string]: ProcessedGeneratedEntrypoint }; type GeneratorCleanups = { [generatorEntrypointId: string]: () => (void | Promise) }; -let storedGeneratedCommands: ProcessedGeneratedCommands = {} +let storedGeneratedEntrypoints: ProcessedGeneratedEntrypoints = {} let generatorCleanups: GeneratorCleanups = {} export async function runEntrypointGenerators(): Promise { @@ -79,7 +79,7 @@ export async function runEntrypointGenerators(): Promise { } } - storedGeneratedCommands = {} + storedGeneratedEntrypoints = {} generatorCleanups = {} await reloadSearchIndex(true) @@ -91,14 +91,14 @@ export async function runEntrypointGenerators(): Promise { op_log_info("entrypoint_generator", `Running entrypoint generator entrypoint ${generatorEntrypointId}`) - const add = (id: string, data: GeneratedCommand) => { + const add = (id: string, data: GeneratedEntrypoint) => { op_log_info("entrypoint_generator", `Adding entry '${id}' by entrypoint generator entrypoint '${generatorEntrypointId}'`) if (data.actions.length < 1) { throw new Error(`Error when adding entry '${id}': at least one action should be provided`) } - const derivedActions: GeneratedCommandDerivedAction[] = [] + const derivedActions: GeneratedEntrypointDerivedAction[] = [] for (const action of data.actions) { const label = action.label; @@ -132,7 +132,7 @@ export async function runEntrypointGenerators(): Promise { const lookupId = generatorEntrypointId + ":" + id; - storedGeneratedCommands[lookupId] = { + storedGeneratedEntrypoints[lookupId] = { generatorEntrypointId: generatorEntrypointId, id: id, uuid: crypto.randomUUID(), @@ -146,7 +146,7 @@ export async function runEntrypointGenerators(): Promise { op_log_info("entrypoint_generator", `Removing entry '${id}' by entrypoint generator entrypoint '${generatorEntrypointId}'`) const lookupId = generatorEntrypointId + ":" + id; - delete storedGeneratedCommands[lookupId] + delete storedGeneratedEntrypoints[lookupId] reloadSearchIndex(true) } @@ -155,19 +155,19 @@ export async function runEntrypointGenerators(): Promise { op_log_debug("entrypoint_generator", `Getting entry '${id}' by entrypoint generator entrypoint '${generatorEntrypointId}'`) const lookupId = generatorEntrypointId + ":" + id; - const generatedCommand = storedGeneratedCommands[lookupId]; - if (generatedCommand) { - return generatedCommand.command + const generatedEntrypoint = storedGeneratedEntrypoints[lookupId]; + if (generatedEntrypoint) { + return generatedEntrypoint.command } else { return undefined } } - const getAll = (): { [id: string]: GeneratedCommand } => { + const getAll = (): { [id: string]: GeneratedEntrypoint } => { op_log_debug("entrypoint_generator", `Getting all entries by entrypoint generator entrypoint '${generatorEntrypointId}'`) return Object.fromEntries( - Object.entries(storedGeneratedCommands) + Object.entries(storedGeneratedEntrypoints) .map(([_lookupId, value]) => [value.id, value.command]) ) } @@ -191,8 +191,8 @@ export async function runEntrypointGenerators(): Promise { } } -export function generatedCommandSearchIndex(): GeneratedSearchItem[] { - return Object.entries(storedGeneratedCommands).map(([entrypointLookupId, value]) => ({ +export function generatedEntrypointSearchIndex(): GeneratedSearchItem[] { + return Object.entries(storedGeneratedEntrypoints).map(([entrypointLookupId, value]) => ({ generator_entrypoint_id: value.generatorEntrypointId, entrypoint_id: entrypointLookupId, entrypoint_uuid: value.uuid, @@ -208,8 +208,8 @@ export function generatedCommandSearchIndex(): GeneratedSearchItem[] { })) } -export async function runGeneratedCommandAction(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean) { - const command = storedGeneratedCommands[entrypointId]; +export async function runGeneratedEntrypointAction(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean) { + const command = storedGeneratedEntrypoints[entrypointId]; if (command) { const id = await fetch_action_id_for_shortcut(command.generatorEntrypointId, key, modifierShift, modifierControl, modifierAlt, modifierMeta); @@ -222,11 +222,11 @@ export async function runGeneratedCommandAction(entrypointId: string, key: strin } } -export function runGeneratedCommand(entrypointId: string, action_index: number) { - const generatedCommand = storedGeneratedCommands[entrypointId]; +export function runGeneratedEntrypoint(entrypointId: string, action_index: number) { + const generatedEntrypoint = storedGeneratedEntrypoints[entrypointId]; - if (generatedCommand) { - const action = generatedCommand.derivedActions[action_index]; + if (generatedEntrypoint) { + const action = generatedEntrypoint.derivedActions[action_index]; if (action) { runAction(entrypointId, action) } else { @@ -237,7 +237,7 @@ export function runGeneratedCommand(entrypointId: string, action_index: number) } } -function runAction(entrypointId: string, action: GeneratedCommandDerivedAction) { +function runAction(entrypointId: string, action: GeneratedEntrypointDerivedAction) { switch (action.type) { case "Command": { action.run() @@ -245,7 +245,7 @@ function runAction(entrypointId: string, action: GeneratedCommandDerivedAction) break; } case "View": { - const entrypointName = storedGeneratedCommands[entrypointId] + const entrypointName = storedGeneratedEntrypoints[entrypointId] .command .name diff --git a/js/core/src/search-index.ts b/js/core/src/search-index.ts index e890c5c..f7fc547 100644 --- a/js/core/src/search-index.ts +++ b/js/core/src/search-index.ts @@ -1,6 +1,6 @@ -import { generatedCommandSearchIndex } from "./entrypoint-generator"; +import { generatedEntrypointSearchIndex } from "./entrypoint-generator"; import { reload_search_index } from "ext:core/ops"; export async function reloadSearchIndex(refreshSearchList: boolean) { - await reload_search_index(generatedCommandSearchIndex(), refreshSearchList); + await reload_search_index(generatedEntrypointSearchIndex(), refreshSearchList); } \ No newline at end of file diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 2f5b410..5ecd95f 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -45,7 +45,7 @@ type MacOSDesktopSettings13AndPostData = { icon: ArrayBuffer | undefined, } -type PluginEvent = ViewEvent | NotReactsKeyboardEvent | RunCommand | RunGeneratedCommand | OpenView | CloseView | OpenInlineView | ReloadSearchIndex | RefreshSearchIndex +type PluginEvent = ViewEvent | NotReactsKeyboardEvent | RunCommand | RunGeneratedEntrypoint | OpenView | CloseView | OpenInlineView | ReloadSearchIndex | RefreshSearchIndex type RenderLocation = "InlineView" | "View" type ViewEvent = { @@ -83,8 +83,8 @@ type RunCommand = { entrypointId: string } -type RunGeneratedCommand = { - type: "RunGeneratedCommand" +type RunGeneratedEntrypoint = { + type: "RunGeneratedEntrypoint" entrypointId: string actionIndex: number } @@ -118,15 +118,15 @@ type UiWidget = { type Props = { [key: string]: any }; type PropsWithChildren = { children?: UiWidget[] } & Props; -type GeneratedCommandAccessory = GeneratedCommandTextAccessory | GeneratedCommandIconAccessory; +type GeneratedEntrypointAccessory = GeneratedEntrypointTextAccessory | GeneratedEntrypointIconAccessory; -interface GeneratedCommandTextAccessory { +interface GeneratedEntrypointTextAccessory { text: string icon?: string tooltip?: string } -interface GeneratedCommandIconAccessory { +interface GeneratedEntrypointIconAccessory { icon: string tooltip?: string } @@ -137,7 +137,7 @@ type GeneratedSearchItem = { entrypoint_uuid: string, entrypoint_icon: ArrayBuffer | undefined, entrypoint_actions: GeneratedSearchItemAction[], - entrypoint_accessories: GeneratedCommandAccessory[], + entrypoint_accessories: GeneratedEntrypointAccessory[], } type GeneratedSearchItemAction = { diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 63cff6f..64d79b1 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -109,7 +109,7 @@ pub enum AppMsg { plugin_id: PluginId, entrypoint_id: EntrypointId, }, - RunGeneratedCommand { + RunGeneratedEntrypoint { plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize @@ -584,7 +584,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { }); Task::batch([ - state.run_generated_command(plugin_id, entrypoint_id, action_index), + state.run_generated_entrypoint(plugin_id, entrypoint_id, action_index), Task::done(AppMsg::PendingPluginViewLoadingBar) ]) } @@ -602,10 +602,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { state.run_command(plugin_id, entrypoint_id), ]) } - AppMsg::RunGeneratedCommand { plugin_id, entrypoint_id, action_index } => { + AppMsg::RunGeneratedEntrypoint { plugin_id, entrypoint_id, action_index } => { Task::batch([ state.hide_window(), - state.run_generated_command(plugin_id, entrypoint_id, action_index), + state.run_generated_entrypoint(plugin_id, entrypoint_id, action_index), ]) } AppMsg::RunPluginAction { render_location, plugin_id, widget_id } => { @@ -646,7 +646,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { let action = &search_result.entrypoint_actions[action_index]; match &action.action_type { SearchResultEntrypointActionType::Command => { - Task::done(AppMsg::RunGeneratedCommand { + Task::done(AppMsg::RunGeneratedEntrypoint { entrypoint_id: search_result.entrypoint_id.clone(), plugin_id: search_result.plugin_id.clone(), action_index, @@ -2080,11 +2080,11 @@ impl AppModel { }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } - fn run_generated_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize) -> Task { + fn run_generated_entrypoint(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize) -> Task { let mut backend_client = self.backend_api.clone(); Task::perform(async move { - backend_client.request_run_generated_command(plugin_id, entrypoint_id, action_index) + backend_client.request_run_generated_entrypoint(plugin_id, entrypoint_id, action_index) .await?; Ok(()) diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 7e8f6a6..ba874ed 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -294,7 +294,7 @@ pub enum BackendRequestData { plugin_id: PluginId, entrypoint_id: EntrypointId }, - RequestRunGeneratedCommand { + RequestRunGeneratedEntrypoint { plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index eff0da7..9274002 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -114,8 +114,8 @@ impl BackendForFrontendApi { Ok(()) } - pub async fn request_run_generated_command(&mut self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize) -> Result<(), BackendForFrontendApiError> { - let request = BackendRequestData::RequestRunGeneratedCommand { + pub async fn request_run_generated_entrypoint(&mut self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize) -> Result<(), BackendForFrontendApiError> { + let request = BackendRequestData::RequestRunGeneratedEntrypoint { plugin_id, entrypoint_id, action_index, diff --git a/rust/plugin_runtime/src/api.rs b/rust/plugin_runtime/src/api.rs index df1a5ba..067c17a 100644 --- a/rust/plugin_runtime/src/api.rs +++ b/rust/plugin_runtime/src/api.rs @@ -7,7 +7,7 @@ use gauntlet_utils::channel::{RequestError, RequestSender}; #[allow(async_fn_in_trait)] pub trait BackendForPluginRuntimeApi { - async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> ; + async fn reload_search_index(&self, generated_entrypoints: Vec, refresh_search_list: bool) -> anyhow::Result<()> ; async fn get_asset_data(&self, path: &str) -> anyhow::Result>; async fn get_entrypoint_generator_entrypoint_ids(&self) -> anyhow::Result>; async fn get_plugin_preferences(&self) -> anyhow::Result>; @@ -78,9 +78,9 @@ impl BackendForPluginRuntimeApiProxy { } impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { - async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { + async fn reload_search_index(&self, generated_entrypoints: Vec, refresh_search_list: bool) -> anyhow::Result<()> { let request = JsRequest::ReloadSearchIndex { - generated_commands, + generated_entrypoints, refresh_search_list, }; diff --git a/rust/plugin_runtime/src/events.rs b/rust/plugin_runtime/src/events.rs index 72599b9..e32434e 100644 --- a/rust/plugin_runtime/src/events.rs +++ b/rust/plugin_runtime/src/events.rs @@ -21,7 +21,7 @@ pub enum JsEvent { #[serde(rename = "entrypointId")] entrypoint_id: String }, - RunGeneratedCommand { + RunGeneratedEntrypoint { #[serde(rename = "entrypointId")] entrypoint_id: String, #[serde(rename = "actionIndex")] diff --git a/rust/plugin_runtime/src/model.rs b/rust/plugin_runtime/src/model.rs index 39b194f..3753cf5 100644 --- a/rust/plugin_runtime/src/model.rs +++ b/rust/plugin_runtime/src/model.rs @@ -132,7 +132,7 @@ pub enum JsRequest { show: bool }, ReloadSearchIndex { - generated_commands: Vec, + generated_entrypoints: Vec, refresh_search_list: bool }, GetAssetData { diff --git a/rust/plugin_runtime/src/search.rs b/rust/plugin_runtime/src/search.rs index d0dadcc..4d6eab0 100644 --- a/rust/plugin_runtime/src/search.rs +++ b/rust/plugin_runtime/src/search.rs @@ -5,7 +5,7 @@ use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; use crate::model::JsGeneratedSearchItem; #[op2(async)] -pub async fn reload_search_index(state: Rc>, #[serde] generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { +pub async fn reload_search_index(state: Rc>, #[serde] generated_entrypoints: Vec, refresh_search_list: bool) -> anyhow::Result<()> { let api = { let state = state.borrow(); @@ -16,7 +16,7 @@ pub async fn reload_search_index(state: Rc>, #[serde] generated api }; - api.reload_search_index(generated_commands, refresh_search_list).await?; + api.reload_search_index(generated_entrypoints, refresh_search_list).await?; Ok(()) } diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index f3a7bfd..9868073 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -212,8 +212,8 @@ async fn handle_request(application_manager: Arc, request_da BackendResponseData::Nothing } - BackendRequestData::RequestRunGeneratedCommand { plugin_id, entrypoint_id, action_index } => { - application_manager.handle_run_generated_command(plugin_id, entrypoint_id, action_index) + BackendRequestData::RequestRunGeneratedEntrypoint { plugin_id, entrypoint_id, action_index } => { + application_manager.handle_run_generated_entrypoint(plugin_id, entrypoint_id, action_index) .await; BackendResponseData::Nothing diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 851a31b..28c93a1 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -10,7 +10,7 @@ pub enum IntermediateUiEvent { RunCommand { entrypoint_id: String }, - RunGeneratedCommand { + RunGeneratedEntrypoint { entrypoint_id: String, action_index: usize }, diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index aa91d6c..931802b 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -98,7 +98,7 @@ pub enum OnePluginCommandData { RunCommand { entrypoint_id: String, }, - RunGeneratedCommand { + RunGeneratedEntrypoint { entrypoint_id: String, action_index: usize }, @@ -382,8 +382,8 @@ async fn event_loop(command_receiver: &mut tokio::sync::broadcast::Receiver { - Some(IntermediateUiEvent::RunGeneratedCommand { + OnePluginCommandData::RunGeneratedEntrypoint { entrypoint_id, action_index } => { + Some(IntermediateUiEvent::RunGeneratedEntrypoint { entrypoint_id, action_index }) @@ -514,8 +514,8 @@ async fn handle_message(message: JsRequest, api: &BackendForPluginRuntimeApiImpl Ok(JsResponse::Nothing) } - JsRequest::ReloadSearchIndex { generated_commands, refresh_search_list } => { - api.reload_search_index(generated_commands, refresh_search_list).await?; + JsRequest::ReloadSearchIndex { generated_entrypoints, refresh_search_list } => { + api.reload_search_index(generated_entrypoints, refresh_search_list).await?; Ok(JsResponse::Nothing) } @@ -616,7 +616,7 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsEvent { IntermediateUiEvent::RunCommand { entrypoint_id } => JsEvent::RunCommand { entrypoint_id }, - IntermediateUiEvent::RunGeneratedCommand { entrypoint_id, action_index } => JsEvent::RunGeneratedCommand { + IntermediateUiEvent::RunGeneratedEntrypoint { entrypoint_id, action_index } => JsEvent::RunGeneratedEntrypoint { entrypoint_id, action_index, }, @@ -699,7 +699,7 @@ impl BackendForPluginRuntimeApiImpl { } impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { - async fn reload_search_index(&self, generated_commands: Vec, refresh_search_list: bool) -> anyhow::Result<()> { + async fn reload_search_index(&self, generated_entrypoints: Vec, refresh_search_list: bool) -> anyhow::Result<()> { self.icon_cache.clear_plugin_icon_cache_dir(&self.plugin_uuid) .context("error when clearing up icon cache before recreating it")?; @@ -727,7 +727,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { .map(|entrypoint| (entrypoint.id.clone(), entrypoint.name.clone())) .collect(); - let mut generated_search_items = generated_commands.into_iter() + let mut generated_search_items = generated_entrypoints.into_iter() .map(|item| { let entrypoint_icon_path = match item.entrypoint_icon { None => None, diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index ca62438..ad8d613 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -355,10 +355,10 @@ impl ApplicationManager { self.mark_entrypoint_frecency(plugin_id, entrypoint_id).await } - pub async fn handle_run_generated_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize) { + pub async fn handle_run_generated_entrypoint(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize) { self.send_command(PluginCommand::One { id: plugin_id.clone(), - data: OnePluginCommandData::RunGeneratedCommand { + data: OnePluginCommandData::RunGeneratedEntrypoint { entrypoint_id: entrypoint_id.to_string(), action_index, } diff --git a/schema/backend.proto b/schema/backend.proto index 89c7f17..c7b1b37 100644 --- a/schema/backend.proto +++ b/schema/backend.proto @@ -155,7 +155,7 @@ message RpcSearchResult { enum RpcEntrypointTypeSearchResult { SR_COMMAND = 0; SR_VIEW = 1; - SR_GENERATED_COMMAND = 2; + SR_GENERATED_ENTRYPOINT = 2; } enum RpcEntrypointTypeSettings { @@ -194,7 +194,7 @@ message RpcEventRunCommand { string entrypoint_id = 1; } -message RpcEventRunGeneratedCommand { +message RpcEventRunGeneratedEntrypoint { string entrypoint_id = 1; } From d9b60d5e63a9fb5ba505f8801587eddb48d7cdbe Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:44:21 +0100 Subject: [PATCH 271/540] Fix list empty view image being too big --- dev_plugin/gauntlet.toml | 7 +++++++ dev_plugin/src/empty-list.tsx | 12 ++++++++++++ rust/client/src/ui/theme/container.rs | 2 ++ rust/client/src/ui/theme/image.rs | 9 +-------- rust/client/src/ui/theme/mod.rs | 4 ++-- 5 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 dev_plugin/src/empty-list.tsx diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 40e026f..0241033 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -162,6 +162,13 @@ path = 'src/empty.tsx' type = 'view' description = '' +[[entrypoint]] +id = 'empty-list-entrypoint' +name = 'Empty List Entrypoint' +path = 'src/empty-list.tsx' +type = 'view' +description = '' + [[entrypoint]] id = 'entrypoint-generator' name = 'Entrypoint generator' diff --git a/dev_plugin/src/empty-list.tsx b/dev_plugin/src/empty-list.tsx new file mode 100644 index 0000000..9fec8d0 --- /dev/null +++ b/dev_plugin/src/empty-list.tsx @@ -0,0 +1,12 @@ +import { ReactElement } from "react"; +import { List } from "@project-gauntlet/api/components"; + +const alderaanImage = "https://static.wikia.nocookie.net/starwars/images/4/4a/Alderaan.jpg/revision/latest?cb=20061211013805" + +export default function EmptyListView(): ReactElement { + return ( + + + + ) +} diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 07600c6..330ecaf 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -355,6 +355,8 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau } ContainerStyle::EmptyViewImage => { self.padding(theme.empty_view_image.padding.to_iced()) + .max_width(theme.empty_view_image.size.width) + .max_height(theme.empty_view_image.size.height) } ContainerStyle::Main => { self.class(ContainerStyleInner::Main) diff --git a/rust/client/src/ui/theme/image.rs b/rust/client/src/ui/theme/image.rs index c83a99b..96416f4 100644 --- a/rust/client/src/ui/theme/image.rs +++ b/rust/client/src/ui/theme/image.rs @@ -1,8 +1,7 @@ +use crate::ui::theme::{Element, ThemableWidget}; use iced::widget::Image; -use crate::ui::theme::{Element, get_theme, ThemableWidget}; pub enum ImageStyle { - EmptyViewImage, MainListItemIcon, } @@ -10,13 +9,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Image Element<'a, Message> { - let theme = get_theme(); - match kind { - ImageStyle::EmptyViewImage => { - self.width(theme.empty_view_image.size.width) - .height(theme.empty_view_image.size.height) - } ImageStyle::MainListItemIcon => { self.width(18) .height(18) diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index b70dab7..b3e66d5 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -283,8 +283,8 @@ impl GauntletComplexTheme { empty_view_image: ThemePaddingSize { padding: padding_all(8.0), size: ExternalThemeSize { - width: 100.0, - height: 100.0, + width: 150.0, + height: 150.0, }, }, grid_item: ThemeButton { From e5117c6c4500b9b1a71e4bba4dd8a5f9885a083b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:45:02 +0100 Subject: [PATCH 272/540] Fix grid empty view not displaying the image --- dev_plugin/gauntlet.toml | 7 +++ dev_plugin/src/empty-grid.tsx | 12 ++++++ rust/client/src/ui/widget.rs | 81 +++++++++++++++++++---------------- 3 files changed, 64 insertions(+), 36 deletions(-) create mode 100644 dev_plugin/src/empty-grid.tsx diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 0241033..ed1d7e9 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -169,6 +169,13 @@ path = 'src/empty-list.tsx' type = 'view' description = '' +[[entrypoint]] +id = 'empty-grid-entrypoint' +name = 'Empty Grid Entrypoint' +path = 'src/empty-grid.tsx' +type = 'view' +description = '' + [[entrypoint]] id = 'entrypoint-generator' name = 'Entrypoint generator' diff --git a/dev_plugin/src/empty-grid.tsx b/dev_plugin/src/empty-grid.tsx new file mode 100644 index 0000000..7666caa --- /dev/null +++ b/dev_plugin/src/empty-grid.tsx @@ -0,0 +1,12 @@ +import { ReactElement } from "react"; +import { Grid } from "@project-gauntlet/api/components"; + +const alderaanImage = "https://static.wikia.nocookie.net/starwars/images/4/4a/Alderaan.jpg/revision/latest?cb=20061211013805" + +export default function EmptyListView(): ReactElement { + return ( + + + + ) +} diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 69ae275..be49c18 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1686,54 +1686,63 @@ impl<'b> ComponentWidgets<'b> { ) -> Element<'a, ComponentWidgetEvent> { let RootState { show_action_panel, focused_item } = self.root_state(grid_widget.__id__); - let mut pending: Vec<&GridItemWidget> = vec![]; - let mut items: Vec> = vec![]; - let index_counter = &Cell::new(0); - let mut first_section = true; + let content = if grid_widget.content.ordered_members.is_empty() { + match &grid_widget.content.empty_view { + Some(widget) => self.render_empty_view_widget(widget), + None => horizontal_space().into() + } + } else { + let mut pending: Vec<&GridItemWidget> = vec![]; + let mut items: Vec> = vec![]; + let index_counter = &Cell::new(0); + let mut first_section = true; - for members in &grid_widget.content.ordered_members { - match &members { - GridWidgetOrderedMembers::GridItem(widget) => { - first_section = false; - pending.push(widget) - } - GridWidgetOrderedMembers::GridSection(widget) => { - if !pending.is_empty() { - let content = self.render_grid(&pending, &grid_widget.columns, focused_item.index, index_counter); - - items.push(content); - - pending = vec![]; + for members in &grid_widget.content.ordered_members { + match &members { + GridWidgetOrderedMembers::GridItem(widget) => { + first_section = false; + pending.push(widget) } + GridWidgetOrderedMembers::GridSection(widget) => { + if !pending.is_empty() { + let content = self.render_grid(&pending, &grid_widget.columns, focused_item.index, index_counter); - items.push(self.render_grid_section_widget(widget, focused_item.index, index_counter, first_section)); + items.push(content); - first_section = false; + pending = vec![]; + } + + items.push(self.render_grid_section_widget(widget, focused_item.index, index_counter, first_section)); + + first_section = false; + } } } - } - if !pending.is_empty() { - let content = self.render_grid(&pending, &grid_widget.columns, focused_item.index, index_counter); + if !pending.is_empty() { + let content = self.render_grid(&pending, &grid_widget.columns, focused_item.index, index_counter); - items.push(content); - } + items.push(content); + } - let content: Element<_> = column(items) - .into(); + let content: Element<_> = column(items) + .into(); - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::GridInner); + let content: Element<_> = container(content) + .width(Length::Fill) + .themed(ContainerStyle::GridInner); - let content: Element<_> = scrollable(content) - .id(focused_item.scrollable_id.clone()) - .width(Length::Fill) - .into(); + let content: Element<_> = scrollable(content) + .id(focused_item.scrollable_id.clone()) + .width(Length::Fill) + .into(); - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::Grid); + let content: Element<_> = container(content) + .width(Length::Fill) + .themed(ContainerStyle::Grid); + + content + }; self.render_plugin_root( *show_action_panel, From 796008ad39553a462ca70c1ce40fc643cbcfdd4f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:51:39 +0100 Subject: [PATCH 273/540] Make metadata section in detail in list slightly bigger --- rust/client/src/ui/widget.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index be49c18..a980e14 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1115,7 +1115,7 @@ impl<'b> ComponentWidgets<'b> { let content_element: Element<_> = container(content_element) .width(if is_in_list { Length::Fill } else { Length::FillPortion(3) }) - .height(if is_in_list { Length::FillPortion(5) } else { Length::Fill }) + .height(if is_in_list { Length::FillPortion(3) } else { Length::Fill }) .themed(ContainerStyle::DetailContent); content_element From 907e743607ae976cf6c5ad838ea64c7bf065c245 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 7 Jan 2025 21:13:46 +0100 Subject: [PATCH 274/540] Tweak styling of detail metadata when inside a list --- dev_plugin/src/test-list-detail.tsx | 9 +++-- rust/client/src/ui/theme/container.rs | 4 +++ rust/client/src/ui/theme/mod.rs | 4 +++ rust/client/src/ui/widget.rs | 50 +++++++++++++++++---------- 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/dev_plugin/src/test-list-detail.tsx b/dev_plugin/src/test-list-detail.tsx index d5d5854..ffce0bb 100644 --- a/dev_plugin/src/test-list-detail.tsx +++ b/dev_plugin/src/test-list-detail.tsx @@ -21,11 +21,10 @@ export default function Main(): ReactElement { Ezaraa Carnivorous - The Screaming Citadel 1 - Doctor Aphra (2016) 9 - Doctor Aphra (2016) 10 - Doctor Aphra (2016) 11 - Doctor Aphra (2016) 12 + Test 9 + Test 10 + Test 11 + Test 12 diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 330ecaf..0a0eba2 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -32,6 +32,7 @@ pub enum ContainerStyle { MainSearchBar, MetadataInner, MetadataItemValue, + MetadataItemValueInList, MetadataItemLabel, MetadataSeparator, MetadataTagItem, @@ -289,6 +290,9 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau ContainerStyle::MetadataItemValue => { self.padding(theme.metadata_item_value.padding.to_iced()) } + ContainerStyle::MetadataItemValueInList => { + self.padding(theme.metadata_item_value_in_list.padding.to_iced()) + } ContainerStyle::RootBottomPanel => { self.class(ContainerStyleInner::RootBottomPanel) .padding(theme.root_bottom_panel.padding.to_iced()) diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index b3e66d5..31d2d95 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -79,6 +79,7 @@ pub struct GauntletComplexTheme { main_list_item_text: ThemePaddingOnly, main_search_bar: ThemePaddingOnly, metadata_item_value: ThemePaddingOnly, + metadata_item_value_in_list: ThemePaddingOnly, metadata_content_inner: ThemePaddingOnly, metadata_inner: ThemePaddingOnly, metadata_separator: ThemePaddingOnly, @@ -243,6 +244,9 @@ impl GauntletComplexTheme { metadata_item_value: ThemePaddingOnly { padding: padding_axis(8.0, 0.0), }, + metadata_item_value_in_list: ThemePaddingOnly { + padding: padding_axis(2.0, 0.0), + }, metadata_link_icon: ThemePaddingOnly { padding: padding_axis(0.0, 4.0), }, diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index a980e14..af266d5 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -881,7 +881,7 @@ impl<'b> ComponentWidgets<'b> { .themed(ContainerStyle::MetadataTagItem) } - fn render_metadata_tag_list_widget<'a>(&self, widget: &MetadataTagListWidget) -> Element<'a, ComponentWidgetEvent> { + fn render_metadata_tag_list_widget<'a>(&self, widget: &MetadataTagListWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { let content: Vec> = widget.content.ordered_members .iter() .map(|members| { @@ -895,11 +895,11 @@ impl<'b> ComponentWidgets<'b> { .wrap() .into(); - render_metadata_item(&widget.label, value) + render_metadata_item(&widget.label, value, is_in_list) .into() } - fn render_metadata_link_widget<'a>(&self, widget: &MetadataLinkWidget) -> Element<'a, ComponentWidgetEvent> { + fn render_metadata_link_widget<'a>(&self, widget: &MetadataLinkWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); let icon: Element<_> = value(Bootstrap::BoxArrowUpRight) @@ -929,24 +929,24 @@ impl<'b> ComponentWidgets<'b> { .themed(TooltipStyle::Tooltip) }; - render_metadata_item(&widget.label, content) + render_metadata_item(&widget.label, content, is_in_list) .into() } - fn render_metadata_value_widget<'a>(&self, widget: &MetadataValueWidget) -> Element<'a, ComponentWidgetEvent> { + fn render_metadata_value_widget<'a>(&self, widget: &MetadataValueWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { let value: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); - render_metadata_item(&widget.label, value) + render_metadata_item(&widget.label, value, is_in_list) .into() } - fn render_metadata_icon_widget<'a>(&self, widget: &MetadataIconWidget) -> Element<'a, ComponentWidgetEvent> { + fn render_metadata_icon_widget<'a>(&self, widget: &MetadataIconWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { let value = value(icon_to_bootstrap(&widget.icon)) .font(BOOTSTRAP_FONT) .size(26) .into(); - render_metadata_item(&widget.label, value) + render_metadata_item(&widget.label, value, is_in_list) .into() } @@ -959,15 +959,15 @@ impl<'b> ComponentWidgets<'b> { .themed(ContainerStyle::MetadataSeparator) } - fn render_metadata_widget<'a>(&self, widget: &MetadataWidget) -> Element<'a, ComponentWidgetEvent> { + fn render_metadata_widget<'a>(&self, widget: &MetadataWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { let content: Vec> = widget.content.ordered_members .iter() .map(|members| { match members { - MetadataWidgetOrderedMembers::MetadataTagList(content) => self.render_metadata_tag_list_widget(content), - MetadataWidgetOrderedMembers::MetadataLink(content) => self.render_metadata_link_widget(content), - MetadataWidgetOrderedMembers::MetadataValue(content) => self.render_metadata_value_widget(content), - MetadataWidgetOrderedMembers::MetadataIcon(content) => self.render_metadata_icon_widget(content), + MetadataWidgetOrderedMembers::MetadataTagList(content) => self.render_metadata_tag_list_widget(content, is_in_list), + MetadataWidgetOrderedMembers::MetadataLink(content) => self.render_metadata_link_widget(content, is_in_list), + MetadataWidgetOrderedMembers::MetadataValue(content) => self.render_metadata_value_widget(content, is_in_list), + MetadataWidgetOrderedMembers::MetadataIcon(content) => self.render_metadata_icon_widget(content, is_in_list), MetadataWidgetOrderedMembers::MetadataSeparator(content) => self.render_metadata_separator_widget(content), } }) @@ -1094,7 +1094,7 @@ impl<'b> ComponentWidgets<'b> { let metadata_element = widget.content.metadata .as_ref() .map(|widget| { - let content = self.render_metadata_widget(widget); + let content = self.render_metadata_widget(widget, is_in_list); container(content) .width(if is_in_list { Length::Fill } else { Length::FillPortion(2) }) @@ -2022,7 +2022,7 @@ impl Display for SelectItem { } -fn render_metadata_item<'a>(label: &str, value: Element<'a, ComponentWidgetEvent>) -> Element<'a, ComponentWidgetEvent> { +fn render_metadata_item<'a>(label: &str, value: Element<'a, ComponentWidgetEvent>, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { let label: Element<_> = text(label.to_string()) .shaping(Shaping::Advanced) .themed(TextStyle::MetadataItemLabel); @@ -2030,11 +2030,23 @@ fn render_metadata_item<'a>(label: &str, value: Element<'a, ComponentWidgetEvent let label = container(label) .themed(ContainerStyle::MetadataItemLabel); - let value = container(value) - .themed(ContainerStyle::MetadataItemValue); + if is_in_list { + let space = horizontal_space() + .into(); - column(vec![label, value]) - .into() + let value = container(value) + .themed(ContainerStyle::MetadataItemValueInList); + + row(vec![label, space, value]) + .width(Length::Fill) + .into() + } else { + let value = container(value) + .themed(ContainerStyle::MetadataItemValue); + + column(vec![label, value]) + .into() + } } fn grid_width(columns: &Option) -> usize { From 95b0d0d326580a2f4309b4ed7f9f9f113c3ba979 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 8 Jan 2025 20:23:44 +0100 Subject: [PATCH 275/540] Fix windows on x11 sometimes not matching real state, if plugin was launched when windows already existed --- .../src/plugins/applications/linux/x11.rs | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/rust/plugin_runtime/src/plugins/applications/linux/x11.rs b/rust/plugin_runtime/src/plugins/applications/linux/x11.rs index a716161..a2eac9a 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/x11.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/x11.rs @@ -226,7 +226,19 @@ pub fn listen_on_x11_events( conn.change_window_attributes(screen.root, &aux)?.check()?; - let _ = fetch_existing_windows(screen.root, &conn, &tokio_handle, &sender, atoms); + let mut init_window_data = Vec::::new(); + + let _ = fetch_existing_windows(screen.root, &conn, &tokio_handle, &sender, atoms, &mut init_window_data); + + for window_id in init_window_data { + update_properties( + window_id, + &conn, + &tokio_handle, + &sender, + atoms, + ); + } loop { match conn.wait_for_event()? { @@ -314,11 +326,14 @@ fn fetch_existing_windows( tokio_handle: &Handle, sender: &Sender, atoms: atoms::Atoms, + init_window_data: &mut Vec ) -> anyhow::Result<()> { let query_tree = conn.query_tree(window_id)?.reply()?; let attributes = conn.get_window_attributes(window_id)?.reply()?; + init_window_data.push(window_id); + send_event(&tokio_handle, &sender, JsX11ApplicationEvent::Init { id: format!("{}", window_id), parent_id: format!("{}", query_tree.parent), @@ -331,16 +346,8 @@ fn fetch_existing_windows( }, }); - update_properties( - window_id, - &conn, - tokio_handle, - sender, - atoms, - ); - for window in query_tree.children { - let _ = fetch_existing_windows(window, conn, tokio_handle, sender, atoms); + let _ = fetch_existing_windows(window, conn, tokio_handle, sender, atoms, init_window_data); } Ok(()) From fd01fa2036363b26ea9ea9dfb61fbbeef14e5bdc Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 8 Jan 2025 20:26:52 +0100 Subject: [PATCH 276/540] Add tiny-skia back explicitly --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 86b9f58..0d880f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,8 @@ edition = "2021" [workspace.dependencies] # iced -#iced = { version = "0.13.99", features = ["wgpu", "tokio", "lazy", "advanced", "image"] } -iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", default-features = false, features = ["wgpu", "tokio", "advanced", "image"] } +#iced = { version = "0.13.99", features = ["tiny-skia", "wgpu", "tokio", "lazy", "advanced", "image"] } +iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", default-features = false, features = ["tiny-skia", "wgpu", "tokio", "advanced", "image"] } #iced_aw = { version = "0.11.99", features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } iced_aw = { git = "https://github.com/project-gauntlet/iced_aw.git", branch = "gauntlet-0.13", default-features = false, features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } #iced_table = "0.13.99" From 8cf1fedf209943b6f320252369962ab305b98e66 Mon Sep 17 00:00:00 2001 From: Tristan Schrader Date: Mon, 6 Jan 2025 15:46:46 -0800 Subject: [PATCH 277/540] fix: libxkbcommon + pkg-config for linux devshell Because libxkbcommon is needed during package build now on Linux, we have to included it as a package and configure it for access by smithay-client-toolkit with pkg-config --- nix/default.nix | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nix/default.nix b/nix/default.nix index 3d3114e..fcc3a82 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -11,17 +11,19 @@ system, ... }: let - inherit (pkgs) alejandra cargo cmake deno gauntlet gtk3 libxkbcommon libGL mkShell nodejs protobuf stdenv xorg wayland; + inherit (lib) makeBinPath makeLibraryPath optionals optionalString; + inherit (pkgs) alejandra cargo cmake deno gauntlet gtk3 libxkbcommon libGL mkShell nodejs pkg-config protobuf stdenv xorg wayland; + inherit (stdenv.hostPlatform) isLinux; in { _module.args.pkgs = import inputs.nixpkgs { inherit system; overlays = [inputs.self.overlays.default]; }; devShells.default = mkShell { - packages = [cargo cmake deno nodejs protobuf]; - shellHook = lib.optionalString stdenv.hostPlatform.isLinux '' - export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${lib.makeLibraryPath [libxkbcommon libGL xorg.libX11 wayland]}" - export PATH="$PATH:${lib.makeBinPath [gtk3]}" + packages = [cargo cmake deno nodejs protobuf] ++ optionals isLinux [libxkbcommon pkg-config]; + shellHook = optionalString isLinux '' + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${makeLibraryPath [libGL xorg.libX11 wayland]}" + export PATH="$PATH:${makeBinPath [gtk3]}" ''; }; formatter = alejandra; From f9d664290062709aec698980ee6f4a6c33b12408 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:26:33 +0100 Subject: [PATCH 278/540] Remove onClick events, instead primary action is called on click with id of clicked item. Add onItemFocusChange events --- .../gauntlet/src/window/shared.tsx | 57 ++-- bundled_plugins/gauntlet/src/windows.tsx | 25 +- dev_plugin/gauntlet.toml | 14 + dev_plugin/src/entrypoint-generator.tsx | 2 +- dev_plugin/src/grid-view.tsx | 31 +- dev_plugin/src/hooks-view.tsx | 248 ++++++++++------ dev_plugin/src/list-view.tsx | 43 +-- dev_plugin/src/test-grid-focus.tsx | 27 ++ dev_plugin/src/test-list-detail.tsx | 20 +- dev_plugin/src/test-list-focus.tsx | 28 ++ docs/js/components/action/props/onAction.md | 2 +- .../grid/props/onItemFocusChange.md | 1 + docs/js/components/grid_item/props/id.md | 1 + docs/js/components/grid_item/props/onClick.md | 1 - .../list/props/onItemFocusChange.md | 1 + docs/js/components/list_item/props/id.md | 1 + docs/js/components/list_item/props/onClick.md | 1 - js/api/src/gen/components.tsx | 24 +- rust/client/src/ui/client_context.rs | 4 + rust/client/src/ui/mod.rs | 28 +- rust/client/src/ui/scroll_handle.rs | 10 +- rust/client/src/ui/search_list.rs | 2 +- rust/client/src/ui/state/main_view.rs | 5 +- rust/client/src/ui/state/mod.rs | 12 +- rust/client/src/ui/state/plugin_view.rs | 3 +- rust/client/src/ui/widget.rs | 275 ++++++++++++++---- rust/client/src/ui/widget_container.rs | 34 ++- rust/component_model/src/lib.rs | 14 +- 28 files changed, 641 insertions(+), 273 deletions(-) create mode 100644 dev_plugin/src/test-grid-focus.tsx create mode 100644 dev_plugin/src/test-list-focus.tsx create mode 100644 docs/js/components/grid/props/onItemFocusChange.md create mode 100644 docs/js/components/grid_item/props/id.md delete mode 100644 docs/js/components/grid_item/props/onClick.md create mode 100644 docs/js/components/list/props/onItemFocusChange.md create mode 100644 docs/js/components/list_item/props/id.md delete mode 100644 docs/js/components/list_item/props/onClick.md diff --git a/bundled_plugins/gauntlet/src/window/shared.tsx b/bundled_plugins/gauntlet/src/window/shared.tsx index e1989cd..de9b1c0 100644 --- a/bundled_plugins/gauntlet/src/window/shared.tsx +++ b/bundled_plugins/gauntlet/src/window/shared.tsx @@ -1,6 +1,39 @@ -import { GeneratedEntrypoint, GeneratedEntrypointAccessory, GeneratedEntrypointAction } from "@project-gauntlet/api/helpers"; -import { List } from "@project-gauntlet/api/components"; +import { + GeneratedEntrypoint, + GeneratedEntrypointAccessory, + GeneratedEntrypointAction, + showHud +} from "@project-gauntlet/api/helpers"; import { linux_open_application } from "gauntlet:bridge/internal-linux"; +import React from "react"; +import { Action, ActionPanel, List } from "@project-gauntlet/api/components"; + +export function ListOfWindows({ windows, focus }: { windows: OpenWindowData[], focus: (windowId: string) => void }) { + return ( + + { + if (id) { + focus(id) + console.log("focus: " + id) + } else { + showHud("No window selected") + console.log("No window selected") + } + }} + /> + + } + > + { + windows.map(window => ) + } + + ) +} export type OpenWindowData = { id: string, @@ -45,22 +78,10 @@ export function applicationActions( { label: "Show windows", view: () => { - return ( - - { - appWindows - .map(([_, windowData]) => ( - { - focusWindow(windowData.id) - }} - /> - )) - } - - ) + const appWindowsArr = appWindows + .map(([_, window]) => window); + + return focusWindow(windowId)}/> } } ] diff --git a/bundled_plugins/gauntlet/src/windows.tsx b/bundled_plugins/gauntlet/src/windows.tsx index c521320..87639e2 100644 --- a/bundled_plugins/gauntlet/src/windows.tsx +++ b/bundled_plugins/gauntlet/src/windows.tsx @@ -1,43 +1,32 @@ import React, { ReactElement } from "react"; import { List } from "@project-gauntlet/api/components"; -import { openWindows } from "./window/shared"; +import { ListOfWindows, openWindows } from "./window/shared"; import { current_os, wayland } from "gauntlet:bridge/internal-all"; import { focusWaylandWindow } from "./window/wayland"; import { focusX11Window } from "./window/x11"; export default function Windows(): ReactElement { + const windows = Object.entries(openWindows) + .map(([_, window]) => window) + switch (current_os()) { case "linux": { if (wayland()) { return ( - focusWaylandWindow(windowId)}/> + focusWaylandWindow(windowId)}/> ) } else { return ( - focusX11Window(windowId)}/> + focusX11Window(windowId)}/> ) } } default: { return ( - + ) } } } - -function ListOfWindows({ focus }: { focus: (windowId: string) => void }) { - return ( - - { - Object.entries(openWindows) - .map(([_, window]) => ( - { focus(window.id) }}/> - ) - ) - } - - ) -} \ No newline at end of file diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index ed1d7e9..dae63c1 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -196,6 +196,20 @@ path = 'src/test-list-detail.tsx' type = 'view' description = '' +[[entrypoint]] +id = 'test-list-focus' +name = 'Test List Focus' +path = 'src/test-list-focus.tsx' +type = 'view' +description = '' + +[[entrypoint]] +id = 'test-grid-focus' +name = 'Test Grid Focus' +path = 'src/test-grid-focus.tsx' +type = 'view' +description = '' + [[supported_system]] os = 'linux' diff --git a/dev_plugin/src/entrypoint-generator.tsx b/dev_plugin/src/entrypoint-generator.tsx index 4861c01..ae1f8c4 100644 --- a/dev_plugin/src/entrypoint-generator.tsx +++ b/dev_plugin/src/entrypoint-generator.tsx @@ -5,7 +5,7 @@ import { List } from "@project-gauntlet/api/components"; function ListView(): ReactElement { return ( - + ) } diff --git a/dev_plugin/src/grid-view.tsx b/dev_plugin/src/grid-view.tsx index d7d1cef..e2c4d6e 100644 --- a/dev_plugin/src/grid-view.tsx +++ b/dev_plugin/src/grid-view.tsx @@ -20,7 +20,7 @@ export default function GridView(): ReactElement { onChange={setSearchText} /> - + Test Paragraph Section 1 1 @@ -34,7 +34,7 @@ export default function GridView(): ReactElement { if (title.toLowerCase().includes(searchText?.toLowerCase() ?? "")) { return ( - + Test Paragraph {value} @@ -48,21 +48,21 @@ export default function GridView(): ReactElement { }) } - + Test Paragraph Section 1 1 - + Test Paragraph Section 1 2 - + Test Paragraph Section 1 3 @@ -71,7 +71,7 @@ export default function GridView(): ReactElement { - + Test Paragraph Section 2 1 @@ -79,6 +79,7 @@ export default function GridView(): ReactElement { } @@ -89,14 +90,14 @@ export default function GridView(): ReactElement { - }> + }> - Test Paragraph Section 2 2 + Test Paragraph Section 2 3 - + Test Paragraph Section 2 3 @@ -105,35 +106,35 @@ export default function GridView(): ReactElement { - + Test Paragraph Section 2 1 - + Test Paragraph Section 2 2 - + Test Paragraph Section 2 2 - + Test Paragraph Section 2 3 - + Test Paragraph Section 2 4 @@ -145,7 +146,7 @@ export default function GridView(): ReactElement { { Array.from({ length: 50 }, (_, k) => k + 1) .map(value => ( - + Test {value} diff --git a/dev_plugin/src/hooks-view.tsx b/dev_plugin/src/hooks-view.tsx index 5a0852c..9596f9e 100644 --- a/dev_plugin/src/hooks-view.tsx +++ b/dev_plugin/src/hooks-view.tsx @@ -1,69 +1,156 @@ -import { Icons, List } from "@project-gauntlet/api/components"; -import React, { ReactElement, useRef } from "react"; +import { Action, ActionPanel, Element, Icons, List } from "@project-gauntlet/api/components"; +import React, { ReactElement, ReactNode, useRef, useState } from "react"; import { useCachedPromise, useFetch, useNavigation, usePromise } from "@project-gauntlet/api/hooks"; export default function ListView(): ReactElement { const { pushView } = useNavigation(); + const [id, setId] = useState(undefined); + return ( - - pushView()}/> - pushView()}/> - pushView()}/> - pushView()}/> - pushView()}/> - pushView()}/> - pushView()}/> - pushView()}/> - pushView()}/> - pushView()}/> - pushView()}/> - pushView()}/> - pushView()}/> + + pushPrimaryAction(id, pushView)}/> + + } + onItemFocusChange={itemId => setId(itemId)} + > + + + + + + + + + + + + + ) } -function UsePromiseTestBasic(): ReactElement { +function pushPrimaryAction(id: string | undefined, pushView: (component: ReactNode) => void) { + switch (id) { + case "UsePromiseTestBasic": { + pushView() + break + } + case "UsePromiseTestExecuteFalse": { + pushView() + break + } + case "UsePromiseTestRevalidate": { + pushView() + break + } + case "UsePromiseTestAbortableRevalidate": { + pushView() + break + } + case "UsePromiseTestMutate": { + pushView() + break + } + case "UsePromiseTestMutateOptimistic": { + pushView() + break + } + case "UsePromiseTestMutateOptimisticRollback": { + pushView() + break + } + case "UsePromiseTestMutateNoRevalidate": { + pushView() + break + } + case "UsePromiseTestThrow": { + pushView() + break + } + case "UseCachedPromiseBasic": { + pushView() + break + } + case "UseCachedPromiseInitialState": { + pushView() + break + } + case "UseFetchBasic": { + pushView() + break + } + case "UseFetchMap": { + pushView() + break + } + } +} + +function actionPanel(runAction?: () => void): Element { const { popView } = useNavigation(); + + return ( + + { + switch (itemId) { + case "go-back": { + popView() + break; + } + case "run": { + runAction?.() + break; + } + } + }} + /> + + ) +} + +function UsePromiseTestBasic(): ReactElement { const { data, error, isLoading } = usePromise( - async (one, two, three) => await inNSec(5), + async (_one, _two, _three) => await inNSec(5), [1, 2, 3] ); printState(data, error, isLoading) return ( - + - popView()}/> + ) } function UseCachedPromiseBasic(): ReactElement { - const { popView } = useNavigation(); const { data, error, isLoading } = useCachedPromise( - async (one, two, three) => await inNSec(5), + async (_one, _two, _three) => await inNSec(5), [1, 2, 3] ); printState(data, error, isLoading) return ( - + - popView()}/> + ) } function UseCachedPromiseInitialState(): ReactElement { - const { popView } = useNavigation(); const { data, error, isLoading } = useCachedPromise( - async (one, two, three) => await inNSec(5), + async (_one, _two, _three) => await inNSec(5), [1, 2, 3], { initialState: () => "initial" @@ -73,18 +160,17 @@ function UseCachedPromiseInitialState(): ReactElement { printState(data, error, isLoading) return ( - + - popView()}/> + ) } function UsePromiseTestExecuteFalse(): ReactElement { - const { popView } = useNavigation(); const { data, error, isLoading } = usePromise( - async (one, two, three) => await inNSec(5), + async (_one, _two, _three) => await inNSec(5), [1, 2, 3], { execute: false @@ -94,40 +180,37 @@ function UsePromiseTestExecuteFalse(): ReactElement { printState(data, error, isLoading) return ( - + - popView()}/> + ) } function UsePromiseTestRevalidate(): ReactElement { - const { popView } = useNavigation(); - const { data, error, isLoading, revalidate } = usePromise( - async (one, two, three) => await inNSec(5), + async (_one, _two, _three) => await inNSec(5), [1, 2, 3], ); printState(data, error, isLoading) return ( - + revalidate())} isLoading={isLoading}> - revalidate()}/> - popView()}/> + + ) } function UsePromiseTestAbortableRevalidate(): ReactElement { - const { popView } = useNavigation(); const abortable = useRef(); const { data, error, isLoading, revalidate } = usePromise( - async (one, two, three) => { + async (_one, _two, _three) => { await inNSec(5) }, [1, 2, 3], @@ -139,47 +222,46 @@ function UsePromiseTestAbortableRevalidate(): ReactElement { printState(data, error, isLoading) return ( - + revalidate())} isLoading={isLoading}> - revalidate()}/> - popView()}/> + + ) } function UsePromiseTestMutate(): ReactElement { - const { popView } = useNavigation(); const { data, error, isLoading, mutate } = usePromise( - async (one, two, three) => await inNSec(5), + async (_one, _two, _three) => await inNSec(5), [1, 2, 3], ); printState(data, error, isLoading) - const onClick = async () => { + const onAction = async () => { await mutate(inNSec(5)) }; + return ( - + - - popView()}/> + + ) } function UsePromiseTestMutateOptimistic(): ReactElement { - const { popView } = useNavigation(); const { data, error, isLoading, mutate } = usePromise( - async (one, two, three) => await inNSec(5), + async (_one, _two, _three) => await inNSec(5), [1, 2, 3], ); printState(data, error, isLoading) - const onClick = async () => { + const onAction = async () => { await mutate( inNSec(5), { @@ -187,27 +269,26 @@ function UsePromiseTestMutateOptimistic(): ReactElement { } ) }; - + return ( - + - - popView()}/> + + ) } function UsePromiseTestMutateOptimisticRollback(): ReactElement { - const { popView } = useNavigation(); const { data, error, isLoading, mutate } = usePromise( - async (one, two, three) => await inNSec(5), + async (_one, _two, _three) => await inNSec(5), [1, 2, 3], ); printState(data, error, isLoading) - const onClick = async () => { + const onAction = async () => { await mutate( new Promise((_resolve, reject) => { setTimeout( @@ -225,45 +306,45 @@ function UsePromiseTestMutateOptimisticRollback(): ReactElement { }; return ( - + - - popView()}/> + + ) } function UsePromiseTestMutateNoRevalidate(): ReactElement { - const { popView } = useNavigation(); const { data, error, isLoading, mutate } = usePromise( - async (one, two, three) => await inNSec(5), + async (_one, _two, _three) => await inNSec(5), [1, 2, 3], ); printState(data, error, isLoading) + const onAction = async () => { + await mutate( + inNSec(5), + { + shouldRevalidateAfter: false, + } + ) + } + return ( - + - async () => { - await mutate( - inNSec(5), - { - shouldRevalidateAfter: false, - } - ) - }}/> - popView()}/> + + ) } function UsePromiseTestThrow(): ReactElement { - const { popView } = useNavigation(); const { data, error, isLoading } = usePromise( - async (one, two, three) => { + async (_one, _two, _three) => { throw new Error("test") }, [1, 2, 3], @@ -272,17 +353,15 @@ function UsePromiseTestThrow(): ReactElement { printState(data, error, isLoading) return ( - + - popView()}/> + ) } function UseFetchBasic(): ReactElement { - const { popView } = useNavigation(); - interface GithubLatestRelease { } @@ -294,9 +373,9 @@ function UseFetchBasic(): ReactElement { printState(data, error, isLoading) return ( - + - popView()}/> + ) @@ -307,7 +386,6 @@ function UseFetchMap(): ReactElement { url: string } - const { popView } = useNavigation(); const { data, error, isLoading } = useFetch( "https://api.github.com/repos/project-gauntlet/gauntlet/releases/latest", { @@ -318,9 +396,9 @@ function UseFetchMap(): ReactElement { printState(data, error, isLoading) return ( - + - popView()}/> + ) diff --git a/dev_plugin/src/list-view.tsx b/dev_plugin/src/list-view.tsx index 9259710..3a6e717 100644 --- a/dev_plugin/src/list-view.tsx +++ b/dev_plugin/src/list-view.tsx @@ -1,55 +1,60 @@ -import { IconAccessory, Icons, List, TextAccessory } from "@project-gauntlet/api/components"; +import { Action, ActionPanel, IconAccessory, Icons, List, TextAccessory } from "@project-gauntlet/api/components"; import { ReactElement, useState } from "react"; import { Environment } from "@project-gauntlet/api/helpers"; export default function ListView(): ReactElement { const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; - const [id, setId] = useState("default"); - const onClick = () => { - console.log("onClick " + id) - setId(id); + const onClick = (id: string | undefined) => { + if (id == "print-env") { + console.log(Environment.gauntletVersion); + console.log(Environment.isDevelopment); + console.log(Environment.pluginCacheDir); + console.log(Environment.pluginDataDir); + } else { + console.log("onClick " + id) + } }; const [searchText, setSearchText] = useState(""); return ( - + + + + }> - + { numbers.map(value => { const title = "Title " + value; if (title.toLowerCase().includes(searchText?.toLowerCase() ?? "")) { return ( - + ) } else { return undefined } }) } - - + + - { - console.log(Environment.gauntletVersion); - console.log(Environment.isDevelopment); - console.log(Environment.pluginCacheDir); - console.log(Environment.pluginDataDir); - }}/> + - - + + , diff --git a/dev_plugin/src/test-grid-focus.tsx b/dev_plugin/src/test-grid-focus.tsx new file mode 100644 index 0000000..e35cc8e --- /dev/null +++ b/dev_plugin/src/test-grid-focus.tsx @@ -0,0 +1,27 @@ +import { ReactElement } from "react"; +import { Grid } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + const content = ( + + + Test + + + ); + + return ( + console.log("onItemFocusChange", id)}> + {content} + {content} + {content} + {content} + {content} + {content} + {content} + {content} + {content} + {content} + + ) +} diff --git a/dev_plugin/src/test-list-detail.tsx b/dev_plugin/src/test-list-detail.tsx index ffce0bb..f1c6cfc 100644 --- a/dev_plugin/src/test-list-detail.tsx +++ b/dev_plugin/src/test-list-detail.tsx @@ -4,16 +4,16 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - - - - - - - - - - + + + + + + + + + + Sentient diff --git a/dev_plugin/src/test-list-focus.tsx b/dev_plugin/src/test-list-focus.tsx new file mode 100644 index 0000000..5534ba2 --- /dev/null +++ b/dev_plugin/src/test-list-focus.tsx @@ -0,0 +1,28 @@ +import { ReactElement, useState } from "react"; +import { List } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + const [id, setId] = useState(undefined); + + return ( + + + + + + + + + + + + + + + Focused: {id} + + + + + ) +} diff --git a/docs/js/components/action/props/onAction.md b/docs/js/components/action/props/onAction.md index 3993926..2346de8 100644 --- a/docs/js/components/action/props/onAction.md +++ b/docs/js/components/action/props/onAction.md @@ -1 +1 @@ -Function that is called when the button in UI is clicked or the shortcut is pressed \ No newline at end of file +Function that is called when action button is clicked or the shortcut is pressed (including primary and secondary shortcuts). ID parameter is an id of currently focused grid or list item \ No newline at end of file diff --git a/docs/js/components/grid/props/onItemFocusChange.md b/docs/js/components/grid/props/onItemFocusChange.md new file mode 100644 index 0000000..797f991 --- /dev/null +++ b/docs/js/components/grid/props/onItemFocusChange.md @@ -0,0 +1 @@ +Function that is called when focused item changes. Argument is an ID of new focused item \ No newline at end of file diff --git a/docs/js/components/grid_item/props/id.md b/docs/js/components/grid_item/props/id.md new file mode 100644 index 0000000..4297e57 --- /dev/null +++ b/docs/js/components/grid_item/props/id.md @@ -0,0 +1 @@ +ID of the grid item. Used in Grid's onItemFocusChange event and Action's onAction event \ No newline at end of file diff --git a/docs/js/components/grid_item/props/onClick.md b/docs/js/components/grid_item/props/onClick.md deleted file mode 100644 index 4463e3d..0000000 --- a/docs/js/components/grid_item/props/onClick.md +++ /dev/null @@ -1 +0,0 @@ -Function that will be called when user selects an item on the grid. \ No newline at end of file diff --git a/docs/js/components/list/props/onItemFocusChange.md b/docs/js/components/list/props/onItemFocusChange.md new file mode 100644 index 0000000..b0ae091 --- /dev/null +++ b/docs/js/components/list/props/onItemFocusChange.md @@ -0,0 +1 @@ +Function that is called when focused item changes. Argument is an ID of new focused item \ No newline at end of file diff --git a/docs/js/components/list_item/props/id.md b/docs/js/components/list_item/props/id.md new file mode 100644 index 0000000..ef0db98 --- /dev/null +++ b/docs/js/components/list_item/props/id.md @@ -0,0 +1 @@ +ID of the list item. Used in List's onItemFocusChange event \ No newline at end of file diff --git a/docs/js/components/list_item/props/onClick.md b/docs/js/components/list_item/props/onClick.md deleted file mode 100644 index 4463e3d..0000000 --- a/docs/js/components/list_item/props/onClick.md +++ /dev/null @@ -1 +0,0 @@ -Function that will be called when user selects an item on the grid. \ No newline at end of file diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index d8ef3b6..72913f3 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -6,7 +6,7 @@ declare global { ["gauntlet:action"]: { id?: string; label: string; - onAction: () => void; + onAction: (id: string | undefined) => void; }; ["gauntlet:action_panel_section"]: { children?: ElementComponent; @@ -139,10 +139,10 @@ declare global { }; ["gauntlet:list_item"]: { children?: ElementComponent; + id: string; title: string; subtitle?: string; icon?: ImageLike; - onClick?: () => void; }; ["gauntlet:list_section"]: { children?: ElementComponent; @@ -152,12 +152,13 @@ declare global { ["gauntlet:list"]: { children?: ElementComponent; isLoading?: boolean; + onItemFocusChange?: (itemId: string | undefined) => void; }; ["gauntlet:grid_item"]: { children?: ElementComponent; + id: string; title?: string; subtitle?: string; - onClick?: () => void; }; ["gauntlet:grid_section"]: { children?: ElementComponent; @@ -169,6 +170,7 @@ declare global { children?: ElementComponent; isLoading?: boolean; columns?: number; + onItemFocusChange?: (itemId: string | undefined) => void; }; } } @@ -364,7 +366,7 @@ export type ImageLike = ImageSource | Icons; export interface ActionProps { id?: string; label: string; - onAction: () => void; + onAction: (id: string | undefined) => void; } export const Action: FC = (props: ActionProps): ReactNode => { return ; @@ -677,14 +679,14 @@ export const SearchBar: FC = (props: SearchBarProps): ReactNode return ; }; export interface ListItemProps { + id: string; title: string; subtitle?: string; icon?: ImageLike; accessories?: (ElementComponent | ElementComponent)[]; - onClick?: () => void; } export const ListItem: FC = (props: ListItemProps): ReactNode => { - return {props.accessories as any}; + return {props.accessories as any}; }; export interface ListSectionProps { children?: ElementComponent; @@ -701,6 +703,7 @@ export interface ListProps { children?: ElementComponent; actions?: ElementComponent; isLoading?: boolean; + onItemFocusChange?: (itemId: string | undefined) => void; } export const List: FC & { Item: typeof ListItem; @@ -709,7 +712,7 @@ export const List: FC & { EmptyView: typeof EmptyView; Detail: typeof Detail; } = (props: ListProps): ReactNode => { - return {props.actions as any}{props.children}; + return {props.actions as any}{props.children}; }; List.Item = ListItem; List.Section = ListSection; @@ -718,15 +721,15 @@ List.EmptyView = EmptyView; List.Detail = Detail; export interface GridItemProps { children?: ElementComponent; + id: string; title?: string; subtitle?: string; accessory?: ElementComponent; - onClick?: () => void; } export const GridItem: FC & { Content: typeof Content; } = (props: GridItemProps): ReactNode => { - return {props.accessory as any}{props.children}; + return {props.accessory as any}{props.children}; }; GridItem.Content = Content; export interface GridSectionProps { @@ -746,6 +749,7 @@ export interface GridProps { isLoading?: boolean; actions?: ElementComponent; columns?: number; + onItemFocusChange?: (itemId: string | undefined) => void; } export const Grid: FC & { Item: typeof GridItem; @@ -753,7 +757,7 @@ export const Grid: FC & { SearchBar: typeof SearchBar; EmptyView: typeof EmptyView; } = (props: GridProps): ReactNode => { - return {props.actions as any}{props.children}; + return {props.actions as any}{props.children}; }; Grid.Item = GridItem; Grid.Section = GridSection; diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index 346df6a..b5f05fa 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -133,6 +133,10 @@ impl ClientContext { self.view.get_action_ids() } + pub fn get_focused_item_id(&self) -> Option { + self.view.get_focused_item_id() + } + pub fn focus_up(&self) -> Task { self.view.focus_up() } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 64d79b1..27ccd62 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -118,7 +118,8 @@ pub enum AppMsg { RunPluginAction { render_location: UiRenderLocation, plugin_id: PluginId, - widget_id: UiWidgetId + widget_id: UiWidgetId, + id: Option, }, PromptChanged(String), PromptSubmit, @@ -192,9 +193,9 @@ pub enum AppMsg { OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus, OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { search_result: SearchResult, widget_id: UiWidgetId }, OnAnyActionMainViewInlineViewPanelKeyboardWithFocus { widget_id: UiWidgetId }, - OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id: UiWidgetId }, - OnAnyActionPluginViewAnyPanelKeyboardWithFocus { widget_id: UiWidgetId }, - OnAnyActionPluginViewAnyPanel { widget_id: UiWidgetId }, + OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id: UiWidgetId, id: Option }, + OnAnyActionPluginViewAnyPanelKeyboardWithFocus { widget_id: UiWidgetId, id: Option }, + OnAnyActionPluginViewAnyPanel { widget_id: UiWidgetId, id: Option }, OnAnyActionMainViewSearchResultPanelMouse { widget_id: UiWidgetId }, OnPrimaryActionMainViewActionPanelMouse { widget_id: UiWidgetId }, ResetMainViewState, @@ -608,13 +609,14 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { state.run_generated_entrypoint(plugin_id, entrypoint_id, action_index), ]) } - AppMsg::RunPluginAction { render_location, plugin_id, widget_id } => { + AppMsg::RunPluginAction { render_location, plugin_id, widget_id, id } => { let widget_event = ComponentWidgetEvent::RunAction { widget_id, + id }; Task::batch([ - state.hide_window(), + // state.hide_window(), // TODO Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) ]) } @@ -1158,22 +1160,24 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { render_location: UiRenderLocation::InlineView, plugin_id, widget_id, + id: None }) ]) } None => Task::none() } } - AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id } => { - Task::done(AppMsg::OnAnyActionPluginViewAnyPanel { widget_id }) + AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id, id } => { + Task::done(AppMsg::OnAnyActionPluginViewAnyPanel { widget_id, id }) } - AppMsg::OnAnyActionPluginViewAnyPanelKeyboardWithFocus { widget_id } => { + AppMsg::OnAnyActionPluginViewAnyPanelKeyboardWithFocus { widget_id, id } => { Task::batch([ Task::done(AppMsg::ToggleActionPanel { keyboard: true }), Task::done(AppMsg::RunPluginAction { render_location: UiRenderLocation::View, plugin_id: state.client_context.get_view_plugin_id(), widget_id, + id }) ]) } @@ -1205,6 +1209,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { render_location: UiRenderLocation::InlineView, plugin_id, widget_id, + id: None }) } None => Task::none() @@ -1235,11 +1240,12 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { GlobalState::PluginView { .. } => Task::none() } } - AppMsg::OnAnyActionPluginViewAnyPanel { widget_id } => { + AppMsg::OnAnyActionPluginViewAnyPanel { widget_id, id } => { Task::done(AppMsg::RunPluginAction { render_location: UiRenderLocation::View, plugin_id: state.client_context.get_view_plugin_id(), widget_id, + id }) } AppMsg::OpenPluginView(plugin_id, entrypoint_id) => { @@ -1812,7 +1818,7 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { content, primary_action, action_panel, - None::<&ScrollHandle>, + None::<&ScrollHandle>, "", || AppMsg::ToggleActionPanel { keyboard: false }, |widget_id| AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id }, diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs index 5e552ea..222cd80 100644 --- a/rust/client/src/ui/scroll_handle.rs +++ b/rust/client/src/ui/scroll_handle.rs @@ -7,8 +7,7 @@ pub const ESTIMATED_MAIN_LIST_ITEM_HEIGHT: f32 = 38.8; pub const ESTIMATED_ACTION_ITEM_HEIGHT: f32 = 38.8; // TODO #[derive(Clone, Debug)] -pub struct ScrollHandle { - phantom: PhantomData, +pub struct ScrollHandle { pub scrollable_id: Id, pub index: Option, offset: usize, @@ -16,10 +15,9 @@ pub struct ScrollHandle { item_height: f32, } -impl ScrollHandle { - pub fn new(first_focused: bool, item_height: f32, rows_per_view: usize) -> ScrollHandle { +impl ScrollHandle { + pub fn new(first_focused: bool, item_height: f32, rows_per_view: usize) -> ScrollHandle { ScrollHandle { - phantom: PhantomData, scrollable_id: Id::unique(), index: if first_focused { Some(0) } else { None }, offset: 0, @@ -37,7 +35,7 @@ impl ScrollHandle { self.index = None; } - pub fn get<'a>(&self, search_results: &'a [T]) -> Option<&'a T> { + pub fn get<'a, T>(&self, search_results: &'a [T]) -> Option<&'a T> { match self.index { None => None, Some(index) => search_results.get(index) diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index 8419270..7e9b068 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -19,7 +19,7 @@ use iced::{Alignment, Length}; pub fn search_list<'a>( search_results: &'a [SearchResult], - focused_search_result: &ScrollHandle, + focused_search_result: &ScrollHandle, ) -> Element<'a, SearchResult> { let items: Vec> = search_results .iter() diff --git a/rust/client/src/ui/state/main_view.rs b/rust/client/src/ui/state/main_view.rs index 7815ee9..408dfc0 100644 --- a/rust/client/src/ui/state/main_view.rs +++ b/rust/client/src/ui/state/main_view.rs @@ -1,15 +1,14 @@ use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_ACTION_ITEM_HEIGHT}; -use gauntlet_common::model::{SearchResultEntrypointAction, UiWidgetId}; pub enum MainViewState { None, SearchResultActionPanel { // ephemeral state - focused_action_item: ScrollHandle, + focused_action_item: ScrollHandle, }, InlineViewActionPanel { // ephemeral state - focused_action_item: ScrollHandle, + focused_action_item: ScrollHandle, } } diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index d19fd27..79b1728 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -18,7 +18,7 @@ pub enum GlobalState { search_field_id: text_input::Id, // ephemeral state - focused_search_result: ScrollHandle, + focused_search_result: ScrollHandle, // state sub_state: MainViewState, @@ -168,12 +168,13 @@ impl Focus for GlobalState { } GlobalState::PluginView { sub_state, .. } => { let action_ids = client_context.get_action_ids(); + let focused_item_id = client_context.get_focused_item_id(); match sub_state { PluginViewState::None => { if let Some(widget_id) = action_ids.get(0) { let widget_id = *widget_id; - Task::done(AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id }) + Task::done(AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id, id: focused_item_id }) } else { Task::none() } @@ -181,7 +182,7 @@ impl Focus for GlobalState { PluginViewState::ActionPanel { focused_action_item, .. } => { if let Some(widget_id) = focused_action_item.get(&action_ids) { let widget_id = *widget_id; - Task::done(AppMsg::OnAnyActionPluginViewAnyPanelKeyboardWithFocus { widget_id }) + Task::done(AppMsg::OnAnyActionPluginViewAnyPanelKeyboardWithFocus { widget_id, id: focused_item_id }) } else { Task::none() } @@ -212,12 +213,13 @@ impl Focus for GlobalState { } GlobalState::PluginView { sub_state, .. } => { let action_ids = client_context.get_action_ids(); + let focused_item_id = client_context.get_focused_item_id(); match sub_state { PluginViewState::None => { if let Some(widget_id) = action_ids.get(1) { let widget_id = *widget_id; - Task::done(AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id }) + Task::done(AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id, id: focused_item_id }) } else { Task::none() } @@ -237,7 +239,7 @@ impl Focus for GlobalState { GlobalState::MainView { sub_state, .. } => { match sub_state { MainViewState::None => { - Task::perform(async {}, |_| AppMsg::HideWindow) + Task::done(AppMsg::HideWindow) } MainViewState::SearchResultActionPanel { .. } => { MainViewState::initial(sub_state); diff --git a/rust/client/src/ui/state/plugin_view.rs b/rust/client/src/ui/state/plugin_view.rs index cb5b2e8..c7ac0e2 100644 --- a/rust/client/src/ui/state/plugin_view.rs +++ b/rust/client/src/ui/state/plugin_view.rs @@ -1,12 +1,11 @@ use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_ACTION_ITEM_HEIGHT}; -use gauntlet_common::model::UiWidgetId; #[derive(Debug, Clone)] pub enum PluginViewState { None, ActionPanel { // ephemeral state - focused_action_item: ScrollHandle, + focused_action_item: ScrollHandle, } } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index af266d5..0f47868 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -15,7 +15,7 @@ use crate::ui::theme::text_input::TextInputStyle; use crate::ui::theme::tooltip::TooltipStyle; use crate::ui::theme::{Element, ThemableWidget}; use crate::ui::AppMsg; -use gauntlet_common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Icons, ImageLike, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiWidgetId}; +use gauntlet_common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Icons, ImageLike, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiRenderLocation, UiWidgetId}; use gauntlet_common_ui::shortcut_to_text; use iced::alignment::{Horizontal, Vertical}; use iced::font::Weight; @@ -38,18 +38,21 @@ use std::sync::Arc; pub struct ComponentWidgets<'b> { root_widget: &'b mut Option>, state: &'b mut HashMap, - images: &'b HashMap> + plugin_id: PluginId, + images: &'b HashMap>, } impl<'b> ComponentWidgets<'b> { pub fn new( root_widget: &'b mut Option>, state: &'b mut HashMap, + plugin_id: PluginId, images: &'b HashMap> ) -> ComponentWidgets<'b> { Self { root_widget, state, + plugin_id, images } } @@ -246,7 +249,7 @@ struct SelectState { #[derive(Debug, Clone)] struct RootState { show_action_panel: bool, - focused_item: ScrollHandle, + focused_item: ScrollHandle, } impl ComponentWidgetState { @@ -368,6 +371,32 @@ impl<'b> ComponentWidgets<'b> { result } + pub fn get_focused_item_id(&self) -> Option { + let Some(root_widget) = &self.root_widget else { + return None; + }; + + let Some(content) = &root_widget.content else { + return None; + }; + + match content { + RootWidgetMembers::Detail(_) => None, + RootWidgetMembers::Form(_) => None, + RootWidgetMembers::Inline(_) => None, + RootWidgetMembers::List(widget) => { + let RootState { focused_item, .. } = self.root_state(widget.__id__); + + ComponentWidgets::list_focused_item_id(focused_item, widget) + }, + RootWidgetMembers::Grid(widget) => { + let RootState { focused_item, .. } = self.root_state(widget.__id__); + + ComponentWidgets::grid_focused_item_id(focused_item, widget) + }, + } + } + fn grid_section_sizes(grid_widget: &GridWidget) -> Vec { let mut amount_per_section: Vec = vec![]; let mut pending_section_size = 0; @@ -569,11 +598,18 @@ impl<'b> ComponentWidgets<'b> { RootWidgetMembers::Detail(_) => Task::none(), RootWidgetMembers::Form(_) => Task::none(), RootWidgetMembers::Inline(_) => Task::none(), - RootWidgetMembers::List(widget) => { - let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, widget.__id__); + RootWidgetMembers::List(list_widget) => { + let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, list_widget.__id__); - focused_item.focus_previous() - .unwrap_or_else(|| Task::none()) + let focus_task = focused_item.focus_previous() + .unwrap_or_else(|| Task::none()); + + let item_focus_event = ComponentWidgets::list_item_focus_event(self.plugin_id.clone(), focused_item, list_widget); + + Task::batch([ + item_focus_event, + focus_task + ]) } RootWidgetMembers::Grid(grid_widget) => { let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); @@ -584,7 +620,7 @@ impl<'b> ComponentWidgets<'b> { let amount_per_section_total = Self::grid_section_sizes(grid_widget); - match grid_up_offset(*current_index, amount_per_section_total) { + let focus_task = match grid_up_offset(*current_index, amount_per_section_total) { None => Task::none(), Some(data) => { match focused_item.focus_previous_in(data.offset) { @@ -592,7 +628,14 @@ impl<'b> ComponentWidgets<'b> { Some(_) => focused_item.scroll_to(data.row_index) } } - } + }; + + let item_focus_event = ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); + + Task::batch([ + item_focus_event, + focus_task + ]) } } } @@ -632,8 +675,15 @@ impl<'b> ComponentWidgets<'b> { }) .count(); - focused_item.focus_next(total) - .unwrap_or_else(|| Task::none()) + let focus_task = focused_item.focus_next(total) + .unwrap_or_else(|| Task::none()); + + let item_focus_event = ComponentWidgets::list_item_focus_event(self.plugin_id.clone(), focused_item, widget); + + Task::batch([ + item_focus_event, + focus_task + ]) } RootWidgetMembers::Grid(grid_widget) => { let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); @@ -656,13 +706,16 @@ impl<'b> ComponentWidgets<'b> { let _ = focused_item.focus_next(total); + let item_focus_event = ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); + return Task::batch([ unfocus, - focused_item.scroll_to(0) + focused_item.scroll_to(0), + item_focus_event ]) }; - match grid_down_offset(*current_index, amount_per_section_total) { + let focus_task = match grid_down_offset(*current_index, amount_per_section_total) { None => Task::none(), Some(data) => { match focused_item.focus_next_in(total, data.offset) { @@ -670,7 +723,14 @@ impl<'b> ComponentWidgets<'b> { Some(_) => focused_item.scroll_to(data.row_index) } } - } + }; + + let item_focus_event = ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); + + Task::batch([ + item_focus_event, + focus_task + ]) } } } @@ -689,14 +749,14 @@ impl<'b> ComponentWidgets<'b> { RootWidgetMembers::Form(_) => Task::none(), RootWidgetMembers::Inline(_) => Task::none(), RootWidgetMembers::List(_) => Task::none(), - RootWidgetMembers::Grid(widget) => { - let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, widget.__id__); + RootWidgetMembers::Grid(grid_widget) => { + let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); let _ = focused_item.focus_previous(); // focused_item.scroll_to(0) - // TODO - Task::none() + + ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget) } } } @@ -740,11 +800,98 @@ impl<'b> ComponentWidgets<'b> { let _ = focused_item.focus_next(total); // focused_item.scroll_to(0) - Task::none() + + ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget) } } } + fn list_focused_item_id(focused_item: &ScrollHandle, widget: &ListWidget) -> Option { + let mut items = vec![]; + + for members in &widget.content.ordered_members { + match &members { + ListWidgetOrderedMembers::ListItem(item) => { + items.push(&item.id); + } + ListWidgetOrderedMembers::ListSection(section) => { + for members in §ion.content.ordered_members { + match &members { + ListSectionWidgetOrderedMembers::ListItem(item) => { + items.push(&item.id); + } + } + } + } + } + } + + match focused_item.get(&items) { + None => None, + Some(item_id) => Some(item_id.to_string()) + } + } + + fn list_item_focus_event(plugin_id: PluginId, focused_item: &ScrollHandle, widget: &ListWidget) -> Task { + let widget_event = match ComponentWidgets::list_focused_item_id(focused_item, widget) { + None => { + ComponentWidgetEvent::FocusListItem { list_widget_id: widget.__id__, item_id: None } + } + Some(item_id) => { + ComponentWidgetEvent::FocusListItem { list_widget_id: widget.__id__, item_id: Some(item_id) } + } + }; + + Task::done(AppMsg::WidgetEvent { + plugin_id, + render_location: UiRenderLocation::View, + widget_event, + }) + } + + fn grid_focused_item_id(focused_item: &ScrollHandle, widget: &GridWidget) -> Option { + let mut items = vec![]; + + for members in &widget.content.ordered_members { + match &members { + GridWidgetOrderedMembers::GridItem(item) => { + items.push(&item.id); + } + GridWidgetOrderedMembers::GridSection(section) => { + for members in §ion.content.ordered_members { + match &members { + GridSectionWidgetOrderedMembers::GridItem(item) => { + items.push(&item.id); + } + } + } + } + } + } + + match focused_item.get(&items) { + None => None, + Some(item_id) => Some(item_id.to_string()) + } + } + + fn grid_item_focus_event(plugin_id: PluginId, focused_item: &ScrollHandle, widget: &GridWidget) -> Task { + let widget_event = match ComponentWidgets::grid_focused_item_id(focused_item, widget) { + None => { + ComponentWidgetEvent::FocusGridItem { grid_widget_id: widget.__id__, item_id: None } + } + Some(item_id) => { + ComponentWidgetEvent::FocusGridItem { grid_widget_id: widget.__id__, item_id: Some(item_id) } + } + }; + + Task::done(AppMsg::WidgetEvent { + plugin_id, + render_location: UiRenderLocation::View, + widget_event, + }) + } + pub fn get_action_panel(&self, action_shortcuts: &HashMap) -> Option { let Some(root_widget) = &self.root_widget else { return None; @@ -818,6 +965,7 @@ impl<'b> ComponentWidgets<'b> { self.render_plugin_root( *show_action_panel, widget.__id__, + None, &None, &widget.content.actions, content, @@ -1329,6 +1477,7 @@ impl<'b> ComponentWidgets<'b> { self.render_plugin_root( *show_action_panel, widget_id, + None, &None, &widget.content.actions, content, @@ -1561,9 +1710,12 @@ impl<'b> ComponentWidgets<'b> { .height(Length::Fill) .into(); + let focused_item_id = ComponentWidgets::list_focused_item_id(focused_item, list_widget); + self.render_plugin_root( *show_action_panel, widget_id, + focused_item_id, &list_widget.content.search_bar, &list_widget.content.actions, content, @@ -1671,8 +1823,16 @@ impl<'b> ComponentWidgets<'b> { index_counter.set(index_counter.get() + 1); + let action_ids = self.get_action_ids(); + let primary_action = action_ids.first(); + + let on_press_msg = match primary_action { + None => ComponentWidgetEvent::Noop, + Some(widget_id) => ComponentWidgetEvent::RunPrimaryAction { widget_id: *widget_id, id: Some(widget.id.clone()) } + }; + button(content) - .on_press(ComponentWidgetEvent::ListItemClick { widget_id: widget.__id__ }) + .on_press(on_press_msg) .width(Length::Fill) .themed(style) } @@ -1744,9 +1904,12 @@ impl<'b> ComponentWidgets<'b> { content }; + let focused_item_id = ComponentWidgets::grid_focused_item_id(focused_item, grid_widget); + self.render_plugin_root( *show_action_panel, grid_widget.__id__, + focused_item_id, &grid_widget.content.search_bar, &grid_widget.content.actions, content, @@ -1814,8 +1977,16 @@ impl<'b> ComponentWidgets<'b> { index_counter.set(index_counter.get() + 1); + let action_ids = self.get_action_ids(); + let primary_action = action_ids.first(); + + let on_press_msg = match primary_action { + None => ComponentWidgetEvent::Noop, + Some(widget_id) => ComponentWidgetEvent::RunPrimaryAction { widget_id: *widget_id, id: Some(widget.id.clone()) } + }; + let content: Element<_> = button(content) - .on_press(ComponentWidgetEvent::GridItemClick { widget_id: widget.__id__ }) + .on_press(on_press_msg) .width(Length::Fill) .themed(style); @@ -1932,7 +2103,8 @@ impl<'b> ComponentWidgets<'b> { fn render_plugin_root<'a>( &self, show_action_panel: bool, - widget_id: UiWidgetId, + root_widget_id: UiWidgetId, + focused_item_id: Option, search_bar: &Option, action_panel: &Option, content: Element<'a, ComponentWidgetEvent>, @@ -1979,11 +2151,11 @@ impl<'b> ComponentWidgets<'b> { content, primary_action, action_panel, - None::<&ScrollHandle>, + None::<&ScrollHandle>, entrypoint_name, - || ComponentWidgetEvent::ToggleActionPanel { widget_id }, - |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id }, - |widget_id| ComponentWidgetEvent::ActionClick { widget_id }, + || ComponentWidgetEvent::ToggleActionPanel { widget_id: root_widget_id }, + |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id, id: focused_item_id.clone() }, + |widget_id| ComponentWidgetEvent::ActionClick { widget_id, id: focused_item_id.clone() }, || ComponentWidgetEvent::Noop, ) } @@ -1998,9 +2170,9 @@ impl<'b> ComponentWidgets<'b> { action_panel, Some(&focused_action_item), entrypoint_name, - || ComponentWidgetEvent::ToggleActionPanel { widget_id }, - |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id }, - |widget_id| ComponentWidgetEvent::ActionClick { widget_id }, + || ComponentWidgetEvent::ToggleActionPanel { widget_id: root_widget_id }, + |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id, id: focused_item_id.clone() }, + |widget_id| ComponentWidgetEvent::ActionClick { widget_id, id: focused_item_id.clone() }, || ComponentWidgetEvent::Noop, ) } @@ -2305,10 +2477,10 @@ fn render_action_panel_items<'a, T: 'a + Clone>( columns } -fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T, ACTION>( +fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T>( action_panel: ActionPanel, on_action_click: F, - action_panel_scroll_handle: &ScrollHandle, + action_panel_scroll_handle: &ScrollHandle, ) -> Element<'a, T> { let columns = render_action_panel_items(true, action_panel.title, action_panel.items, action_panel_scroll_handle.index, &on_action_click, &Cell::new(0)); @@ -2324,7 +2496,7 @@ fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T, ACTION>( .themed(ContainerStyle::ActionPanel) } -pub fn render_root<'a, T: 'a + Clone, ACTION>( +pub fn render_root<'a, T: 'a + Clone>( show_action_panel: bool, top_panel: Element<'a, T>, top_separator: Element<'a, T>, @@ -2332,7 +2504,7 @@ pub fn render_root<'a, T: 'a + Clone, ACTION>( content: Element<'a, T>, primary_action: Option<(String, UiWidgetId, PhysicalShortcut)>, action_panel: Option, - action_panel_scroll_handle: Option<&ScrollHandle>, + action_panel_scroll_handle: Option<&ScrollHandle>, entrypoint_name: &str, on_panel_toggle_click: impl Fn() -> T, on_panel_primary_click: impl Fn(UiWidgetId) -> T, @@ -2644,9 +2816,11 @@ pub enum ComponentWidgetEvent { }, ActionClick { widget_id: UiWidgetId, + id: Option }, RunAction { - widget_id: UiWidgetId + widget_id: UiWidgetId, + id: Option }, ToggleDatePicker { widget_id: UiWidgetId, @@ -2681,15 +2855,18 @@ pub enum ComponentWidgetEvent { ToggleActionPanel { widget_id: UiWidgetId, }, - ListItemClick { - widget_id: UiWidgetId, + FocusListItem { + list_widget_id: UiWidgetId, + item_id: Option, }, - GridItemClick { - widget_id: UiWidgetId, + FocusGridItem { + grid_widget_id: UiWidgetId, + item_id: Option, }, PreviousView, RunPrimaryAction { widget_id: UiWidgetId, + id: Option, }, Noop, } @@ -2707,8 +2884,8 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::TagClick { widget_id } => { Some(create_metadata_tag_item_on_click_event(widget_id)) } - ComponentWidgetEvent::RunAction { widget_id } | ComponentWidgetEvent::ActionClick { widget_id } => { - Some(create_action_on_action_event(widget_id)) + ComponentWidgetEvent::RunAction { widget_id, id } | ComponentWidgetEvent::ActionClick { widget_id, id } => { + Some(create_action_on_action_event(widget_id, id)) } ComponentWidgetEvent::ToggleDatePicker { widget_id } => { let state = state.expect("state should always exist for "); @@ -2813,18 +2990,18 @@ impl ComponentWidgetEvent { event: AppMsg::ToggleActionPanel { keyboard: false } }) } - ComponentWidgetEvent::ListItemClick { widget_id } => { - Some(create_list_item_on_click_event(widget_id)) + ComponentWidgetEvent::FocusListItem { list_widget_id, item_id } => { + Some(create_list_on_item_focus_change_event(list_widget_id, item_id)) } - ComponentWidgetEvent::GridItemClick { widget_id } => { - Some(create_grid_item_on_click_event(widget_id)) + ComponentWidgetEvent::FocusGridItem { grid_widget_id, item_id } => { + Some(create_grid_on_item_focus_change_event(grid_widget_id, item_id)) } ComponentWidgetEvent::Noop | ComponentWidgetEvent::PreviousView => { panic!("widget_id on these events is not supposed to be called") } - ComponentWidgetEvent::RunPrimaryAction { widget_id } => { + ComponentWidgetEvent::RunPrimaryAction { widget_id, id } => { Some(UiViewEvent::AppEvent { - event: AppMsg::OnAnyActionPluginViewAnyPanel { widget_id } + event: AppMsg::OnAnyActionPluginViewAnyPanel { widget_id, id } }) } } @@ -2845,9 +3022,9 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::OnChangePasswordField { widget_id, .. } => widget_id, ComponentWidgetEvent::OnChangeSearchBar { widget_id, .. } => widget_id, ComponentWidgetEvent::ToggleActionPanel { widget_id } => widget_id, - ComponentWidgetEvent::ListItemClick { widget_id, .. } => widget_id, - ComponentWidgetEvent::GridItemClick { widget_id, .. } => widget_id, - ComponentWidgetEvent::RunPrimaryAction { widget_id } => widget_id, + ComponentWidgetEvent::FocusListItem { list_widget_id, .. } => list_widget_id, + ComponentWidgetEvent::FocusGridItem { grid_widget_id, .. } => grid_widget_id, + ComponentWidgetEvent::RunPrimaryAction { widget_id, .. } => widget_id, ComponentWidgetEvent::Noop | ComponentWidgetEvent::PreviousView => panic!("widget_id on these events is not supposed to be called"), }.to_owned() } diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 84d98c3..d6c9a56 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -7,7 +7,6 @@ use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidge use std::collections::HashMap; use std::mem; use std::ops::DerefMut; -use std::rc::Rc; use std::sync::{Arc, Mutex}; use iced::Task; use crate::ui::AppMsg; @@ -84,7 +83,7 @@ impl PluginWidgetContainer { *root_widget = Some(container); if first_open { - ComponentWidgets::new(&mut root_widget, &mut state, &self.images) + ComponentWidgets::new(&mut root_widget, &mut state, plugin_id.clone(), &self.images) .first_open() } else { AppMsg::Noop @@ -107,7 +106,7 @@ impl PluginWidgetContainer { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images) + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images) .render_root_widget(plugin_view_state, self.entrypoint_name.as_ref(), action_shortcuts) } @@ -115,7 +114,7 @@ impl PluginWidgetContainer { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images) + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images) .render_root_inline_widget(self.plugin_name.as_ref(), self.entrypoint_name.as_ref()) } @@ -123,69 +122,76 @@ impl PluginWidgetContainer { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).append_text(text) + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).append_text(text) } pub fn backspace_text(&self) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).backspace_text() + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).backspace_text() } pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_search_bar(widget_id) + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).focus_search_bar(widget_id) } pub fn toggle_action_panel(&self) { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).toggle_action_panel() + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).toggle_action_panel() } pub fn get_action_ids(&self) -> Vec { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).get_action_ids() + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).get_action_ids() + } + + pub fn get_focused_item_id(&self) -> Option { + let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); + let mut state = self.state.lock().expect("lock is poisoned"); + + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).get_focused_item_id() } pub fn get_action_panel(&self, action_shortcuts: &HashMap) -> Option { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).get_action_panel(action_shortcuts) + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).get_action_panel(action_shortcuts) } pub fn focus_up(&self) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_up() + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).focus_up() } pub fn focus_down(&self) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_down() + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).focus_down() } pub fn focus_left(&self) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_left() + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).focus_left() } pub fn focus_right(&self) -> Task { let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); let mut state = self.state.lock().expect("lock is poisoned"); - ComponentWidgets::new(&mut root_widget, &mut state, &self.images).focus_right() + ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).focus_right() } } diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index 53e14e1..63dfefa 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -602,7 +602,9 @@ pub fn create_component_model() -> Vec { [ property("id", mark_doc!("/action/props/id.md"), true, PropertyType::String), property("label", mark_doc!("/action/props/label.md"), false, PropertyType::String), - event("onAction", mark_doc!("/action/props/onAction.md"), false, []) + event("onAction", mark_doc!("/action/props/onAction.md"), false, [ + property("id", "".to_string(), true, PropertyType::String) + ]) ], children_none(), ); @@ -1082,11 +1084,11 @@ pub fn create_component_model() -> Vec { mark_doc!("/list_item/description.md"), "ListItem", [ + property("id", mark_doc!("/list_item/props/id.md"), false, PropertyType::String), property("title", mark_doc!("/list_item/props/title.md"),false, PropertyType::String), property("subtitle", mark_doc!("/list_item/props/subtitle.md"),true, PropertyType::String), property("icon", mark_doc!("/list_item/props/icon.md"),true, PropertyType::SharedTypeRef { name: "ImageLike".to_owned() }), property("accessories", mark_doc!("/list_item/props/accessories.md"),true, PropertyType::Array { item: Box::new(PropertyType::Union { items: vec![component_ref(&accessory_text_component, Arity::ZeroOrMore), component_ref(&accessory_icon_component, Arity::ZeroOrMore)]}) }), - event("onClick", mark_doc!("/list_item/props/onClick.md"), true, []) ], children_none(), ); @@ -1114,6 +1116,9 @@ pub fn create_component_model() -> Vec { [ property("actions", mark_doc!("/list/props/actions.md"), true, component_ref(&action_panel_component, Arity::ZeroOrOne)), property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), + event("onItemFocusChange", mark_doc!("/list/props/onItemFocusChange.md"), true, [ + property("itemId", "".to_string(), true, PropertyType::String) + ]) ], children_members( [ @@ -1133,10 +1138,10 @@ pub fn create_component_model() -> Vec { mark_doc!("/grid_item/description.md"), "GridItem", [ + property("id", mark_doc!("/list_item/props/id.md"), false, PropertyType::String), property("title", mark_doc!("/grid_item/props/title.md"), true, PropertyType::String), property("subtitle", mark_doc!("/grid_item/props/subtitle.md"), true, PropertyType::String), property("accessory", mark_doc!("/grid_item/props/accessory.md"),true, component_ref(&accessory_icon_component, Arity::ZeroOrOne)), - event("onClick", mark_doc!("/grid_item/props/onClick.md"), true, []) ], children_members( [], @@ -1177,6 +1182,9 @@ pub fn create_component_model() -> Vec { property("columns", mark_doc!("/grid/props/columns.md"),true, PropertyType::Number), // TODO default // fit // inset + event("onItemFocusChange", mark_doc!("/grid/props/onItemFocusChange.md"), true, [ + property("itemId", "".to_string(), true, PropertyType::String) + ]) ], children_members( [ From 4d137256928b9b7d923514207d73c7c349ea0b4f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:32:14 +0100 Subject: [PATCH 279/540] Make windows_app_from_path async --- rust/plugin_runtime/src/plugins/applications/windows.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rust/plugin_runtime/src/plugins/applications/windows.rs b/rust/plugin_runtime/src/plugins/applications/windows.rs index cde3645..108aee9 100644 --- a/rust/plugin_runtime/src/plugins/applications/windows.rs +++ b/rust/plugin_runtime/src/plugins/applications/windows.rs @@ -6,6 +6,7 @@ use deno_core::op2; use std::path::PathBuf; use anyhow::{anyhow, Context}; use image::RgbaImage; +use tokio::task::spawn_blocking; use windows::core::{GUID, HSTRING, PWSTR}; use windows::Win32::Foundation::{HANDLE, HWND}; use windows::Win32::Graphics::Gdi; @@ -54,6 +55,10 @@ fn windows_open_application(#[string] file_path: String) -> anyhow::Result<()> { #[op2(async)] #[serde] async fn windows_app_from_path(#[string] file_path: String) -> anyhow::Result> { + Ok(spawn_blocking(|| windows_app_from_path_blocking(file_path)).await?) +} + +fn windows_app_from_path_blocking(file_path: String) -> anyhow::Result> { if PathBuf::from(&file_path).exists() { let name = extract_name(&file_path)?; let icon = extract_icon(&file_path) From 72ba3f03cb3d8e284b4cae9de609908018998541 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:40:55 +0100 Subject: [PATCH 280/540] Move npm version bump to final state of publishing, so nix hash is not mismatched on release tag --- js/api/package.json | 2 +- js/build/src/main.ts | 20 +++++++++---------- nix/overlay.nix | 2 +- package-lock.json | 2 +- .../src/plugins/applications/windows.rs | 2 +- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/js/api/package.json b/js/api/package.json index 9663918..3394d07 100644 --- a/js/api/package.json +++ b/js/api/package.json @@ -1,6 +1,6 @@ { "name": "@project-gauntlet/api", - "version": "0.12.0", + "version": "0.0.0", "type": "module", "exports": { "./components": { diff --git a/js/build/src/main.ts b/js/build/src/main.ts index 33e8a04..0f454e5 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -152,7 +152,7 @@ async function doPublishFinal() { buildJs(projectRoot) - publishNpmPackage(projectRoot) + await publishNpmPackage(projectRoot) } function build(projectRoot: string, arch: string) { @@ -234,14 +234,6 @@ async function makeRepoChanges(projectRoot: string): Promise<{ releaseNotes: str console.log("Writing changelog file...") await writeFile(changelogFilePath, newChangelog.join(EOL)) - const bumpNpmPackage = (packageDir: string) => { - spawnWithErrors('npm', ['version', `0.${newVersion}.0`], { cwd: packageDir }) - } - - console.log("Bump version for api subproject...") - const apiProjectPath = path.join(projectRoot, "js", "api"); - bumpNpmPackage(apiProjectPath) - console.log("git add all files...") await git.raw('add', '-A') console.log("git commit...") @@ -456,9 +448,15 @@ async function packageForWindows(projectRoot: string, arch: string): Promise<{ f } -function publishNpmPackage(projectRoot: string) { - console.log("Publishing npm api package...") +async function publishNpmPackage(projectRoot: string) { + const version = await readVersion(projectRoot) + const apiProjectPath = path.join(projectRoot, "js", "api"); + + console.log("Bump version for api subproject...") + spawnWithErrors('npm', ['version', `0.${version}.0`], { cwd: apiProjectPath }) + + console.log("Publishing npm api package...") spawnWithErrors('npm', ['publish'], { cwd: apiProjectPath }) } diff --git a/nix/overlay.nix b/nix/overlay.nix index 24bea30..ce69b6a 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -6,7 +6,7 @@ flake.overlays.default = final: _: let # These must be updated following the instructions in ./nix/README.md when dependencies are updated or the version bumped version = "v12"; - npmDepsHash = "sha256-NADYbP3NUUezlMN5nPqZ3qpBRZNkSWf8HBwn9zqsEYw="; + npmDepsHash = "sha256-rk5aLdXoSpqMNcSVIRJTKN1KqtddVPiKkZ1YWZ+n5m8="; RUSTY_V8_ARCHIVE = fetchRustyV8 "130.0.2" { aarch64-darwin = "sha256-aWZ/4Q4Wttx37xOdBmTCPGP+eYGhr4CM1UkYq8pC7Qs="; aarch64-linux = "sha256-p9+tHmKIM5wBABubHIAstpwfzO19ypPzOuaV4b6loCU="; diff --git a/package-lock.json b/package-lock.json index 0f18683..519073c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ }, "js/api": { "name": "@project-gauntlet/api", - "version": "0.12.0", + "version": "0.0.0", "devDependencies": { "@project-gauntlet/typings": "*", "@rollup/plugin-alias": "^5.1.1", diff --git a/rust/plugin_runtime/src/plugins/applications/windows.rs b/rust/plugin_runtime/src/plugins/applications/windows.rs index 108aee9..e82194c 100644 --- a/rust/plugin_runtime/src/plugins/applications/windows.rs +++ b/rust/plugin_runtime/src/plugins/applications/windows.rs @@ -55,7 +55,7 @@ fn windows_open_application(#[string] file_path: String) -> anyhow::Result<()> { #[op2(async)] #[serde] async fn windows_app_from_path(#[string] file_path: String) -> anyhow::Result> { - Ok(spawn_blocking(|| windows_app_from_path_blocking(file_path)).await?) + spawn_blocking(|| windows_app_from_path_blocking(file_path)).await } fn windows_app_from_path_blocking(file_path: String) -> anyhow::Result> { From 033f2c8d82948dbd7488ee681bdb03f6512e680f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 10 Jan 2025 22:02:24 +0100 Subject: [PATCH 281/540] Fix build on windows --- rust/plugin_runtime/src/plugins/applications/windows.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/plugin_runtime/src/plugins/applications/windows.rs b/rust/plugin_runtime/src/plugins/applications/windows.rs index e82194c..4a2cd06 100644 --- a/rust/plugin_runtime/src/plugins/applications/windows.rs +++ b/rust/plugin_runtime/src/plugins/applications/windows.rs @@ -55,7 +55,7 @@ fn windows_open_application(#[string] file_path: String) -> anyhow::Result<()> { #[op2(async)] #[serde] async fn windows_app_from_path(#[string] file_path: String) -> anyhow::Result> { - spawn_blocking(|| windows_app_from_path_blocking(file_path)).await + spawn_blocking(|| windows_app_from_path_blocking(file_path)).await? } fn windows_app_from_path_blocking(file_path: String) -> anyhow::Result> { From d3ac37996ae31a86a3b2674cdda58c9b52ae0eaf Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 11 Jan 2025 19:48:58 +0100 Subject: [PATCH 282/540] Do not call git pull in build workflow to fix build on pr branches --- .github/workflows/setup-linux.yaml | 4 +--- .github/workflows/setup-macos.yaml | 4 +--- .github/workflows/setup-windows.yaml | 4 +--- js/build/src/main.ts | 23 ++++++++++++++++++++++- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/.github/workflows/setup-linux.yaml b/.github/workflows/setup-linux.yaml index ba86a02..a02813a 100644 --- a/.github/workflows/setup-linux.yaml +++ b/.github/workflows/setup-linux.yaml @@ -21,9 +21,7 @@ jobs: - run: sudo apt-get install -y libxkbcommon-dev - uses: actions/checkout@v4 - with: - submodules: true - - run: git pull + - uses: actions/setup-node@v4 with: node-version: 22 diff --git a/.github/workflows/setup-macos.yaml b/.github/workflows/setup-macos.yaml index 14c3fc6..98104f3 100644 --- a/.github/workflows/setup-macos.yaml +++ b/.github/workflows/setup-macos.yaml @@ -20,9 +20,7 @@ jobs: echo Killing XProtect.; sudo pkill -9 XProtect >/dev/null || true; - uses: actions/checkout@v4 - with: - submodules: true - - run: git pull + - uses: actions/setup-node@v4 with: node-version: 22 diff --git a/.github/workflows/setup-windows.yaml b/.github/workflows/setup-windows.yaml index dd7eb99..380d448 100644 --- a/.github/workflows/setup-windows.yaml +++ b/.github/workflows/setup-windows.yaml @@ -15,9 +15,7 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v4 - with: - submodules: true - - run: git pull + - uses: actions/setup-node@v4 with: node-version: 22 diff --git a/js/build/src/main.ts b/js/build/src/main.ts index 0f454e5..13dcada 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -102,6 +102,11 @@ async function doBuildLinux() { const arch = 'x86_64-unknown-linux-gnu'; const projectRoot = getProjectRoot(); + const git = simpleGit(projectRoot); + + console.log("git pull...") + await git.pull() + await doBuild(projectRoot, arch) packageForLinux(projectRoot, arch) } @@ -109,6 +114,11 @@ async function doBuildLinux() { async function doPublishMacOS() { const projectRoot = getProjectRoot(); + const git = simpleGit(projectRoot); + + console.log("git pull...") + await git.pull() + const arch = 'aarch64-apple-darwin'; build(projectRoot, arch) @@ -129,6 +139,11 @@ async function doBuildMacOS() { async function doPublishWindows() { const projectRoot = getProjectRoot(); + const git = simpleGit(projectRoot); + + console.log("git pull...") + await git.pull() + const arch = 'x86_64-pc-windows-msvc'; build(projectRoot, arch) @@ -147,9 +162,15 @@ async function doBuildWindows() { } async function doPublishFinal() { - console.log("Publishing Gauntlet npm packages...") const projectRoot = getProjectRoot() + const git = simpleGit(projectRoot); + + console.log("git pull...") + await git.pull() + + console.log("Publishing Gauntlet npm packages...") + buildJs(projectRoot) await publishNpmPackage(projectRoot) From a8666b1f57efe867b572efe18e8cca877f7eeadc Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 11 Jan 2025 20:09:44 +0100 Subject: [PATCH 283/540] Use pull_request_target instead of pull_request --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e6e4dc1..4d52eea 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,7 +2,7 @@ name: build on: push: - pull_request: + pull_request_target: branches: - main From 4611d06a8ba4bcdd0b9509be87916bd2e584ef58 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 11 Jan 2025 20:10:13 +0100 Subject: [PATCH 284/540] Do not sign binaries in build workflow --- js/build/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/build/src/main.ts b/js/build/src/main.ts index 13dcada..6c4462e 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -133,7 +133,7 @@ async function doBuildMacOS() { const arch = 'aarch64-apple-darwin'; await doBuild(projectRoot, arch) - await packageForMacos(projectRoot, arch, true, false) + await packageForMacos(projectRoot, arch, false, false) } async function doPublishWindows() { From 026bdf9b59e8a96ac373572050e2ae313525bf99 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 11 Jan 2025 21:02:06 +0100 Subject: [PATCH 285/540] Wire up the Cosmic support for window tracking --- .../src/plugins/applications/linux/wayland/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs b/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs index 1288ea9..b82b757 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs @@ -89,7 +89,11 @@ impl WaylandState { let inner = wlr::WlrWaylandState::new(globals, queue_handle) .map(|state| WaylandStateInner::Wlr(state)) - .or_else(|_| anyhow::Ok(WaylandStateInner::None))?; + .or_else(|test| { + cosmic::CosmicWaylandState::new(globals, queue_handle) + .map(|state| WaylandStateInner::Cosmic(state)) + }) + .unwrap_or(WaylandStateInner::None); Ok(WaylandState { seat_state, From d57d10d1a8a47f0d9989f6ad2c5aa3b9b5d8bcd0 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:50:54 +0100 Subject: [PATCH 286/540] Remove preferences helper functions. Add preferences react hooks. Rename GeneratorProps to GeneratorContext. Add preferences to GeneratorContext --- bundled_plugins/gauntlet/src/applications.tsx | 46 +++++++++---------- dev_plugin/src/detail-view.tsx | 8 ++-- dev_plugin/src/entrypoint-generator.tsx | 6 +-- js/api/src/helpers.ts | 12 ++--- js/api/src/hooks.ts | 12 +++++ js/core/src/entrypoint-generator.ts | 17 +++++-- 6 files changed, 57 insertions(+), 44 deletions(-) diff --git a/bundled_plugins/gauntlet/src/applications.tsx b/bundled_plugins/gauntlet/src/applications.tsx index 03c4910..f9a22d5 100644 --- a/bundled_plugins/gauntlet/src/applications.tsx +++ b/bundled_plugins/gauntlet/src/applications.tsx @@ -1,4 +1,4 @@ -import { GeneratedEntrypoint, GeneratorProps } from "@project-gauntlet/api/helpers"; +import { GeneratedEntrypoint, GeneratorContext } from "@project-gauntlet/api/helpers"; import { walk, WalkOptions } from "@std/fs/walk"; import { debounce } from "@std/async/debounce"; import { current_os, wayland } from "gauntlet:bridge/internal-all"; @@ -20,7 +20,7 @@ import { applicationEventLoopX11, focusX11Window } from "./window/x11"; import { applicationEventLoopWayland, focusWaylandWindow } from "./window/wayland"; import { windows_app_from_path, windows_application_dirs, windows_open_application } from "gauntlet:bridge/internal-windows"; -export default async function Applications({ add, remove, get, getAll }: GeneratorProps): Promise void)> { +export default async function Applications({ add, remove, get, getAll }: GeneratorContext): Promise void)> { switch (current_os()) { case "linux": { @@ -68,27 +68,27 @@ export default async function Applications({ add, remove, get, getAll }: Generat remove, ); - if (wayland()) { - try { - applicationEventLoopWayland( - focusWaylandWindow, - add, - get, - getAll - ); - } catch (e) { - console.log("error when setting up wayland application event loop", e) - } - } else { - try { - applicationEventLoopX11( - focusX11Window, - add, - get, - getAll - ); - } catch (e) { - console.log("error when setting up x11 application event loop", e) + if (wayland()) { + try { + applicationEventLoopWayland( + focusWaylandWindow, + add, + get, + getAll + ); + } catch (e) { + console.log("error when setting up wayland application event loop", e) + } + } else { + try { + applicationEventLoopX11( + focusX11Window, + add, + get, + getAll + ); + } catch (e) { + console.log("error when setting up x11 application event loop", e) } } diff --git a/dev_plugin/src/detail-view.tsx b/dev_plugin/src/detail-view.tsx index f3364c9..ef92776 100644 --- a/dev_plugin/src/detail-view.tsx +++ b/dev_plugin/src/detail-view.tsx @@ -1,8 +1,8 @@ import { ReactElement, useEffect, useState } from 'react'; import upperCase from "lodash/upperCase"; import { Action, ActionPanel, Detail, Icons } from "@project-gauntlet/api/components"; -import { useNavigation } from "@project-gauntlet/api/hooks"; -import { Clipboard, entrypointPreferences, pluginPreferences } from "@project-gauntlet/api/helpers"; +import { useEntrypointPreferences, useNavigation, usePluginPreferences } from "@project-gauntlet/api/hooks"; +import { Clipboard } from "@project-gauntlet/api/helpers"; async function readFile(url: string): Promise { const res = await fetch(url); @@ -55,8 +55,8 @@ export default function DetailView(): ReactElement { const [count, setCount] = useState(0); const { pushView } = useNavigation(); - const { testBool } = pluginPreferences<{ testBool: boolean }>(); - const preferences = entrypointPreferences(); + const { testBool } = usePluginPreferences<{ testBool: boolean }>(); + const preferences = useEntrypointPreferences(); const env = Deno.env.get("RUST_LOG"); console.log("RUST_LOG:", env); diff --git a/dev_plugin/src/entrypoint-generator.tsx b/dev_plugin/src/entrypoint-generator.tsx index ae1f8c4..5236ed6 100644 --- a/dev_plugin/src/entrypoint-generator.tsx +++ b/dev_plugin/src/entrypoint-generator.tsx @@ -1,4 +1,4 @@ -import { GeneratorProps, showHud } from "@project-gauntlet/api/helpers"; +import { GeneratorContext, showHud } from "@project-gauntlet/api/helpers"; import { ReactElement } from "react"; import { List } from "@project-gauntlet/api/components"; @@ -11,7 +11,7 @@ function ListView(): ReactElement { } -export default function EntrypointGenerator({ add, remove: _ }: GeneratorProps): void { +export default function EntrypointGenerator({ add }: GeneratorContext): void { add('generated-test-1', { name: 'Generated Item 1', actions: [ @@ -82,7 +82,7 @@ export default function EntrypointGenerator({ add, remove: _ }: GeneratorProps): ], accessories: [ { - text: "1 window open" + text: "Accessory" } ], }) diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index 0a2e5ee..210f745 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -21,14 +21,6 @@ export function assetData(path: string): Promise { return getAssetData(path) } -export function pluginPreferences>(): T { - return getPluginPreferences() -} - -export function entrypointPreferences>(): T { - return getEntrypointPreferences() -} - export function showHud(display: string): void { return showHudWindow(display) } @@ -67,11 +59,13 @@ export interface GeneratedEntrypointIconAccessory { tooltip?: string } -export type GeneratorProps = { +export type GeneratorContext

= { add: (id: string, data: GeneratedEntrypoint) => void, remove: (id: string) => void, get: (id: string) => GeneratedEntrypoint | undefined getAll: () => { [id: string]: GeneratedEntrypoint }, + pluginPreferences: P, + entrypointPreferences: E, }; export const Clipboard: Clipboard = { diff --git a/js/api/src/hooks.ts b/js/api/src/hooks.ts index aa1f8cf..5d4527c 100644 --- a/js/api/src/hooks.ts +++ b/js/api/src/hooks.ts @@ -15,6 +15,18 @@ export function useNavigation(): { popView: () => void, pushView: (component: Re } } +export function usePluginPreferences>(): T { + const { pluginPreferences }: { pluginPreferences: () => T } = useGauntletContext(); + + return pluginPreferences() +} + +export function useEntrypointPreferences>(): T { + const { entrypointPreferences }: { entrypointPreferences: () => T } = useGauntletContext(); + + return entrypointPreferences() +} + export type AsyncState = { isLoading: boolean; error?: unknown; diff --git a/js/core/src/entrypoint-generator.ts b/js/core/src/entrypoint-generator.ts index 69fc2a4..2e9ab6b 100644 --- a/js/core/src/entrypoint-generator.ts +++ b/js/core/src/entrypoint-generator.ts @@ -3,7 +3,9 @@ import { get_entrypoint_generator_entrypoint_ids, op_log_info, op_log_debug, - update_loading_bar + update_loading_bar, + get_plugin_preferences, + get_entrypoint_preferences } from "ext:core/ops"; import { reloadSearchIndex } from "./search-index"; import type { FC } from "react"; @@ -30,14 +32,16 @@ interface GeneratedEntrypointActionView { view: FC } -type GeneratorProps = { +export type GeneratorContext

= { add: (id: string, data: GeneratedEntrypoint) => void, remove: (id: string) => void, get: (id: string) => GeneratedEntrypoint | undefined - getAll: () => { [id: string]: GeneratedEntrypoint } + getAll: () => { [id: string]: GeneratedEntrypoint }, + pluginPreferences: P, + entrypointPreferences: E, }; -type Generator = (props: GeneratorProps) => void | (() => (void | Promise)) | Promise (void | Promise))> +type Generator = (props: GeneratorContext) => void | (() => (void | Promise)) | Promise (void | Promise))> type ProcessedGeneratedEntrypoint = { generatorEntrypointId: string, @@ -172,11 +176,14 @@ export async function runEntrypointGenerators(): Promise { ) } + const pluginPreferences = get_plugin_preferences(); + const entrypointPreferences = get_entrypoint_preferences(generatorEntrypointId); + // noinspection ES6MissingAwait (async () => { try { update_loading_bar(generatorEntrypointId, true) - let cleanup = await generator({ add, remove, get, getAll }) + let cleanup = await generator({ add, remove, get, getAll, pluginPreferences, entrypointPreferences }) update_loading_bar(generatorEntrypointId, false) if (typeof cleanup === "function") { generatorCleanups[generatorEntrypointId] = cleanup From 61b71c8858a64d4616c9425f3b0115658d0ede40 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 11 Jan 2025 23:01:22 +0100 Subject: [PATCH 287/540] Re-run entrypoint generators after changing preference value --- rust/server/src/plugins/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index ad8d613..32e6193 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -292,6 +292,8 @@ impl ApplicationManager { self.db_repository.set_preference_value(plugin_id.to_string(), entrypoint_id.map(|id| id.to_string()), preference_id, user_data) .await?; + self.request_search_index_reload(plugin_id); + Ok(()) } From 4304d35d6a48bad4b0c5120a3b07bfad1f6f452a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 11 Jan 2025 23:23:08 +0100 Subject: [PATCH 288/540] Reload whole plugin when changing entrypoint state or preference value instead of just reloading search index --- js/core/src/core.tsx | 4 ---- js/typings/index.d.ts | 6 +----- rust/common/src/model.rs | 14 +++++++------- rust/plugin_runtime/src/events.rs | 1 - rust/server/src/model.rs | 1 - rust/server/src/plugins/js.rs | 5 ----- rust/server/src/plugins/mod.rs | 12 +++--------- 7 files changed, 11 insertions(+), 32 deletions(-) diff --git a/js/core/src/core.tsx b/js/core/src/core.tsx index 8d88a25..2f3aa74 100644 --- a/js/core/src/core.tsx +++ b/js/core/src/core.tsx @@ -130,10 +130,6 @@ export async function runPluginLoop() { } break; } - case "ReloadSearchIndex": { - runEntrypointGenerators() - break; - } case "RefreshSearchIndex": { // noinspection ES6MissingAwait reloadSearchIndex(false) diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 5ecd95f..adb9292 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -45,7 +45,7 @@ type MacOSDesktopSettings13AndPostData = { icon: ArrayBuffer | undefined, } -type PluginEvent = ViewEvent | NotReactsKeyboardEvent | RunCommand | RunGeneratedEntrypoint | OpenView | CloseView | OpenInlineView | ReloadSearchIndex | RefreshSearchIndex +type PluginEvent = ViewEvent | NotReactsKeyboardEvent | RunCommand | RunGeneratedEntrypoint | OpenView | CloseView | OpenInlineView | RefreshSearchIndex type RenderLocation = "InlineView" | "View" type ViewEvent = { @@ -94,10 +94,6 @@ type OpenInlineView = { text: string } -type ReloadSearchIndex = { - type: "ReloadSearchIndex" -} - type RefreshSearchIndex = { type: "RefreshSearchIndex" } diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index ba874ed..c6d94c5 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::fmt::Display; +use std::fmt::{Display, Formatter}; use std::path::PathBuf; use std::sync::Arc; @@ -54,9 +54,9 @@ impl PluginId { } } -impl ToString for PluginId { - fn to_string(&self) -> String { - self.0.to_string() +impl Display for PluginId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) } } @@ -69,9 +69,9 @@ impl EntrypointId { } } -impl ToString for EntrypointId { - fn to_string(&self) -> String { - self.0.to_string() +impl Display for EntrypointId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) } } diff --git a/rust/plugin_runtime/src/events.rs b/rust/plugin_runtime/src/events.rs index e32434e..cf7d0d8 100644 --- a/rust/plugin_runtime/src/events.rs +++ b/rust/plugin_runtime/src/events.rs @@ -53,7 +53,6 @@ pub enum JsEvent { #[serde(rename = "text")] text: String, }, - ReloadSearchIndex, RefreshSearchIndex, } diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 28c93a1..c55a86c 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -31,7 +31,6 @@ pub enum IntermediateUiEvent { OpenInlineView { text: String, }, - ReloadSearchIndex, RefreshSearchIndex, } diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 931802b..a7b2587 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -116,7 +116,6 @@ pub enum OnePluginCommandData { modifier_alt: bool, modifier_meta: bool, }, - ReloadSearchIndex, RefreshSearchIndex, } @@ -406,9 +405,6 @@ async fn event_loop(command_receiver: &mut tokio::sync::broadcast::Receiver { - Some(IntermediateUiEvent::ReloadSearchIndex) - } OnePluginCommandData::RefreshSearchIndex => { Some(IntermediateUiEvent::RefreshSearchIndex) } @@ -654,7 +650,6 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsEvent { } } IntermediateUiEvent::OpenInlineView { text } => JsEvent::OpenInlineView { text }, - IntermediateUiEvent::ReloadSearchIndex => JsEvent::ReloadSearchIndex, IntermediateUiEvent::RefreshSearchIndex => JsEvent::RefreshSearchIndex, } } diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 32e6193..677e99c 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -263,7 +263,8 @@ impl ApplicationManager { self.db_repository.set_plugin_entrypoint_enabled(&plugin_id.to_string(), &entrypoint_id.to_string(), enabled) .await?; - self.request_search_index_reload(plugin_id); + + self.reload_plugin(plugin_id.clone()).await?; Ok(()) } @@ -292,7 +293,7 @@ impl ApplicationManager { self.db_repository.set_preference_value(plugin_id.to_string(), entrypoint_id.map(|id| id.to_string()), preference_id, user_data) .await?; - self.request_search_index_reload(plugin_id); + self.reload_plugin(plugin_id.clone()).await?; Ok(()) } @@ -417,13 +418,6 @@ impl ApplicationManager { }) } - pub fn request_search_index_reload(&self, plugin_id: PluginId) { - self.send_command(PluginCommand::One { - id: plugin_id, - data: OnePluginCommandData::ReloadSearchIndex - }) - } - pub fn request_search_index_refresh(&self, plugin_id: PluginId) { self.send_command(PluginCommand::One { id: plugin_id, From a905795c36ecc1eaf4e0482e98d930ace3b08714 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 11 Jan 2025 23:24:33 +0100 Subject: [PATCH 289/540] Add preference to enable window tracking. Currently disabled by default --- bundled_plugins/gauntlet/gauntlet.toml | 7 +++++ bundled_plugins/gauntlet/src/applications.tsx | 13 +++++++--- .../gauntlet/src/window/shared.tsx | 26 +++++++++++++++---- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/bundled_plugins/gauntlet/gauntlet.toml b/bundled_plugins/gauntlet/gauntlet.toml index 8501ab5..a448f8e 100644 --- a/bundled_plugins/gauntlet/gauntlet.toml +++ b/bundled_plugins/gauntlet/gauntlet.toml @@ -9,6 +9,13 @@ path = 'src/applications.tsx' type = 'entrypoint-generator' description = 'Run installed applications from your system' +[[entrypoint.preferences]] +id = 'windowTracking' +name = 'Window Tracking' +type = 'bool' +default = false +description = "Enables Window Tracking" + [[entrypoint]] id = 'windows' name = 'All Open Windows' diff --git a/bundled_plugins/gauntlet/src/applications.tsx b/bundled_plugins/gauntlet/src/applications.tsx index f9a22d5..027f4f6 100644 --- a/bundled_plugins/gauntlet/src/applications.tsx +++ b/bundled_plugins/gauntlet/src/applications.tsx @@ -20,7 +20,10 @@ import { applicationEventLoopX11, focusX11Window } from "./window/x11"; import { applicationEventLoopWayland, focusWaylandWindow } from "./window/wayland"; import { windows_app_from_path, windows_application_dirs, windows_open_application } from "gauntlet:bridge/internal-windows"; -export default async function Applications({ add, remove, get, getAll }: GeneratorContext): Promise void)> { +type EntrypointPreferences = { windowTracking: boolean }; + +export default async function Applications(context: GeneratorContext): Promise void)> { + const { add, remove, get, getAll, entrypointPreferences: { windowTracking } } = context; switch (current_os()) { case "linux": { @@ -33,12 +36,13 @@ export default async function Applications({ add, remove, get, getAll }: Generat name: data.name, actions: applicationActions( id, + windowTracking, () => { linux_open_application(id) }, focusWaylandWindow, ), - accessories: applicationAccessories(id), + accessories: applicationAccessories(id, windowTracking), icon: data.icon, // TODO lazy icons "__linux__": { startupWmClass: data.startup_wm_class, @@ -50,12 +54,13 @@ export default async function Applications({ add, remove, get, getAll }: Generat name: data.name, actions: applicationActions( id, + windowTracking, () => { linux_open_application(id) }, focusX11Window, ), - accessories: applicationAccessories(id), + accessories: applicationAccessories(id, windowTracking), icon: data.icon, // TODO lazy icons "__linux__": { startupWmClass: data.startup_wm_class, @@ -68,6 +73,7 @@ export default async function Applications({ add, remove, get, getAll }: Generat remove, ); + if (windowTracking) { if (wayland()) { try { applicationEventLoopWayland( @@ -89,6 +95,7 @@ export default async function Applications({ add, remove, get, getAll }: Generat ); } catch (e) { console.log("error when setting up x11 application event loop", e) + } } } diff --git a/bundled_plugins/gauntlet/src/window/shared.tsx b/bundled_plugins/gauntlet/src/window/shared.tsx index de9b1c0..96506fb 100644 --- a/bundled_plugins/gauntlet/src/window/shared.tsx +++ b/bundled_plugins/gauntlet/src/window/shared.tsx @@ -45,9 +45,21 @@ export const openWindows: Record = {}; export function applicationActions( id: string, + windowTracking: boolean, openApplication: () => void, focusWindow: (windowId: string) => void, ): GeneratedEntrypointAction[] { + if (!windowTracking) { + return [ + { + label: "Open application", + run: () => { + openApplication() + }, + } + ] + } + const appWindows = Object.entries(openWindows) .filter(([_, windowData]) => windowData.appId == id) @@ -90,7 +102,11 @@ export function applicationActions( } } -export function applicationAccessories(id: string): GeneratedEntrypointAccessory[] { +export function applicationAccessories(id: string, windowTracking: boolean): GeneratedEntrypointAccessory[] { + if (!windowTracking) { + return [] + } + const appWindows = Object.entries(openWindows) .filter(([_, windowData]) => windowData.appId == id) @@ -123,8 +139,8 @@ export function addOpenWindow( add(appId, { ...generatedEntrypoint, - actions: applicationActions(appId, openApplication, focusWindow), - accessories: applicationAccessories(appId) + actions: applicationActions(appId, true, openApplication, focusWindow), + accessories: applicationAccessories(appId, true) }) } } @@ -145,8 +161,8 @@ export function deleteOpenWindow( if (generatedEntrypoint) { add(openWindow.appId, { ...generatedEntrypoint, - actions: applicationActions(openWindow.appId, openApplication(openWindow.appId), focusWindow), - accessories: applicationAccessories(openWindow.appId) + actions: applicationActions(openWindow.appId, true, openApplication(openWindow.appId), focusWindow), + accessories: applicationAccessories(openWindow.appId, true) }) } } From e7c85753221079802f84c273476f90f69805e41f Mon Sep 17 00:00:00 2001 From: davfsa Date: Sat, 11 Jan 2025 14:06:25 +0100 Subject: [PATCH 290/540] Reduce release binary size `strip = true` alone reduces the size by 40MB! --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 0d880f4..cec3740 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,11 @@ gauntlet-cli = { path = "rust/cli" } release = ["gauntlet-cli/release"] scenario_runner = ["gauntlet-cli/scenario_runner"] +[profile.release] +opt-level = "s" +lto = true +strip = true + [patch.crates-io] # NOTE https://github.com/ipetkov/crane/issues/336 libffi-sys = { git = "https://github.com/tov/libffi-rs", rev = "d0704d634b6f3ffef5b6fc7e07fe965a1cff5c7b" } From b59762bd5b7f036b8744ef4e05b5054eed252185 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 12 Jan 2025 20:17:57 +0100 Subject: [PATCH 291/540] Fix icons on macOS sometimes not loading --- rust/client/src/ui/search_list.rs | 2 +- rust/common/src/model.rs | 2 +- rust/server/src/plugins/icon_cache.rs | 28 +-------------------------- rust/server/src/plugins/js.rs | 22 +++++++-------------- rust/server/src/search.rs | 8 ++++---- 5 files changed, 14 insertions(+), 48 deletions(-) diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index 7e9b068..ee9a42e 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -50,7 +50,7 @@ pub fn search_list<'a>( let mut button_content = vec![]; if let Some(path) = &search_result.entrypoint_icon { - let image: Element<_> = iced::widget::image(Handle::from_path(path)) + let image: Element<_> = iced::widget::image(Handle::from_bytes(path.clone())) .themed(ImageStyle::MainListItemIcon); let image: Element<_> = container(image) diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index c6d94c5..1e0547e 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -112,7 +112,7 @@ pub struct SearchResult { pub entrypoint_id: EntrypointId, pub entrypoint_name: String, pub entrypoint_generator_name: Option, - pub entrypoint_icon: Option, + pub entrypoint_icon: Option, pub entrypoint_type: SearchResultEntrypointType, pub entrypoint_actions: Vec, pub entrypoint_accessories: Vec, diff --git a/rust/server/src/plugins/icon_cache.rs b/rust/server/src/plugins/icon_cache.rs index e57f21b..0c411bd 100644 --- a/rust/server/src/plugins/icon_cache.rs +++ b/rust/server/src/plugins/icon_cache.rs @@ -1,4 +1,3 @@ -use anyhow::anyhow; use gauntlet_common::dirs::Dirs; #[derive(Clone)] @@ -13,6 +12,7 @@ impl IconCache { } } + // legacy pub fn clear_all_icon_cache_dir(&self) -> anyhow::Result<()> { let cache_dir = self.dirs.icon_cache_dir(); std::fs::create_dir_all(&cache_dir)?; @@ -23,32 +23,6 @@ impl IconCache { Ok(()) } - - pub fn clear_plugin_icon_cache_dir(&self, plugin_uuid: &str) -> anyhow::Result<()> { - let cache_dir = self.dirs.icon_cache_dir(); - let plugin_cache_dir = cache_dir.join(plugin_uuid); - - if plugin_cache_dir.exists() { - std::fs::remove_dir_all(&plugin_cache_dir)?; - } - - Ok(()) - } - - pub fn save_entrypoint_icon_to_cache(&self, plugin_uuid: &str, entrypoint_uuid: &str, data: impl AsRef<[u8]>) -> anyhow::Result { - let cache_dir = self.dirs.icon_cache_dir(); - let plugin_cache_dir = cache_dir.join(plugin_uuid); - std::fs::create_dir_all(&plugin_cache_dir)?; - - let path_to_icon = plugin_cache_dir.join(format!("{}.png", &entrypoint_uuid)); - - std::fs::write(&path_to_icon, data).expect(&format!("unable to create icon file {:?}", &path_to_icon)); - - let path_to_icon = path_to_icon.to_str() - .ok_or(anyhow!("unable to convert {:?} to utf-8 while saving icon to cache", &path_to_icon))?; - - Ok(path_to_icon.to_string()) - } } diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index a7b2587..a4b1cef 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -12,7 +12,6 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, Context}; -use bytes::Bytes; use futures::AsyncBufReadExt; use interprocess::local_socket::{ListenerOptions, ToFsName, ToNsName}; use interprocess::local_socket::tokio::{RecvHalf, SendHalf}; @@ -334,10 +333,6 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run drop((recver, sender)); - if let Err(err) = cache.clear_plugin_icon_cache_dir(&plugin_uuid) { - tracing::error!(target = "plugin", "plugin {:?} unable to cleanup icon cache {:?}", plugin_id, err) - } - #[cfg(not(feature = "scenario_runner"))] { let code = runtime_process.wait() @@ -695,9 +690,6 @@ impl BackendForPluginRuntimeApiImpl { impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { async fn reload_search_index(&self, generated_entrypoints: Vec, refresh_search_list: bool) -> anyhow::Result<()> { - self.icon_cache.clear_plugin_icon_cache_dir(&self.plugin_uuid) - .context("error when clearing up icon cache before recreating it")?; - let DbReadPlugin { name, .. } = self.repository.get_plugin_by_id(&self.plugin_id.to_string()) .await .context("error when getting plugin by id")?; @@ -724,9 +716,9 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { let mut generated_search_items = generated_entrypoints.into_iter() .map(|item| { - let entrypoint_icon_path = match item.entrypoint_icon { + let entrypoint_icon = match item.entrypoint_icon { None => None, - Some(data) => Some(self.icon_cache.save_entrypoint_icon_to_cache(&self.plugin_uuid, &item.entrypoint_uuid, &data)?), + Some(data) => Some(bytes::Bytes::from(data)), }; let entrypoint_frecency = frecency_map.get(&item.entrypoint_id).cloned().unwrap_or(0.0); @@ -775,7 +767,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { entrypoint_type: SearchResultEntrypointType::Generated, entrypoint_id: EntrypointId::from_string(item.entrypoint_id), entrypoint_name: item.entrypoint_name, - entrypoint_icon_path, + entrypoint_icon, entrypoint_frecency, entrypoint_actions, entrypoint_accessories, @@ -805,12 +797,12 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { let entrypoint_frecency = frecency_map.get(&entrypoint_id).cloned().unwrap_or(0.0); - let entrypoint_icon_path = match entrypoint.icon_path { + let entrypoint_icon = match entrypoint.icon_path { None => None, Some(path_to_asset) => { match icon_asset_data.get(&(entrypoint.id, path_to_asset)) { None => None, - Some(data) => Some(self.icon_cache.save_entrypoint_icon_to_cache(&self.plugin_uuid, &entrypoint.uuid, data)?) + Some(data) => Some(bytes::Bytes::copy_from_slice(data)) } }, }; @@ -824,7 +816,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { entrypoint_name: entrypoint.name, entrypoint_generator_name: None, entrypoint_id, - entrypoint_icon_path, + entrypoint_icon, entrypoint_frecency, entrypoint_actions: vec![], entrypoint_accessories: vec![], @@ -836,7 +828,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { entrypoint_name: entrypoint.name, entrypoint_generator_name: None, entrypoint_id, - entrypoint_icon_path, + entrypoint_icon, entrypoint_frecency, entrypoint_actions: vec![], entrypoint_accessories: vec![], diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index fd6391b..3a06299 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -27,7 +27,7 @@ pub struct SearchIndex { struct EntrypointData { entrypoint_generator_name: Option, entrypoint_type: SearchResultEntrypointType, - icon_path: Option, + icon: Option, frecency: f64, actions: Vec, accessories: Vec, @@ -50,7 +50,7 @@ pub struct SearchIndexItem { pub entrypoint_name: String, pub entrypoint_generator_name: Option, pub entrypoint_id: EntrypointId, - pub entrypoint_icon_path: Option, + pub entrypoint_icon: Option, pub entrypoint_frecency: f64, pub entrypoint_actions: Vec, pub entrypoint_accessories: Vec, @@ -167,7 +167,7 @@ impl SearchIndex { let data = EntrypointData { entrypoint_generator_name: item.entrypoint_generator_name, entrypoint_type: item.entrypoint_type, - icon_path: item.entrypoint_icon_path, + icon: item.entrypoint_icon, frecency: item.entrypoint_frecency, actions, accessories: item.entrypoint_accessories, @@ -293,7 +293,7 @@ impl SearchIndex { entrypoint_name, entrypoint_generator_name: entrypoint_data.entrypoint_generator_name.clone(), entrypoint_id, - entrypoint_icon: entrypoint_data.icon_path.clone(), + entrypoint_icon: entrypoint_data.icon.clone(), plugin_name, plugin_id, entrypoint_actions, From ea527a7ece5f2e33f2de085dfed766f52030ac26 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 12 Jan 2025 20:25:51 +0100 Subject: [PATCH 292/540] Rename Window Tracking preference into Experimental Window Tracking --- bundled_plugins/gauntlet/gauntlet.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundled_plugins/gauntlet/gauntlet.toml b/bundled_plugins/gauntlet/gauntlet.toml index a448f8e..03ca1a9 100644 --- a/bundled_plugins/gauntlet/gauntlet.toml +++ b/bundled_plugins/gauntlet/gauntlet.toml @@ -11,10 +11,10 @@ description = 'Run installed applications from your system' [[entrypoint.preferences]] id = 'windowTracking' -name = 'Window Tracking' +name = 'Experimental Window Tracking' type = 'bool' default = false -description = "Enables Window Tracking" +description = "Enables experimental window tracking" [[entrypoint]] id = 'windows' From 8ffe84c4404d0af89d648320316130354a3a9843 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 12 Jan 2025 20:38:41 +0100 Subject: [PATCH 293/540] Use size optimized cargo profile only on releases, exclude nix as well --- Cargo.toml | 2 +- js/build/src/main.ts | 23 ++++++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cec3740..2edf73f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,7 @@ gauntlet-cli = { path = "rust/cli" } release = ["gauntlet-cli/release"] scenario_runner = ["gauntlet-cli/scenario_runner"] -[profile.release] +[profile.release-size] opt-level = "s" lto = true strip = true diff --git a/js/build/src/main.ts b/js/build/src/main.ts index 6c4462e..c6c5295 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -91,7 +91,7 @@ async function doPublishLinux() { const arch = 'x86_64-unknown-linux-gnu'; - build(projectRoot, arch) + buildSize(projectRoot, arch) const { fileName, filePath } = packageForLinux(projectRoot, arch) @@ -121,7 +121,7 @@ async function doPublishMacOS() { const arch = 'aarch64-apple-darwin'; - build(projectRoot, arch) + buildSize(projectRoot, arch) const { fileName, filePath } = await packageForMacos(projectRoot, arch, true, true) @@ -146,7 +146,7 @@ async function doPublishWindows() { const arch = 'x86_64-pc-windows-msvc'; - build(projectRoot, arch) + buildSize(projectRoot, arch) const { fileName, filePath } = await packageForWindows(projectRoot, arch) @@ -179,12 +179,29 @@ async function doPublishFinal() { function build(projectRoot: string, arch: string) { buildJs(projectRoot) + buildRust(projectRoot, arch) +} + +function buildSize(projectRoot: string, arch: string) { + buildJs(projectRoot) + + buildRustSize(projectRoot, arch) +} + +function buildRust(projectRoot: string, arch: string) { console.log("Building rust...") spawnWithErrors('cargo', ['build', '--release', '--features', 'release', '--target', arch], { cwd: projectRoot }); } +function buildRustSize(projectRoot: string, arch: string) { + console.log("Building rust...") + spawnWithErrors('cargo', ['build', '--profile', 'release-size', '--features', 'release', '--target', arch], { + cwd: projectRoot + }); +} + function buildJs(projectRoot: string) { console.log("Building js...") spawnWithErrors('npm', ['run', 'build'], { cwd: projectRoot }); From bb1562a36246b521952928904675148db958e84b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 12 Jan 2025 20:41:58 +0100 Subject: [PATCH 294/540] Fix invalid cargo profile --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 2edf73f..0a86411 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ release = ["gauntlet-cli/release"] scenario_runner = ["gauntlet-cli/scenario_runner"] [profile.release-size] +inherits = "release" opt-level = "s" lto = true strip = true From 565519fa3094f2ac443151e729f2b86cd4f747cb Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 12 Jan 2025 21:47:11 +0100 Subject: [PATCH 295/540] Add ability to control whether window is closed based on return value of action --- .../gauntlet/src/window/shared.tsx | 4 +--- js/core/src/render.tsx | 16 ++++++++++++--- js/typings/index.d.ts | 1 + rust/client/src/ui/mod.rs | 20 +++++++++++++++---- rust/common/src/model.rs | 1 + rust/common/src/rpc/frontend_api.rs | 8 ++++++++ rust/plugin_runtime/src/api.rs | 10 ++++++++++ rust/plugin_runtime/src/deno.rs | 3 ++- rust/plugin_runtime/src/model.rs | 1 + rust/plugin_runtime/src/ui.rs | 15 ++++++++++++++ rust/scenario_runner/src/frontend_mock.rs | 2 +- rust/server/src/plugins/js.rs | 11 ++++++++++ 12 files changed, 80 insertions(+), 12 deletions(-) diff --git a/bundled_plugins/gauntlet/src/window/shared.tsx b/bundled_plugins/gauntlet/src/window/shared.tsx index 96506fb..30a94f4 100644 --- a/bundled_plugins/gauntlet/src/window/shared.tsx +++ b/bundled_plugins/gauntlet/src/window/shared.tsx @@ -19,9 +19,7 @@ export function ListOfWindows({ windows, focus }: { windows: OpenWindowData[], f if (id) { focus(id) console.log("focus: " + id) - } else { - showHud("No window selected") - console.log("No window selected") + return { close: true } } }} /> diff --git a/js/core/src/render.tsx b/js/core/src/render.tsx index 42bb487..1c2f988 100644 --- a/js/core/src/render.tsx +++ b/js/core/src/render.tsx @@ -2,7 +2,8 @@ import { clear_inline_view, fetch_action_id_for_shortcut, op_log_debug, - op_log_trace + op_log_trace, + hide_window } from "ext:core/ops"; import { clearRenderer, render } from "ext:gauntlet/renderer.js"; import type { FC } from "react"; @@ -96,9 +97,18 @@ export function handleEvent(event: ViewEvent) { } }); - op_log_trace("plugin_event_handler", `Calling handler with arguments ${Deno.inspect(eventArgs)}`) + op_log_trace("plugin_event_handler", `Calling handler with arguments ${Deno.inspect(eventArgs)}`); - property(...eventArgs); + (async () => { + const result = await property(...eventArgs); + + // special case for action results + if (event.eventName == "onAction") { + if (result?.close === true) { + hide_window() + } + } + })(); } else { throw new Error(`Event handler has type ${typeof property}, but should be function`) } diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index adb9292..7ca15d8 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -224,6 +224,7 @@ declare module "ext:core/ops" { function op_entrypoint_names(): Record; function clear_inline_view(): void; function op_plugin_get_pending_event(): Promise; + function hide_window(): void; function get_entrypoint_generator_entrypoint_ids(): Promise diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 27ccd62..40c6df6 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -615,10 +615,17 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { id }; - Task::batch([ - // state.hide_window(), // TODO - Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) - ]) + match render_location { + UiRenderLocation::InlineView => { + Task::batch([ + state.hide_window(), + Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + ]) + } + UiRenderLocation::View => { + Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + } + } } AppMsg::RunSearchItemAction(search_result, action_index) => { match search_result.entrypoint_type { @@ -2298,6 +2305,11 @@ async fn request_loop( AppMsg::ShowWindow } + UiRequestData::HideWindow => { + responder.respond(UiResponseData::Nothing); + + AppMsg::HideWindow + } UiRequestData::ShowPreferenceRequiredView { plugin_id, entrypoint_id, diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 1e0547e..73aa054 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -218,6 +218,7 @@ pub enum UiResponseData { #[derive(Debug)] pub enum UiRequestData { ShowWindow, + HideWindow, ClearInlineView { plugin_id: PluginId }, diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index f8b4869..f1baf14 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -89,6 +89,14 @@ impl FrontendApi { Ok(()) } + pub async fn hide_window(&self) -> Result<(), FrontendApiError> { + let UiResponseData::Nothing = self.frontend_sender.send_receive(UiRequestData::HideWindow).await? else { + unreachable!() + }; + + Ok(()) + } + pub async fn show_preference_required_view( &self, plugin_id: PluginId, diff --git a/rust/plugin_runtime/src/api.rs b/rust/plugin_runtime/src/api.rs index 067c17a..0a49beb 100644 --- a/rust/plugin_runtime/src/api.rs +++ b/rust/plugin_runtime/src/api.rs @@ -21,6 +21,7 @@ pub trait BackendForPluginRuntimeApi { async fn clipboard_clear(&self) -> anyhow::Result<()>; async fn ui_update_loading_bar(&self, entrypoint_id: EntrypointId, show: bool) -> anyhow::Result<()>; async fn ui_show_hud(&self, display: String) -> anyhow::Result<()>; + async fn ui_hide_window(&self) -> anyhow::Result<()>; async fn ui_get_action_id_for_shortcut( &self, entrypoint_id: EntrypointId, @@ -222,6 +223,15 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { } } + async fn ui_hide_window(&self) -> anyhow::Result<()> { + let request = JsRequest::HideWindow; + + match self.request(request).await? { + JsResponse::Nothing => Ok(()), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + } + } + async fn ui_get_action_id_for_shortcut(&self, entrypoint_id: EntrypointId, key: String, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> anyhow::Result> { let request = JsRequest::GetActionIdForShortcut { entrypoint_id, diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index 73503d1..ea29d59 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -34,7 +34,7 @@ use crate::plugins::numbat::{run_numbat, NumbatContext}; use crate::plugins::settings::open_settings; use crate::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; use crate::search::reload_search_index; -use crate::ui::{clear_inline_view, fetch_action_id_for_shortcut, op_component_model, op_entrypoint_names, op_inline_view_entrypoint_id, op_react_replace_view, show_hud, show_plugin_error_view, show_preferences_required_view, update_loading_bar}; +use crate::ui::{clear_inline_view, fetch_action_id_for_shortcut, hide_window, op_component_model, op_entrypoint_names, op_inline_view_entrypoint_id, op_react_replace_view, show_hud, show_plugin_error_view, show_preferences_required_view, update_loading_bar}; @@ -184,6 +184,7 @@ deno_core::extension!( op_component_model, fetch_action_id_for_shortcut, show_hud, + hide_window, update_loading_bar, // preferences diff --git a/rust/plugin_runtime/src/model.rs b/rust/plugin_runtime/src/model.rs index 3753cf5..2c53544 100644 --- a/rust/plugin_runtime/src/model.rs +++ b/rust/plugin_runtime/src/model.rs @@ -127,6 +127,7 @@ pub enum JsRequest { ShowHud { display: String }, + HideWindow, UpdateLoadingBar { entrypoint_id: EntrypointId, show: bool diff --git a/rust/plugin_runtime/src/ui.rs b/rust/plugin_runtime/src/ui.rs index c9939f4..7cce870 100644 --- a/rust/plugin_runtime/src/ui.rs +++ b/rust/plugin_runtime/src/ui.rs @@ -212,6 +212,21 @@ pub async fn show_hud(state: Rc>, #[string] display: String) -> api.ui_show_hud(display).await } +#[op2(async)] +pub async fn hide_window(state: Rc>) -> anyhow::Result<()> { + let api = { + let state = state.borrow(); + + let api = state + .borrow::() + .clone(); + + api + }; + + api.ui_hide_window().await +} + #[op2(async)] pub async fn update_loading_bar(state: Rc>, #[string] entrypoint_id: String, show: bool) -> anyhow::Result<()> { let api = { diff --git a/rust/scenario_runner/src/frontend_mock.rs b/rust/scenario_runner/src/frontend_mock.rs index aa78a71..6a8279f 100644 --- a/rust/scenario_runner/src/frontend_mock.rs +++ b/rust/scenario_runner/src/frontend_mock.rs @@ -151,7 +151,7 @@ async fn request_loop(mut request_receiver: RequestReceiver { + UiRequestData::UpdateLoadingBar { .. } | UiRequestData::ShowHud { .. } | UiRequestData::ShowWindow | UiRequestData::HideWindow | UiRequestData::ClearInlineView { .. } | UiRequestData::SetTheme { .. } => { unreachable!() } UiRequestData::SetGlobalShortcut { .. } | UiRequestData::RequestSearchResultUpdate => { diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index a4b1cef..a846d71 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -500,6 +500,11 @@ async fn handle_message(message: JsRequest, api: &BackendForPluginRuntimeApiImpl Ok(JsResponse::Nothing) } + JsRequest::HideWindow => { + api.ui_hide_window().await?; + + Ok(JsResponse::Nothing) + } JsRequest::UpdateLoadingBar { entrypoint_id, show } => { api.ui_update_loading_bar(entrypoint_id, show).await?; @@ -987,6 +992,12 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { Ok(()) } + async fn ui_hide_window(&self) -> anyhow::Result<()> { + self.frontend_api.hide_window().await?; + + Ok(()) + } + async fn ui_get_action_id_for_shortcut( &self, entrypoint_id: EntrypointId, From 2ee1e645c9075985729e2b9631406b7f43ee9036 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:53:08 +0100 Subject: [PATCH 296/540] Add config option to disable close on unfocus functionality --- dev_data/config/config.toml | 4 +- rust/client/src/ui/mod.rs | 10 ++++ rust/common/src/model.rs | 1 + rust/server/src/lib.rs | 6 +-- rust/server/src/plugins/config_reader.rs | 60 +++++++++++++++++------- rust/server/src/plugins/mod.rs | 4 +- 6 files changed, 60 insertions(+), 25 deletions(-) diff --git a/dev_data/config/config.toml b/dev_data/config/config.toml index 2a4f5a0..440aa77 100644 --- a/dev_data/config/config.toml +++ b/dev_data/config/config.toml @@ -1,3 +1,3 @@ -[[plugins]] -id = "some-plugin-id" \ No newline at end of file +#[main_window] +#close_on_unfocus = false \ No newline at end of file diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 40c6df6..8ef63eb 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -71,6 +71,7 @@ pub struct AppModel { #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: tray_icon::TrayIcon, theme: GauntletComplexTheme, + close_on_unfocus: bool, // ephemeral state prompt: String, @@ -522,6 +523,7 @@ fn new( #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: sys_tray::create_tray(), theme, + close_on_unfocus: setup_data.close_on_unfocus, // ephemeral state prompt: "".to_string(), @@ -970,6 +972,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } AppMsg::IcedEvent(window_id, Event::Window(window::Event::Focused)) => { + if !state.close_on_unfocus { + return Task::none() + } + if window_id != state.main_window_id { return Task::none() } @@ -977,6 +983,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { state.on_focused() } AppMsg::IcedEvent(window_id, Event::Window(window::Event::Unfocused)) => { + if !state.close_on_unfocus { + return Task::none() + } + if window_id != state.main_window_id { return Task::none() } diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 73aa054..835a22b 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -202,6 +202,7 @@ pub struct UiTheme { pub struct UiSetupData { pub theme: UiTheme, pub global_shortcut: Option, + pub close_on_unfocus: bool, } #[derive(Debug)] diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index 9868073..49a6b48 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -151,7 +151,7 @@ async fn run_server(frontend_sender: RequestSender Self { Self { dirs, - repository + repository, + close_on_unfocus: AtomicBool::new(true), } } pub async fn reload_config(&self) -> anyhow::Result<()> { let config = self.read_config(); - for plugin in config.plugins { - let exists = self.repository.does_plugin_exist(&plugin.id).await?; - if !exists { - let pending = self.repository.is_plugin_pending(&plugin.id).await?; - if !pending { - let pending_plugin = DbWritePendingPlugin { - id: plugin.id - }; - self.repository.save_pending_plugin(pending_plugin).await? - } - } - } + // for plugin in config.plugins { + // let exists = self.repository.does_plugin_exist(&plugin.id).await?; + // if !exists { + // let pending = self.repository.is_plugin_pending(&plugin.id).await?; + // if !pending { + // let pending_plugin = DbWritePendingPlugin { + // id: plugin.id + // }; + // self.repository.save_pending_plugin(pending_plugin).await? + // } + // } + // } + + self.close_on_unfocus.store(config.main_window.close_on_unfocus, Ordering::SeqCst); Ok(()) } @@ -55,21 +61,39 @@ impl ConfigReader { } } } + + pub fn close_on_unfocus(&self) -> bool { + self.close_on_unfocus.load(Ordering::SeqCst) + } } #[derive(Debug, Deserialize, Default)] pub struct ApplicationConfig { - // #[serde(default)] // TODO + main_window: ApplicationConfigWindow + // #[serde(default)] // configuration_mode: ConfigurationModeConfig, - #[serde(default)] - plugins: Vec, + // #[serde(default)] + // plugins: Vec, } #[derive(Debug, Deserialize)] -struct PluginEntryConfig { - id: String, +pub struct ApplicationConfigWindow { + close_on_unfocus: bool } +impl Default for ApplicationConfigWindow { + fn default() -> Self { + Self { + close_on_unfocus: true, + } + } +} + +// #[derive(Debug, Deserialize)] +// struct PluginEntryConfig { +// id: String, +// } + // #[derive(Deserialize, Debug, Default)] // enum ConfigurationModeConfig { // #[serde(rename = "config")] diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 677e99c..0b3a7f4 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -90,10 +90,12 @@ impl ApplicationManager { pub async fn setup_data(&self) -> anyhow::Result { let theme = self.settings.effective_theme().await?; let global_shortcut = self.settings.effective_global_shortcut().await?; + let close_on_unfocus = self.config_reader.close_on_unfocus(); Ok(UiSetupData { theme, - global_shortcut + global_shortcut, + close_on_unfocus }) } From b6fe543df768ade16646f20fdbe36edb40ff92fc Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 13 Jan 2025 21:47:16 +0100 Subject: [PATCH 297/540] Fix action not being run if list or grid view has focused search bar --- Cargo.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c51ffb..90a3a8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5167,7 +5167,7 @@ dependencies = [ [[package]] name = "iced" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" dependencies = [ "iced_core", "iced_futures", @@ -5195,7 +5195,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" dependencies = [ "bitflags 2.6.0", "bytes", @@ -5222,7 +5222,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" dependencies = [ "futures", "iced_core", @@ -5236,7 +5236,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5288,7 +5288,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -5300,7 +5300,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" dependencies = [ "bytes", "iced_core", @@ -5321,7 +5321,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" dependencies = [ "bytemuck", "cosmic-text", @@ -5336,7 +5336,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5355,7 +5355,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" dependencies = [ "iced_renderer", "iced_runtime", @@ -5369,7 +5369,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#2a90afd516762fe133108f5d8d09393c0221f6db" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" dependencies = [ "iced_futures", "iced_graphics", From 4cf3d3210a842c164629a0d4872c6ccfe6b8099f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 13 Jan 2025 22:09:26 +0100 Subject: [PATCH 298/540] Add ability to open new instance of application there are already windows open and window tracking is enabled --- bundled_plugins/gauntlet/src/window/shared.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bundled_plugins/gauntlet/src/window/shared.tsx b/bundled_plugins/gauntlet/src/window/shared.tsx index 30a94f4..309aada 100644 --- a/bundled_plugins/gauntlet/src/window/shared.tsx +++ b/bundled_plugins/gauntlet/src/window/shared.tsx @@ -81,6 +81,12 @@ export function applicationActions( let [windowId, _] = appWindow!!; focusWindow(windowId) }, + }, + { + label: "Open new instance", + run: () => { + openApplication() + }, } ] } else if (appWindows.length > 1) { @@ -93,6 +99,12 @@ export function applicationActions( return focusWindow(windowId)}/> } + }, + { + label: "Open new instance", + run: () => { + openApplication() + }, } ] } else { From c492d267118dca72a0cfefd840f69fc29d7fe4ae Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:52:43 +0100 Subject: [PATCH 299/540] Add config setting to chose on which monitor window appears: always on the same or current active --- Cargo.lock | 26 +++---- rust/client/src/ui/mod.rs | 24 ++++++- rust/common/src/model.rs | 22 ++++++ rust/common/src/rpc/backend_api.rs | 35 ++++++++- rust/common/src/rpc/backend_server.rs | 43 ++++++++++- rust/common/src/rpc/frontend_api.rs | 20 +++++- rust/management_client/src/ui.rs | 12 +++- rust/management_client/src/views/general.rs | 72 ++++++++++++++++--- rust/server/src/plugins/config_reader.rs | 4 +- rust/server/src/plugins/data_db_repository.rs | 8 +++ rust/server/src/plugins/mod.rs | 14 +++- rust/server/src/plugins/settings.rs | 37 +++++++++- rust/server/src/rpc.rs | 10 ++- schema/backend.proto | 17 +++++ 14 files changed, 306 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90a3a8a..cb38c75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3090,7 +3090,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "dpi" version = "0.1.1" -source = "git+https://github.com/project-gauntlet/winit.git?rev=e710490e0da949f68b40a4e9de2678ac77543f3e#e710490e0da949f68b40a4e9de2678ac77543f3e" +source = "git+https://github.com/project-gauntlet/winit.git?rev=b0adb1dd33d82c5fd129c3f9ff5bf2f319955ad1#b0adb1dd33d82c5fd129c3f9ff5bf2f319955ad1" [[package]] name = "dprint-swc-ext" @@ -5167,7 +5167,7 @@ dependencies = [ [[package]] name = "iced" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" dependencies = [ "iced_core", "iced_futures", @@ -5195,7 +5195,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" dependencies = [ "bitflags 2.6.0", "bytes", @@ -5222,7 +5222,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" dependencies = [ "futures", "iced_core", @@ -5236,7 +5236,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5288,7 +5288,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -5300,7 +5300,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" dependencies = [ "bytes", "iced_core", @@ -5321,7 +5321,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" dependencies = [ "bytemuck", "cosmic-text", @@ -5336,7 +5336,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5355,7 +5355,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" dependencies = [ "iced_renderer", "iced_runtime", @@ -5369,7 +5369,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#0523292e22f50310c74fc955b90bcd8cb56cc3e1" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" dependencies = [ "iced_futures", "iced_graphics", @@ -12511,7 +12511,7 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" version = "0.30.99" -source = "git+https://github.com/project-gauntlet/winit.git?rev=e710490e0da949f68b40a4e9de2678ac77543f3e#e710490e0da949f68b40a4e9de2678ac77543f3e" +source = "git+https://github.com/project-gauntlet/winit.git?rev=b0adb1dd33d82c5fd129c3f9ff5bf2f319955ad1#b0adb1dd33d82c5fd129c3f9ff5bf2f319955ad1" dependencies = [ "ahash 0.8.11", "android-activity", @@ -12525,7 +12525,7 @@ dependencies = [ "core-foundation 0.9.4", "core-graphics 0.23.2", "cursor-icon", - "dpi 0.1.1 (git+https://github.com/project-gauntlet/winit.git?rev=e710490e0da949f68b40a4e9de2678ac77543f3e)", + "dpi 0.1.1 (git+https://github.com/project-gauntlet/winit.git?rev=b0adb1dd33d82c5fd129c3f9ff5bf2f319955ad1)", "js-sys", "libc", "memmap2 0.9.5", diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 8ef63eb..66cd03d 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -24,7 +24,7 @@ use serde::Deserialize; use tokio::sync::{Mutex as TokioMutex, RwLock as TokioRwLock}; use client_context::ClientContext; -use gauntlet_common::model::{BackendRequestData, BackendResponseData, EntrypointId, UiTheme, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointActionType, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiSetupData, UiWidgetId}; +use gauntlet_common::model::{BackendRequestData, BackendResponseData, EntrypointId, UiTheme, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointActionType, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiSetupData, UiWidgetId, WindowPositionMode}; use gauntlet_common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFrontendApiError}; use gauntlet_common::scenario_convert::{ui_render_location_from_scenario}; use gauntlet_common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; @@ -71,6 +71,7 @@ pub struct AppModel { #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: tray_icon::TrayIcon, theme: GauntletComplexTheme, + window_position_mode: WindowPositionMode, close_on_unfocus: bool, // ephemeral state @@ -223,6 +224,9 @@ pub enum AppMsg { SetTheme { theme: UiTheme }, + SetWindowPositionMode { + mode: WindowPositionMode + }, } #[cfg(target_os = "linux")] @@ -523,6 +527,7 @@ fn new( #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: sys_tray::create_tray(), theme, + window_position_mode: setup_data.window_position_mode, close_on_unfocus: setup_data.close_on_unfocus, // ephemeral state @@ -1376,6 +1381,11 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { GauntletComplexTheme::update_global(state.theme.clone()); + Task::none() + } + AppMsg::SetWindowPositionMode { mode } => { + state.window_position_mode = mode; + Task::none() } } @@ -2053,6 +2063,11 @@ impl AppModel { #[cfg(not(target_os = "linux"))] let open_task = Task::batch([ window::gain_focus(self.main_window_id), + #[cfg(target_os = "macos")] + match self.window_position_mode { + WindowPositionMode::Static => Task::none(), + WindowPositionMode::ActiveMonitor => window::move_to_active_monitor(self.main_window_id), + }, window::change_mode(self.main_window_id, Mode::Windowed) ]); @@ -2378,6 +2393,13 @@ async fn request_loop( theme, } } + UiRequestData::SetWindowPositionMode { mode } => { + responder.respond(UiResponseData::Nothing); + + AppMsg::SetWindowPositionMode { + mode, + } + } } }; diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 835a22b..a4ae32f 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -157,6 +157,24 @@ pub enum UiThemeMode { Dark } +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum WindowPositionMode { + Static, + ActiveMonitor +} + +impl Display for WindowPositionMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let label = match self { + WindowPositionMode::Static => "Static", + WindowPositionMode::ActiveMonitor => "Active Monitor", + }; + + write!(f, "{}", label) + } +} + + #[derive(Debug, Clone)] pub struct UiThemeColor { pub r: f32, @@ -203,6 +221,7 @@ pub struct UiSetupData { pub theme: UiTheme, pub global_shortcut: Option, pub close_on_unfocus: bool, + pub window_position_mode: WindowPositionMode, } #[derive(Debug)] @@ -259,6 +278,9 @@ pub enum UiRequestData { SetTheme { theme: UiTheme }, + SetWindowPositionMode { + mode: WindowPositionMode + }, } #[derive(Debug)] diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index 9274002..9e315a3 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -5,8 +5,8 @@ use tonic::transport::Channel; use gauntlet_utils::channel::{RequestError, RequestSender}; -use crate::model::{BackendRequestData, BackendResponseData, DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, SettingsTheme, UiPropertyValue, UiSetupData, UiWidgetId}; -use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadStatus, RpcDownloadStatusRequest, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetThemeRequest, RpcPingRequest, RpcPluginsRequest, RpcRemovePluginRequest, RpcSaveLocalPluginRequest, RpcSetEntrypointStateRequest, RpcSetGlobalShortcutRequest, RpcSetPluginStateRequest, RpcSetPreferenceValueRequest, RpcSetThemeRequest, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowWindowRequest}; +use crate::model::{BackendRequestData, BackendResponseData, DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, SettingsTheme, UiPropertyValue, UiSetupData, UiWidgetId, WindowPositionMode}; +use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadStatus, RpcDownloadStatusRequest, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetThemeRequest, RpcGetWindowPositionModeRequest, RpcPingRequest, RpcPluginsRequest, RpcRemovePluginRequest, RpcSaveLocalPluginRequest, RpcSetEntrypointStateRequest, RpcSetGlobalShortcutRequest, RpcSetPluginStateRequest, RpcSetPreferenceValueRequest, RpcSetThemeRequest, RpcSetWindowPositionModeRequest, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowWindowRequest}; use crate::rpc::grpc::rpc_backend_client::RpcBackendClient; use crate::rpc::grpc_convert::{plugin_preference_from_rpc, plugin_preference_user_data_from_rpc, plugin_preference_user_data_to_rpc}; @@ -453,6 +453,37 @@ impl BackendApi { Ok(theme) } + pub async fn set_window_position_mode(&mut self, mode: WindowPositionMode) -> Result<(), BackendApiError> { + let mode = match mode { + WindowPositionMode::Static => "Static", + WindowPositionMode::ActiveMonitor => "ActiveMonitor", + }; + + let request = RpcSetWindowPositionModeRequest { + mode: mode.to_string() + }; + + self.client.set_window_position_mode(Request::new(request)) + .await?; + + Ok(()) + } + + pub async fn get_window_position_mode(&mut self) -> Result { + let response = self.client.get_window_position_mode(Request::new(RpcGetWindowPositionModeRequest::default())) + .await?; + + let mode = response.into_inner().mode; + + let mode = match mode.as_str() { + "Static" => WindowPositionMode::Static, + "ActiveMonitor" => WindowPositionMode::ActiveMonitor, + _ => unreachable!() + }; + + Ok(mode) + } + pub async fn set_preference_value(&mut self, plugin_id: PluginId, entrypoint_id: Option, id: String, user_data: PluginPreferenceUserData) -> Result<(), BackendApiError> { let request = RpcSetPreferenceValueRequest { plugin_id: plugin_id.to_string(), diff --git a/rust/common/src/rpc/backend_server.rs b/rust/common/src/rpc/backend_server.rs index f2d45f4..edacb16 100644 --- a/rust/common/src/rpc/backend_server.rs +++ b/rust/common/src/rpc/backend_server.rs @@ -6,8 +6,8 @@ use tokio::net::TcpStream; use tonic::{Request, Response, Status}; use tonic::transport::Server; -use crate::model::{DownloadStatus, EntrypointId, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SettingsEntrypointType, SettingsPlugin, SettingsTheme}; -use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadPluginResponse, RpcDownloadStatus, RpcDownloadStatusRequest, RpcDownloadStatusResponse, RpcDownloadStatusValue, RpcEntrypoint, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetGlobalShortcutResponse, RpcGetThemeRequest, RpcGetThemeResponse, RpcPingRequest, RpcPingResponse, RpcPlugin, RpcPluginsRequest, RpcPluginsResponse, RpcRemovePluginRequest, RpcRemovePluginResponse, RpcSaveLocalPluginRequest, RpcSaveLocalPluginResponse, RpcSetEntrypointStateRequest, RpcSetEntrypointStateResponse, RpcSetGlobalShortcutRequest, RpcSetGlobalShortcutResponse, RpcSetPluginStateRequest, RpcSetPluginStateResponse, RpcSetPreferenceValueRequest, RpcSetPreferenceValueResponse, RpcSetThemeRequest, RpcSetThemeResponse, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowSettingsWindowResponse, RpcShowWindowRequest, RpcShowWindowResponse}; +use crate::model::{DownloadStatus, EntrypointId, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SettingsEntrypointType, SettingsPlugin, SettingsTheme, WindowPositionMode}; +use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadPluginResponse, RpcDownloadStatus, RpcDownloadStatusRequest, RpcDownloadStatusResponse, RpcDownloadStatusValue, RpcEntrypoint, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetGlobalShortcutResponse, RpcGetThemeRequest, RpcGetThemeResponse, RpcGetWindowPositionModeRequest, RpcGetWindowPositionModeResponse, RpcPingRequest, RpcPingResponse, RpcPlugin, RpcPluginsRequest, RpcPluginsResponse, RpcRemovePluginRequest, RpcRemovePluginResponse, RpcSaveLocalPluginRequest, RpcSaveLocalPluginResponse, RpcSetEntrypointStateRequest, RpcSetEntrypointStateResponse, RpcSetGlobalShortcutRequest, RpcSetGlobalShortcutResponse, RpcSetPluginStateRequest, RpcSetPluginStateResponse, RpcSetPreferenceValueRequest, RpcSetPreferenceValueResponse, RpcSetThemeRequest, RpcSetThemeResponse, RpcSetWindowPositionModeRequest, RpcSetWindowPositionModeResponse, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowSettingsWindowResponse, RpcShowWindowRequest, RpcShowWindowResponse}; use crate::rpc::grpc::rpc_backend_server::{RpcBackend, RpcBackendServer}; use crate::rpc::grpc_convert::{plugin_preference_to_rpc, plugin_preference_user_data_from_rpc, plugin_preference_user_data_to_rpc}; @@ -84,6 +84,15 @@ pub trait BackendServer { &self, ) -> anyhow::Result; + async fn set_window_position_mode( + &self, + mode: WindowPositionMode + ) -> anyhow::Result<()>; + + async fn get_window_position_mode( + &self, + ) -> anyhow::Result; + async fn set_preference_value( &self, plugin_id: PluginId, @@ -304,6 +313,36 @@ impl RpcBackend for RpcBackendServerImpl { theme: theme.to_string(), })) } + async fn set_window_position_mode(&self, request: Request) -> Result, Status> { + let mode = request.into_inner().mode; + + let mode = match mode.as_str() { + "Static" => WindowPositionMode::Static, + "ActiveMonitor" => WindowPositionMode::ActiveMonitor, + _ => unreachable!() + }; + + self.server.set_window_position_mode(mode) + .await + .map_err(|err| Status::internal(format!("{:#}", err)))?; + + Ok(Response::new(RpcSetWindowPositionModeResponse::default())) + } + + async fn get_window_position_mode(&self, _request: Request) -> Result, Status> { + let mode = self.server.get_window_position_mode() + .await + .map_err(|err| Status::internal(format!("{:#}", err)))?; + + let mode = match mode { + WindowPositionMode::Static => "Static", + WindowPositionMode::ActiveMonitor => "ActiveMonitor", + }; + + Ok(Response::new(RpcGetWindowPositionModeResponse { + mode: mode.to_string(), + })) + } async fn download_plugin(&self, request: Request) -> Result, Status> { let request = request.into_inner(); diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index f1baf14..fbd0c04 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -3,7 +3,7 @@ use anyhow::anyhow; use thiserror::Error; use gauntlet_utils::channel::{RequestError, RequestSender}; -use crate::model::{EntrypointId, UiTheme, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId}; +use crate::model::{EntrypointId, UiTheme, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId, WindowPositionMode}; #[derive(Error, Debug)] pub enum FrontendApiError { @@ -206,4 +206,22 @@ impl FrontendApi { UiResponseData::Err(err) => Err(err) } } + + pub async fn set_window_position_mode( + &self, + mode: WindowPositionMode + ) -> anyhow::Result<()> { + let request = UiRequestData::SetWindowPositionMode { + mode, + }; + + let data = self.frontend_sender.send_receive(request) + .await + .map_err(|err| anyhow!("error: {:?}", err))?; + + match data { + UiResponseData::Nothing => Ok(()), + UiResponseData::Err(err) => Err(err) + } + } } \ No newline at end of file diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index 42a073a..316300f 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -8,7 +8,7 @@ use iced_aw::Spinner; use iced_fonts::{Bootstrap, BOOTSTRAP_FONT, BOOTSTRAP_FONT_BYTES}; use itertools::Itertools; -use gauntlet_common::model::{DownloadStatus, PhysicalShortcut, PluginId, SettingsTheme}; +use gauntlet_common::model::{DownloadStatus, PhysicalShortcut, PluginId, SettingsTheme, WindowPositionMode}; use gauntlet_common::rpc::backend_api::{BackendApi, BackendApiError}; use gauntlet_common_ui::padding; use crate::theme::{Element, GauntletSettingsTheme}; @@ -113,6 +113,7 @@ fn new() -> (ManagementAppModel, Task) { Ok(init) => { ManagementAppMsg::General(ManagementAppGeneralMsgIn::InitSetting { theme: init.theme, + window_position_mode: init.window_position_mode, shortcut: init.global_shortcut, shortcut_error: init.global_shortcut_error }) @@ -130,7 +131,8 @@ fn new() -> (ManagementAppModel, Task) { struct InitSettingsData { global_shortcut: Option, global_shortcut_error: Option, - theme: SettingsTheme + theme: SettingsTheme, + window_position_mode: WindowPositionMode } async fn init_data(mut backend_api: BackendApi) -> Result { @@ -140,10 +142,14 @@ async fn init_data(mut backend_api: BackendApi) -> Result, theme: SettingsTheme, + window_position_mode: WindowPositionMode, current_shortcut: Option, current_shortcut_error: Option, currently_capturing: bool @@ -24,8 +25,10 @@ pub enum ManagementAppGeneralMsgIn { ShortcutCaptured(Option), CapturingChanged(bool), ThemeChanged(SettingsTheme), + WindowPositionModeChanged(WindowPositionMode), InitSetting { theme: SettingsTheme, + window_position_mode: WindowPositionMode, shortcut: Option, shortcut_error: Option }, @@ -43,6 +46,7 @@ impl ManagementAppGeneralState { Self { backend_api, theme: SettingsTheme::AutoDetect, + window_position_mode: WindowPositionMode::Static, current_shortcut: None, current_shortcut_error: None, currently_capturing: false, @@ -73,8 +77,9 @@ impl ManagementAppGeneralState { ManagementAppGeneralMsgIn::Noop => { Task::none() } - ManagementAppGeneralMsgIn::InitSetting { theme, shortcut, shortcut_error } => { + ManagementAppGeneralMsgIn::InitSetting { theme, window_position_mode, shortcut, shortcut_error } => { self.theme = theme; + self.window_position_mode = window_position_mode; self.current_shortcut = shortcut; self.current_shortcut_error = shortcut_error; @@ -98,6 +103,18 @@ impl ManagementAppGeneralState { }, |result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop)) } + ManagementAppGeneralMsgIn::WindowPositionModeChanged(mode) => { + self.window_position_mode = mode.clone(); + + let mut backend_api = backend_api.clone(); + + Task::perform(async move { + backend_api.set_window_position_mode(mode) + .await?; + + Ok(()) + }, |result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop)) + } } } @@ -120,7 +137,30 @@ impl ManagementAppGeneralState { Some(self.shortcut_capture_after()) ); + let theme_field = self.theme_field(); + let mut content = vec![global_shortcut_field, theme_field]; + + #[cfg(target_os = "macos")] + { + content.push(self.window_position_mode_field()) + } + + let content: Element<_> = column(content) + .into(); + + let content: Element<_> = container(content) + .width(Length::Fill) + .into(); + + let content: Element<_> = container(content) + .width(Length::Fill) + .into(); + + content + } + + fn theme_field(&self) -> Element { let theme_field = match &self.theme { SettingsTheme::ThemeFile => { let theme_field: Element<_> = text("Unable to change because theme config file is present ") @@ -168,18 +208,32 @@ impl ManagementAppGeneralState { None ); - let content: Element<_> = column(vec![global_shortcut_field, theme_field]) - .into(); + theme_field + } - let content: Element<_> = container(content) + fn window_position_mode_field(&self) -> Element { + let items = [ + WindowPositionMode::Static, + WindowPositionMode::ActiveMonitor, + ]; + + let field: Element<_> = pick_list( + items, + Some(self.window_position_mode.clone()), + move |item| ManagementAppGeneralMsgIn::WindowPositionModeChanged(item), + ).into(); + + let field: Element<_> = container(field) .width(Length::Fill) .into(); - let content: Element<_> = container(content) - .width(Length::Fill) - .into(); + let field = self.view_field( + "Window Position Mode", + field, + None + ); - content + field } fn view_field<'a>(&'a self, label: &'a str, input: Element<'a, ManagementAppGeneralMsgIn>, after: Option>) -> Element<'a, ManagementAppGeneralMsgIn> { diff --git a/rust/server/src/plugins/config_reader.rs b/rust/server/src/plugins/config_reader.rs index 1158311..3b33c52 100644 --- a/rust/server/src/plugins/config_reader.rs +++ b/rust/server/src/plugins/config_reader.rs @@ -36,7 +36,7 @@ impl ConfigReader { // } // } - self.close_on_unfocus.store(config.main_window.close_on_unfocus, Ordering::SeqCst); + self.close_on_unfocus.store(config.main_window.unwrap_or_default().close_on_unfocus, Ordering::SeqCst); Ok(()) } @@ -69,7 +69,7 @@ impl ConfigReader { #[derive(Debug, Deserialize, Default)] pub struct ApplicationConfig { - main_window: ApplicationConfigWindow + main_window: Option // #[serde(default)] // configuration_mode: ConfigurationModeConfig, // #[serde(default)] diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 380102a..14a8356 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -238,6 +238,8 @@ pub struct DbSettingsGlobalShortcutData { pub struct DbSettings { // none means auto-detect pub theme: Option, + // none is static + pub window_position_mode: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -250,6 +252,12 @@ pub enum DbTheme { Legacy } +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum DbWindowPositionMode { + #[serde(rename = "active_monitor")] + ActiveMonitor, +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub enum DbPluginActionShortcutKind { #[serde(rename = "main")] diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 0b3a7f4..b0dd6a3 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -7,7 +7,7 @@ use anyhow::anyhow; use include_dir::{include_dir, Dir}; use tokio::runtime::Handle; -use gauntlet_common::model::{DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, SettingsTheme, UiPropertyValue, UiRequestData, UiResponseData, UiSetupData, UiWidgetId}; +use gauntlet_common::model::{DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, SettingsTheme, UiPropertyValue, UiRequestData, UiResponseData, UiSetupData, UiWidgetId, WindowPositionMode}; use gauntlet_common::rpc::frontend_api::FrontendApi; use gauntlet_common::{settings_env_data_to_string, SettingsEnvData}; use gauntlet_utils::channel::RequestSender; @@ -90,12 +90,14 @@ impl ApplicationManager { pub async fn setup_data(&self) -> anyhow::Result { let theme = self.settings.effective_theme().await?; let global_shortcut = self.settings.effective_global_shortcut().await?; + let window_position_mode = self.settings.window_position_mode_setting().await?; let close_on_unfocus = self.config_reader.close_on_unfocus(); Ok(UiSetupData { theme, global_shortcut, - close_on_unfocus + close_on_unfocus, + window_position_mode }) } @@ -287,6 +289,14 @@ impl ApplicationManager { self.settings.theme_setting().await } + pub async fn set_window_position_mode(&self, mode: WindowPositionMode) -> anyhow::Result<()> { + self.settings.set_window_position_mode_setting(mode).await + } + + pub async fn get_window_position_mode(&self) -> anyhow::Result { + self.settings.window_position_mode_setting().await + } + pub async fn set_preference_value(&self, plugin_id: PluginId, entrypoint_id: Option, preference_id: String, preference_value: PluginPreferenceUserData) -> anyhow::Result<()> { tracing::debug!(target = "plugin", "Setting preference value for plugin id: {:?}, entrypoint_id: {:?}, preference_id: {}", plugin_id, entrypoint_id, preference_id); diff --git a/rust/server/src/plugins/settings.rs b/rust/server/src/plugins/settings.rs index 24d2ee4..51020fc 100644 --- a/rust/server/src/plugins/settings.rs +++ b/rust/server/src/plugins/settings.rs @@ -1,9 +1,9 @@ -use crate::plugins::data_db_repository::{DataDbRepository, DbTheme}; +use crate::plugins::data_db_repository::{DataDbRepository, DbTheme, DbWindowPositionMode}; use crate::plugins::theme::{read_theme_file, BundledThemes}; use anyhow::anyhow; use dark_light::Mode; use gauntlet_common::dirs::Dirs; -use gauntlet_common::model::{PhysicalKey, PhysicalShortcut, SettingsTheme, UiTheme}; +use gauntlet_common::model::{PhysicalKey, PhysicalShortcut, SettingsTheme, UiTheme, WindowPositionMode}; use gauntlet_common::rpc::frontend_api::FrontendApi; use std::env::consts::OS; @@ -151,6 +151,39 @@ impl Settings { Ok(()) } + pub async fn window_position_mode_setting(&self) -> anyhow::Result { + let mut settings = self.repository + .get_settings() + .await?; + + let window_position_mode = match &settings.window_position_mode { + None => WindowPositionMode::Static, + Some(DbWindowPositionMode::ActiveMonitor) => WindowPositionMode::ActiveMonitor + }; + + Ok(window_position_mode) + } + + pub async fn set_window_position_mode_setting(&self, mode: WindowPositionMode) -> anyhow::Result<()> { + + let mut settings = self.repository + .get_settings() + .await?; + + let window_position_mode = match mode { + WindowPositionMode::Static => None, + WindowPositionMode::ActiveMonitor => Some(DbWindowPositionMode::ActiveMonitor), + }; + + settings.window_position_mode = window_position_mode; + + self.repository.set_settings(settings).await?; + + self.frontend_api.set_window_position_mode(mode).await?; + + Ok(()) + } + fn autodetect_theme(&self) -> UiTheme { match OS { "macos" => { diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index 12876a7..599b020 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; use gauntlet_common::{settings_env_data_to_string, SettingsEnvData}; -use gauntlet_common::model::{DownloadStatus, EntrypointId, PluginId, PluginPreferenceUserData, SettingsPlugin, UiPropertyValue, SearchResult, UiWidgetId, PhysicalKey, PhysicalShortcut, LocalSaveData, SettingsTheme}; +use gauntlet_common::model::{DownloadStatus, EntrypointId, PluginId, PluginPreferenceUserData, SettingsPlugin, UiPropertyValue, SearchResult, UiWidgetId, PhysicalKey, PhysicalShortcut, LocalSaveData, SettingsTheme, WindowPositionMode}; use gauntlet_common::rpc::backend_server::BackendServer; use crate::plugins::ApplicationManager; @@ -94,6 +94,14 @@ impl BackendServer for BackendServerImpl { self.application_manager.get_theme().await } + async fn set_window_position_mode(&self, mode: WindowPositionMode) -> anyhow::Result<()> { + self.application_manager.set_window_position_mode(mode).await + } + + async fn get_window_position_mode(&self, ) -> anyhow::Result { + self.application_manager.get_window_position_mode().await + } + async fn set_preference_value(&self, plugin_id: PluginId, entrypoint_id: Option, preference_id: String, preference_value: PluginPreferenceUserData) -> anyhow::Result<()> { let result = self.application_manager.set_preference_value(plugin_id, entrypoint_id, preference_id, preference_value) .await; diff --git a/schema/backend.proto b/schema/backend.proto index c7b1b37..c0cbfd8 100644 --- a/schema/backend.proto +++ b/schema/backend.proto @@ -25,6 +25,9 @@ service RpcBackend { rpc SetTheme (RpcSetThemeRequest) returns (RpcSetThemeResponse); rpc GetTheme (RpcGetThemeRequest) returns (RpcGetThemeResponse); + rpc SetWindowPositionMode (RpcSetWindowPositionModeRequest) returns (RpcSetWindowPositionModeResponse); + rpc GetWindowPositionMode (RpcGetWindowPositionModeRequest) returns (RpcGetWindowPositionModeResponse); + rpc DownloadPlugin (RpcDownloadPluginRequest) returns (RpcDownloadPluginResponse); rpc DownloadStatus (RpcDownloadStatusRequest) returns (RpcDownloadStatusResponse); @@ -108,6 +111,20 @@ message RpcGetThemeResponse { string theme = 1; } +message RpcSetWindowPositionModeRequest { + string mode = 1; +} + +message RpcSetWindowPositionModeResponse { +} + +message RpcGetWindowPositionModeRequest { +} + +message RpcGetWindowPositionModeResponse { + string mode = 1; +} + message RpcSetPreferenceValueRequest { string plugin_id = 1; string entrypoint_id = 2; From 47bf9f28e0eeba6f6087e38528cad73beeeeb938 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 16 Jan 2025 21:00:24 +0100 Subject: [PATCH 300/540] Rename windowTracking preference id into experimentalWindowTracking --- bundled_plugins/gauntlet/gauntlet.toml | 2 +- bundled_plugins/gauntlet/src/applications.tsx | 14 +++++++------- bundled_plugins/gauntlet/src/window/shared.tsx | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bundled_plugins/gauntlet/gauntlet.toml b/bundled_plugins/gauntlet/gauntlet.toml index 03ca1a9..198730b 100644 --- a/bundled_plugins/gauntlet/gauntlet.toml +++ b/bundled_plugins/gauntlet/gauntlet.toml @@ -10,7 +10,7 @@ type = 'entrypoint-generator' description = 'Run installed applications from your system' [[entrypoint.preferences]] -id = 'windowTracking' +id = 'experimentalWindowTracking' name = 'Experimental Window Tracking' type = 'bool' default = false diff --git a/bundled_plugins/gauntlet/src/applications.tsx b/bundled_plugins/gauntlet/src/applications.tsx index 027f4f6..199fa5e 100644 --- a/bundled_plugins/gauntlet/src/applications.tsx +++ b/bundled_plugins/gauntlet/src/applications.tsx @@ -20,10 +20,10 @@ import { applicationEventLoopX11, focusX11Window } from "./window/x11"; import { applicationEventLoopWayland, focusWaylandWindow } from "./window/wayland"; import { windows_app_from_path, windows_application_dirs, windows_open_application } from "gauntlet:bridge/internal-windows"; -type EntrypointPreferences = { windowTracking: boolean }; +type EntrypointPreferences = { experimentalWindowTracking: boolean }; export default async function Applications(context: GeneratorContext): Promise void)> { - const { add, remove, get, getAll, entrypointPreferences: { windowTracking } } = context; + const { add, remove, get, getAll, entrypointPreferences: { experimentalWindowTracking } } = context; switch (current_os()) { case "linux": { @@ -36,13 +36,13 @@ export default async function Applications(context: GeneratorContext { linux_open_application(id) }, focusWaylandWindow, ), - accessories: applicationAccessories(id, windowTracking), + accessories: applicationAccessories(id, experimentalWindowTracking), icon: data.icon, // TODO lazy icons "__linux__": { startupWmClass: data.startup_wm_class, @@ -54,13 +54,13 @@ export default async function Applications(context: GeneratorContext { linux_open_application(id) }, focusX11Window, ), - accessories: applicationAccessories(id, windowTracking), + accessories: applicationAccessories(id, experimentalWindowTracking), icon: data.icon, // TODO lazy icons "__linux__": { startupWmClass: data.startup_wm_class, @@ -73,7 +73,7 @@ export default async function Applications(context: GeneratorContext = {}; export function applicationActions( id: string, - windowTracking: boolean, + experimentalWindowTracking: boolean, openApplication: () => void, focusWindow: (windowId: string) => void, ): GeneratedEntrypointAction[] { - if (!windowTracking) { + if (!experimentalWindowTracking) { return [ { label: "Open application", @@ -112,8 +112,8 @@ export function applicationActions( } } -export function applicationAccessories(id: string, windowTracking: boolean): GeneratedEntrypointAccessory[] { - if (!windowTracking) { +export function applicationAccessories(id: string, experimentalWindowTracking: boolean): GeneratedEntrypointAccessory[] { + if (!experimentalWindowTracking) { return [] } From b4e255a92e415d34fdeb306292faae78b3e82102 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 16 Jan 2025 21:21:34 +0100 Subject: [PATCH 301/540] Fix window jumping to another screen if pressing global shortcut with window open when using active monitor position mode --- rust/client/src/ui/mod.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 66cd03d..0dec384 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -67,6 +67,7 @@ pub struct AppModel { frontend_receiver: Arc>>, main_window_id: window::Id, focused: bool, + opened: bool, wayland: bool, #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: tray_icon::TrayIcon, @@ -523,6 +524,7 @@ fn new( frontend_receiver: Arc::new(TokioRwLock::new(frontend_receiver)), main_window_id, focused: false, + opened: !minimized, wayland, #[cfg(any(target_os = "macos", target_os = "windows"))] tray_icon: sys_tray::create_tray(), @@ -2006,6 +2008,7 @@ impl AppModel { fn hide_window(&mut self) -> Task { self.focused = false; + self.opened = false; let mut commands = vec![]; @@ -2049,6 +2052,12 @@ impl AppModel { } fn show_window(&mut self) -> Task { + if self.opened { + return Task::none() + } + + self.opened = true; + #[cfg(target_os = "linux")] let open_task = if self.wayland { let (_, open_task) = open_main_window_wayland(self.main_window_id); From e4df96c890bd627e9bd5d8a54977befac4d67849 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 16 Jan 2025 22:02:43 +0100 Subject: [PATCH 302/540] Save position of the main window to be used after restart --- dev_data/state/.gitignore | 3 ++- rust/client/src/ui/mod.rs | 46 ++++++++++++++++++++++++++-------- rust/common/src/dirs.rs | 3 +++ rust/common/src/model.rs | 3 ++- rust/server/src/plugins/mod.rs | 2 ++ 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/dev_data/state/.gitignore b/dev_data/state/.gitignore index f1a26a4..555232b 100644 --- a/dev_data/state/.gitignore +++ b/dev_data/state/.gitignore @@ -1,2 +1,3 @@ logs -local_storage \ No newline at end of file +local_storage +window_position \ No newline at end of file diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 0dec384..faab7e7 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -12,10 +12,10 @@ use iced::widget::text::Shaping; use iced::widget::text_input::focus; use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, Space}; use iced::window::{Level, Mode, Position, Screenshot}; -use iced::{event, executor, font, futures, keyboard, stream, window, Alignment, Event, Font, Length, Padding, Pixels, Renderer, Settings, Size, Subscription, Task}; +use iced::{event, executor, font, futures, keyboard, stream, window, Alignment, Event, Font, Length, Padding, Pixels, Point, Renderer, Settings, Size, Subscription, Task}; use std::collections::HashMap; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::{Arc, Mutex as StdMutex, Mutex, RwLock as StdRwLock}; use iced::alignment::{Horizontal, Vertical}; @@ -74,6 +74,7 @@ pub struct AppModel { theme: GauntletComplexTheme, window_position_mode: WindowPositionMode, close_on_unfocus: bool, + window_position_file: PathBuf, // ephemeral state prompt: String, @@ -245,10 +246,10 @@ const WINDOW_WIDTH: f32 = 750.0; const WINDOW_HEIGHT: f32 = 450.0; #[cfg(not(target_os = "macos"))] -fn window_settings(visible: bool) -> window::Settings { +fn window_settings(visible: bool, position: Position) -> window::Settings { window::Settings { size: Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), - position: Position::Centered, + position, resizable: false, decorations: false, visible, @@ -260,10 +261,10 @@ fn window_settings(visible: bool) -> window::Settings { } #[cfg(target_os = "macos")] -fn window_settings(visible: bool) -> window::Settings { +fn window_settings(visible: bool, position: Position) -> window::Settings { window::Settings { size: Size::new(WINDOW_WIDTH, WINDOW_HEIGHT), - position: Position::Centered, + position, resizable: false, decorations: true, visible, @@ -296,8 +297,22 @@ fn layer_shell_settings() -> iced_layershell::reexport::NewLayerShellSettings { } } -fn open_main_window_non_wayland(minimized: bool) -> (window::Id, Task) { - let (main_window_id, open_task) = window::open(window_settings(!minimized)); +fn open_main_window_non_wayland(minimized: bool, window_position_file: &PathBuf) -> (window::Id, Task) { + let position = fs::read_to_string(window_position_file) + .map(|data| { + if let Some((x, y)) = data.split_once(":") { + match (x.parse(), y.parse()) { + (Ok(x), Ok(y)) => Some(Position::Specific(Point::new(x, y))), + _ => None + } + } else { + None + } + }) + .unwrap_or(None) + .unwrap_or(Position::Centered); + + let (main_window_id, open_task) = window::open(window_settings(!minimized, position)); (main_window_id, Task::batch([ open_task.map(|_| AppMsg::Noop), @@ -423,11 +438,11 @@ fn new( open_main_window_wayland(id) } } else { - open_main_window_non_wayland(minimized) + open_main_window_non_wayland(minimized, &setup_data.window_position_file) }; #[cfg(not(target_os = "linux"))] - let (main_window_id, open_task) = open_main_window_non_wayland(minimized); + let (main_window_id, open_task) = open_main_window_non_wayland(minimized, &setup_data.window_position_file); tasks.push(open_task); @@ -531,6 +546,7 @@ fn new( theme, window_position_mode: setup_data.window_position_mode, close_on_unfocus: setup_data.close_on_unfocus, + window_position_file: setup_data.window_position_file, // ephemeral state prompt: "".to_string(), @@ -1004,6 +1020,16 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { state.on_unfocused() } } + AppMsg::IcedEvent(window_id, Event::Window(window::Event::Moved(point))) => { + if window_id != state.main_window_id { + return Task::none() + } + + let _ = fs::create_dir_all(state.window_position_file.parent().unwrap()); + let _ = fs::write(&state.window_position_file, format!("{}:{}", point.x, point.y)); + + Task::none() + } AppMsg::IcedEvent(_, _) => Task::none(), AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::Noop, .. } => Task::none(), AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => state.global_state.back(&state.client_context), diff --git a/rust/common/src/dirs.rs b/rust/common/src/dirs.rs index f0562ef..f11cee3 100644 --- a/rust/common/src/dirs.rs +++ b/rust/common/src/dirs.rs @@ -124,4 +124,7 @@ impl Dirs { state_dir.join(format!("project-gauntlet-{}.sock", plugin_uuid)) } + pub fn window_position(&self) -> PathBuf { + self.state_dir().join("window_position") + } } \ No newline at end of file diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index a4ae32f..8235abc 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::fmt::{Display, Formatter}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::anyhow; @@ -218,6 +218,7 @@ pub struct UiTheme { #[derive(Debug)] pub struct UiSetupData { + pub window_position_file: PathBuf, pub theme: UiTheme, pub global_shortcut: Option, pub close_on_unfocus: bool, diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index b0dd6a3..3912751 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -88,12 +88,14 @@ impl ApplicationManager { } pub async fn setup_data(&self) -> anyhow::Result { + let window_position_file = self.dirs.window_position(); let theme = self.settings.effective_theme().await?; let global_shortcut = self.settings.effective_global_shortcut().await?; let window_position_mode = self.settings.window_position_mode_setting().await?; let close_on_unfocus = self.config_reader.close_on_unfocus(); Ok(UiSetupData { + window_position_file, theme, global_shortcut, close_on_unfocus, From 3201c721a5945e413bfb4a805a3714f1e10e6bc4 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 16 Jan 2025 22:41:20 +0100 Subject: [PATCH 303/540] Cleanup not needed usages of Arc> --- rust/client/src/ui/client_context.rs | 20 +-- rust/client/src/ui/mod.rs | 10 +- rust/client/src/ui/state/mod.rs | 16 +-- rust/client/src/ui/widget.rs | 168 +++++++++++++++---------- rust/client/src/ui/widget_container.rs | 127 ++++++++----------- 5 files changed, 173 insertions(+), 168 deletions(-) diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index b5f05fa..3609536 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -106,18 +106,18 @@ impl ClientContext { } } - pub fn handle_event(&self, render_location: UiRenderLocation, plugin_id: &PluginId, event: ComponentWidgetEvent) -> Option { + pub fn handle_event(&mut self, render_location: UiRenderLocation, plugin_id: &PluginId, event: ComponentWidgetEvent) -> Option { match render_location { - UiRenderLocation::InlineView => self.get_inline_view_container(&plugin_id).handle_event(plugin_id.clone(), event), - UiRenderLocation::View => self.get_view_container().handle_event(plugin_id.clone(), event) + UiRenderLocation::InlineView => self.get_mut_inline_view_container(&plugin_id).handle_event(plugin_id.clone(), event), + UiRenderLocation::View => self.get_mut_view_container().handle_event(plugin_id.clone(), event) } } - pub fn append_text(&self, text: &str) -> Task { + pub fn append_text(&mut self, text: &str) -> Task { self.view.append_text(text) } - pub fn backspace_text(&self) -> Task { + pub fn backspace_text(&mut self) -> Task { self.view.backspace_text() } @@ -125,7 +125,7 @@ impl ClientContext { self.view.focus_search_bar(widget_id) } - pub fn toggle_action_panel(&self) { + pub fn toggle_action_panel(&mut self) { self.view.toggle_action_panel() } @@ -137,19 +137,19 @@ impl ClientContext { self.view.get_focused_item_id() } - pub fn focus_up(&self) -> Task { + pub fn focus_up(&mut self) -> Task { self.view.focus_up() } - pub fn focus_down(&self) -> Task { + pub fn focus_down(&mut self) -> Task { self.view.focus_down() } - pub fn focus_left(&self) -> Task { + pub fn focus_left(&mut self) -> Task { self.view.focus_left() } - pub fn focus_right(&self) -> Task { + pub fn focus_right(&mut self) -> Task { self.view.focus_right() } } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index faab7e7..d255944 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -823,10 +823,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { keyboard::Event::KeyPressed { key, modifiers, physical_key, text, .. } => { tracing::debug!("Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", key, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); match key { - Key::Named(Named::ArrowUp) => state.global_state.up(&state.client_context, &state.search_results), - Key::Named(Named::ArrowDown) => state.global_state.down(&state.client_context, &state.search_results), - Key::Named(Named::ArrowLeft) => state.global_state.left(&state.client_context, &state.search_results), - Key::Named(Named::ArrowRight) => state.global_state.right(&state.client_context, &state.search_results), + Key::Named(Named::ArrowUp) => state.global_state.up(&mut state.client_context, &state.search_results), + Key::Named(Named::ArrowDown) => state.global_state.down(&mut state.client_context, &state.search_results), + Key::Named(Named::ArrowLeft) => state.global_state.left(&mut state.client_context, &state.search_results), + Key::Named(Named::ArrowRight) => state.global_state.right(&mut state.client_context, &state.search_results), Key::Named(Named::Escape) => state.global_state.back(&state.client_context), Key::Named(Named::Tab) if !modifiers.shift() => state.global_state.next(&state.client_context), Key::Named(Named::Tab) if modifiers.shift() => state.global_state.previous(&state.client_context), @@ -2164,7 +2164,7 @@ impl AppModel { }, |result| handle_backend_error(result, |()| AppMsg::Noop)) } - fn handle_plugin_event(&self, widget_event: ComponentWidgetEvent, plugin_id: PluginId, render_location: UiRenderLocation) -> Task { + fn handle_plugin_event(&mut self, widget_event: ComponentWidgetEvent, plugin_id: PluginId, render_location: UiRenderLocation) -> Task { let mut backend_client = self.backend_api.clone(); let event = self.client_context.handle_event(render_location, &plugin_id, widget_event.clone()); diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index 79b1728..e4b3190 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -124,10 +124,10 @@ pub trait Focus { fn back(&mut self, client_context: &ClientContext) -> Task; fn next(&mut self, client_context: &ClientContext) -> Task; fn previous(&mut self, client_context: &ClientContext) -> Task; - fn up(&mut self, client_context: &ClientContext, focus_list: &[T]) -> Task; - fn down(&mut self, client_context: &ClientContext, focus_list: &[T]) -> Task; - fn left(&mut self, client_context: &ClientContext, focus_list: &[T]) -> Task; - fn right(&mut self, client_context: &ClientContext, focus_list: &[T]) -> Task; + fn up(&mut self, client_context: &mut ClientContext, focus_list: &[T]) -> Task; + fn down(&mut self, client_context: &mut ClientContext, focus_list: &[T]) -> Task; + fn left(&mut self, client_context: &mut ClientContext, focus_list: &[T]) -> Task; + fn right(&mut self, client_context: &mut ClientContext, focus_list: &[T]) -> Task; } impl Focus for GlobalState { @@ -300,7 +300,7 @@ impl Focus for GlobalState { GlobalState::ErrorView { .. } => Task::none(), } } - fn up(&mut self, client_context: &ClientContext, _focus_list: &[SearchResult]) -> Task { + fn up(&mut self, client_context: &mut ClientContext, _focus_list: &[SearchResult]) -> Task { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { @@ -332,7 +332,7 @@ impl Focus for GlobalState { }, } } - fn down(&mut self, client_context: &ClientContext, focus_list: &[SearchResult]) -> Task { + fn down(&mut self, client_context: &mut ClientContext, focus_list: &[SearchResult]) -> Task { match self { GlobalState::MainView { focused_search_result, sub_state, .. } => { match sub_state { @@ -391,7 +391,7 @@ impl Focus for GlobalState { } } } - fn left(&mut self, client_context: &ClientContext, _focus_list: &[SearchResult]) -> Task { + fn left(&mut self, client_context: &mut ClientContext, _focus_list: &[SearchResult]) -> Task { match self { GlobalState::PluginView { sub_state, .. } => { match sub_state { @@ -405,7 +405,7 @@ impl Focus for GlobalState { GlobalState::ErrorView { .. } => Task::none(), } } - fn right(&mut self, client_context: &ClientContext, _focus_list: &[SearchResult]) -> Task { + fn right(&mut self, client_context: &mut ClientContext, _focus_list: &[SearchResult]) -> Task { match self { GlobalState::PluginView { sub_state, .. } => { match sub_state { diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 0f47868..0830856 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -36,16 +36,16 @@ use std::sync::Arc; #[derive(Debug)] pub struct ComponentWidgets<'b> { - root_widget: &'b mut Option>, - state: &'b mut HashMap, + root_widget: &'b Option>, + state: &'b HashMap, plugin_id: PluginId, images: &'b HashMap>, } impl<'b> ComponentWidgets<'b> { pub fn new( - root_widget: &'b mut Option>, - state: &'b mut HashMap, + root_widget: &'b Option>, + state: &'b HashMap, plugin_id: PluginId, images: &'b HashMap> ) -> ComponentWidgets<'b> { @@ -66,19 +66,6 @@ impl<'b> ComponentWidgets<'b> { } } - fn text_field_state_mut(&mut self, widget_id: UiWidgetId) -> &mut TextFieldState { - Self::text_field_state_mut_on_state(&mut self.state, widget_id) - } - - fn text_field_state_mut_on_state(state: &mut HashMap, widget_id: UiWidgetId) -> &mut TextFieldState { - let state = state.get_mut(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); - - match state { - ComponentWidgetState::TextField(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state) - } - } - fn checkbox_state(&self, widget_id: UiWidgetId) -> &CheckboxState { let state = self.state.get(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); @@ -114,6 +101,43 @@ impl<'b> ComponentWidgets<'b> { _ => panic!("TextFieldState expected, {:?} found", state) } } +} + +#[derive(Debug)] +pub struct ComponentWidgetsMut<'b> { + root_widget: &'b mut Option>, + state: &'b mut HashMap, + plugin_id: PluginId, + images: &'b HashMap>, +} + +impl<'b> ComponentWidgetsMut<'b> { + pub fn new( + root_widget: &'b mut Option>, + state: &'b mut HashMap, + plugin_id: PluginId, + images: &'b HashMap> + ) -> ComponentWidgetsMut<'b> { + Self { + root_widget, + state, + plugin_id, + images + } + } + + fn text_field_state_mut(&mut self, widget_id: UiWidgetId) -> &mut TextFieldState { + Self::text_field_state_mut_on_state(&mut self.state, widget_id) + } + + fn text_field_state_mut_on_state(state: &mut HashMap, widget_id: UiWidgetId) -> &mut TextFieldState { + let state = state.get_mut(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + + match state { + ComponentWidgetState::TextField(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state) + } + } fn root_state_mut(&mut self, widget_id: UiWidgetId) -> &mut RootState { Self::root_state_mut_on_field(&mut self.state, widget_id) @@ -129,7 +153,6 @@ impl<'b> ComponentWidgets<'b> { } } - pub fn create_state(root_widget: &RootWidget) -> HashMap { let mut result = HashMap::new(); @@ -305,7 +328,7 @@ pub enum TextRenderType { H6, } -impl<'b> ComponentWidgets<'b> { +impl<'b> ComponentWidgetsMut<'b> { pub fn toggle_action_panel(&mut self) { let Some(root_widget) = &self.root_widget else { return; @@ -327,7 +350,9 @@ impl<'b> ComponentWidgets<'b> { state.show_action_panel = !state.show_action_panel; } +} +impl<'b> ComponentWidgets<'b> { pub fn get_action_ids(&self) -> Vec { let Some(root_widget) = &self.root_widget else { return vec![]; @@ -397,6 +422,12 @@ impl<'b> ComponentWidgets<'b> { } } + pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Task { + let TextFieldState { text_input_id, .. } = self.text_field_state(widget_id); + + text_input::focus(text_input_id.clone()) + } + fn grid_section_sizes(grid_widget: &GridWidget) -> Vec { let mut amount_per_section: Vec = vec![]; let mut pending_section_size = 0; @@ -466,7 +497,9 @@ impl<'b> ComponentWidgets<'b> { amount_per_section } +} +impl<'b> ComponentWidgetsMut<'b> { pub fn append_text(&mut self, text: &str) -> Task { let Some(root_widget) = &self.root_widget else { return Task::none(); @@ -496,7 +529,7 @@ impl<'b> ComponentWidgets<'b> { _ => return Task::none() }; - let TextFieldState { text_input_id, state_value } = ComponentWidgets::text_field_state_mut_on_state(&mut self.state, widget_id); + let TextFieldState { text_input_id, state_value } = ComponentWidgetsMut::text_field_state_mut_on_state(&mut self.state, widget_id); if let Some(value) = text.chars().next().filter(|c| !c.is_control()) { *state_value = format!("{}{}", state_value, value); @@ -536,7 +569,7 @@ impl<'b> ComponentWidgets<'b> { _ => return Task::none() }; - let TextFieldState { text_input_id, state_value } = ComponentWidgets::text_field_state_mut_on_state(&mut self.state, widget_id); + let TextFieldState { text_input_id, state_value } = ComponentWidgetsMut::text_field_state_mut_on_state(&mut self.state, widget_id); let mut chars = state_value.chars(); chars.next_back(); @@ -545,46 +578,6 @@ impl<'b> ComponentWidgets<'b> { text_input::focus(text_input_id.clone()) } - pub fn first_open(&self) -> AppMsg { - let Some(root_widget) = &self.root_widget else { - return AppMsg::Noop; - }; - - let Some(content) = &root_widget.content else { - return AppMsg::Noop; - }; - - let widget_id = match content { - RootWidgetMembers::List(widget) => { - match &widget.content.search_bar { - None => { - return AppMsg::Noop - } - Some(widget) => widget.__id__ - } - } - RootWidgetMembers::Grid(widget) => { - match &widget.content.search_bar { - None => { - return AppMsg::Noop - } - Some(widget) => widget.__id__ - } - } - _ => return AppMsg::Noop - }; - - AppMsg::FocusPluginViewSearchBar { - widget_id - } - } - - pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Task { - let TextFieldState { text_input_id, .. } = self.text_field_state(widget_id); - - text_input::focus(text_input_id.clone()) - } - pub fn focus_up(&mut self) -> Task { let Some(root_widget) = &self.root_widget else { return Task::none(); @@ -599,7 +592,7 @@ impl<'b> ComponentWidgets<'b> { RootWidgetMembers::Form(_) => Task::none(), RootWidgetMembers::Inline(_) => Task::none(), RootWidgetMembers::List(list_widget) => { - let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, list_widget.__id__); + let RootState { focused_item, .. } = ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, list_widget.__id__); let focus_task = focused_item.focus_previous() .unwrap_or_else(|| Task::none()); @@ -612,13 +605,13 @@ impl<'b> ComponentWidgets<'b> { ]) } RootWidgetMembers::Grid(grid_widget) => { - let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); + let RootState { focused_item, .. } = ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); let Some(current_index) = &focused_item.index else { return Task::none(); }; - let amount_per_section_total = Self::grid_section_sizes(grid_widget); + let amount_per_section_total = ComponentWidgets::grid_section_sizes(grid_widget); let focus_task = match grid_up_offset(*current_index, amount_per_section_total) { None => Task::none(), @@ -654,7 +647,7 @@ impl<'b> ComponentWidgets<'b> { RootWidgetMembers::Form(_) => Task::none(), RootWidgetMembers::Inline(_) => Task::none(), RootWidgetMembers::List(widget) => { - let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, widget.__id__); + let RootState { focused_item, .. } = ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, widget.__id__); let total = widget.content.ordered_members .iter() @@ -686,9 +679,9 @@ impl<'b> ComponentWidgets<'b> { ]) } RootWidgetMembers::Grid(grid_widget) => { - let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); + let RootState { focused_item, .. } = ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); - let amount_per_section_total = Self::grid_section_sizes(grid_widget); + let amount_per_section_total = ComponentWidgets::grid_section_sizes(grid_widget); let total = amount_per_section_total .iter() @@ -750,7 +743,7 @@ impl<'b> ComponentWidgets<'b> { RootWidgetMembers::Inline(_) => Task::none(), RootWidgetMembers::List(_) => Task::none(), RootWidgetMembers::Grid(grid_widget) => { - let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); + let RootState { focused_item, .. } = ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); let _ = focused_item.focus_previous(); @@ -776,7 +769,7 @@ impl<'b> ComponentWidgets<'b> { RootWidgetMembers::Inline(_) => Task::none(), RootWidgetMembers::List(_) => Task::none(), RootWidgetMembers::Grid(grid_widget) => { - let RootState { focused_item, .. } = ComponentWidgets::root_state_mut_on_field(self.state, grid_widget.__id__); + let RootState { focused_item, .. } = ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); let total = grid_widget.content.ordered_members .iter() @@ -805,6 +798,43 @@ impl<'b> ComponentWidgets<'b> { } } } +} + +impl<'b> ComponentWidgets<'b> { + + pub fn first_open(&self) -> AppMsg { + let Some(root_widget) = &self.root_widget else { + return AppMsg::Noop; + }; + + let Some(content) = &root_widget.content else { + return AppMsg::Noop; + }; + + let widget_id = match content { + RootWidgetMembers::List(widget) => { + match &widget.content.search_bar { + None => { + return AppMsg::Noop + } + Some(widget) => widget.__id__ + } + } + RootWidgetMembers::Grid(widget) => { + match &widget.content.search_bar { + None => { + return AppMsg::Noop + } + Some(widget) => widget.__id__ + } + } + _ => return AppMsg::Noop + }; + + AppMsg::FocusPluginViewSearchBar { + widget_id + } + } fn list_focused_item_id(focused_item: &ScrollHandle, widget: &ListWidget) -> Option { let mut items = vec![]; diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index d6c9a56..b839cc5 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -2,7 +2,7 @@ use std::collections::hash_map::Entry; use crate::model::UiViewEvent; use crate::ui::state::PluginViewState; use crate::ui::theme::Element; -use crate::ui::widget::{create_state, ActionPanel, ComponentWidgetEvent, ComponentWidgetState, ComponentWidgets}; +use crate::ui::widget::{create_state, ActionPanel, ComponentWidgetEvent, ComponentWidgetState, ComponentWidgets, ComponentWidgetsMut}; use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiWidgetId}; use std::collections::HashMap; use std::mem; @@ -12,8 +12,8 @@ use iced::Task; use crate::ui::AppMsg; pub struct PluginWidgetContainer { - root_widget: Arc>>>, - state: Arc>>, + root_widget: Option>, + state: HashMap, images: HashMap>, plugin_id: Option, plugin_name: Option, @@ -24,8 +24,8 @@ pub struct PluginWidgetContainer { impl PluginWidgetContainer { pub fn new() -> Self { Self { - root_widget: Arc::new(Mutex::new(None)), - state: Arc::new(Mutex::new(HashMap::new())), + root_widget: None, + state: HashMap::new(), images: HashMap::new(), plugin_id: None, plugin_name: None, @@ -59,15 +59,12 @@ impl PluginWidgetContainer { self.entrypoint_name = Some(entrypoint_name.to_string()); self.images = images; - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - // use new state with values from old state but only widget ids which exists in new state // so we this way we use already existing values but remove state for removed widgets - let old_state = mem::replace(state.deref_mut(), create_state(&container)); + let old_state = mem::replace(&mut self.state, create_state(&container)); for (key, value) in old_state.into_iter() { - match state.entry(key) { + match self.state.entry(key) { Entry::Occupied(mut entry) => { entry.insert(value); } @@ -75,27 +72,25 @@ impl PluginWidgetContainer { } } - let first_open = match root_widget.as_ref() { + let first_open = match self.root_widget.as_ref() { None => true, Some(root_widget) => root_widget.content.is_none() }; - *root_widget = Some(container); + self.root_widget = Some(container); if first_open { - ComponentWidgets::new(&mut root_widget, &mut state, plugin_id.clone(), &self.images) + ComponentWidgets::new(&mut self.root_widget, &mut self.state, plugin_id.clone(), &self.images) .first_open() } else { AppMsg::Noop } } - pub fn handle_event(&self, plugin_id: PluginId, event: ComponentWidgetEvent) -> Option { - let mut state = self.state.lock().expect("lock is poisoned"); - + pub fn handle_event(&mut self, plugin_id: PluginId, event: ComponentWidgetEvent) -> Option { let widget_id = event.widget_id(); - event.handle(plugin_id, state.get_mut(&widget_id)) + event.handle(plugin_id, self.state.get_mut(&widget_id)) } pub fn render_root_widget<'a>( @@ -103,95 +98,75 @@ impl PluginWidgetContainer { plugin_view_state: &PluginViewState, action_shortcuts: &HashMap, ) -> Element<'a, ComponentWidgetEvent> { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images) + ComponentWidgets::new(&self.root_widget, &self.state, self.get_plugin_id(), &self.images) .render_root_widget(plugin_view_state, self.entrypoint_name.as_ref(), action_shortcuts) } pub fn render_inline_root_widget<'a>(&self) -> Element<'a, ComponentWidgetEvent> { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images) + ComponentWidgets::new(&self.root_widget, &self.state, self.get_plugin_id(), &self.images) .render_root_inline_widget(self.plugin_name.as_ref(), self.entrypoint_name.as_ref()) } - pub fn append_text(&self, text: &str) -> Task { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).append_text(text) + pub fn append_text(&mut self, text: &str) -> Task { + let plugin_id = self.get_plugin_id(); + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) + .append_text(text) } - pub fn backspace_text(&self) -> Task { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).backspace_text() + pub fn backspace_text(&mut self) -> Task { + let plugin_id = self.get_plugin_id(); + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) + .backspace_text() } pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Task { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).focus_search_bar(widget_id) + let plugin_id = self.get_plugin_id(); + ComponentWidgets::new(&self.root_widget, &self.state, plugin_id, &self.images) + .focus_search_bar(widget_id) } - pub fn toggle_action_panel(&self) { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).toggle_action_panel() + pub fn toggle_action_panel(&mut self) { + let plugin_id = self.get_plugin_id(); + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) + .toggle_action_panel() } pub fn get_action_ids(&self) -> Vec { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).get_action_ids() + ComponentWidgets::new(&self.root_widget, &self.state, self.get_plugin_id(), &self.images) + .get_action_ids() } pub fn get_focused_item_id(&self) -> Option { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).get_focused_item_id() + ComponentWidgets::new(&self.root_widget, &self.state, self.get_plugin_id(), &self.images) + .get_focused_item_id() } pub fn get_action_panel(&self, action_shortcuts: &HashMap) -> Option { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).get_action_panel(action_shortcuts) + ComponentWidgets::new(&self.root_widget, &self.state, self.get_plugin_id(), &self.images) + .get_action_panel(action_shortcuts) } - pub fn focus_up(&self) -> Task { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).focus_up() + pub fn focus_up(&mut self) -> Task { + let plugin_id = self.get_plugin_id(); + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) + .focus_up() } - pub fn focus_down(&self) -> Task { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).focus_down() + pub fn focus_down(&mut self) -> Task { + let plugin_id = self.get_plugin_id(); + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) + .focus_down() } - pub fn focus_left(&self) -> Task { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).focus_left() + pub fn focus_left(&mut self) -> Task { + let plugin_id = self.get_plugin_id(); + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) + .focus_left() } - pub fn focus_right(&self) -> Task { - let mut root_widget = self.root_widget.lock().expect("lock is poisoned"); - let mut state = self.state.lock().expect("lock is poisoned"); - - ComponentWidgets::new(&mut root_widget, &mut state, self.get_plugin_id(), &self.images).focus_right() + pub fn focus_right(&mut self) -> Task { + let plugin_id = self.get_plugin_id(); + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) + .focus_right() } } From d237d2b38a9546019d89828315b0289067e0ad63 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 18 Jan 2025 19:06:23 +0100 Subject: [PATCH 304/540] Enable experimental window tracking by default --- bundled_plugins/gauntlet/gauntlet.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundled_plugins/gauntlet/gauntlet.toml b/bundled_plugins/gauntlet/gauntlet.toml index 198730b..371d915 100644 --- a/bundled_plugins/gauntlet/gauntlet.toml +++ b/bundled_plugins/gauntlet/gauntlet.toml @@ -13,7 +13,7 @@ description = 'Run installed applications from your system' id = 'experimentalWindowTracking' name = 'Experimental Window Tracking' type = 'bool' -default = false +default = true description = "Enables experimental window tracking" [[entrypoint]] From bd268cbb017b4fc60d1ba1de5ec6e7de1d51f96c Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 18 Jan 2025 21:46:30 +0100 Subject: [PATCH 305/540] Update CHANGELOG.md --- CHANGELOG.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27a44db..e0f4991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,95 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +### General +- Window Tracking + - Gauntlet now tracks opened windows and assigns them to specific application entry in results + - If application has window open, primary action now instead focuses the window, or if there are multiple opens view which contains list of windows that can be focused + - If application has window open, it is still possible to open new application instance by using separate new action + - It is experimental, and it is possible to disable window tracking by unchecking checkbox in Application entrypoint preferences in Settings UI + - Currently supported on + - Linux X11 + - wlroots-based window managers + - Hyprland + - Cosmic +- Added "Show all opened windows" entrypoint to bundled plugin +- Application plugin is now implemented on Windows +- macOS native-like dark and light mode themes are now available +- On macOS theme is now auto-selected based on system theme +- On macOS window can now be dragged to change its position + - Window position is saved and will be used after restart +- Binary side has been reduced by around 40% (contributed by @davfsa) +- Added `main_window.close_on_unfocus` boolean option to config file to disable "close on unfocus" functionality of main window + - Intended to be used when using "window focus follows mouse" functionality of OS, Desktop Environment or Window Manager +- Added option in Settings UI to choose were main window appears when opening it + - Current options + - `Static` + - Window always opens in the same location (on macOS location can be changed by dragging the window) + - `Active Monitor` + - Windows opens on monitor which has currently focused window + - Currently supported only on macOS + +### Theming +- Themes have been reworked + - Removed complex themes + - Removed theme versioning + - Removed sample generation commands + - Themes are now defined in TOML format + - Theme file is located in config directory (varies based on OS) with name `theme.toml` + - Format of theme file has been reworked, see bundled themes for examples +- 3 bundled themes are now available: [Bundled themes](./bundled_themes) + - Legacy (previous default theme) + - macOS Light + - macOS Dark +- It is possible to change theme in Settings UI + - By default, theme is auto-detected to use one of the bundled ones + - Setting is locked if theme config file exists + +### Plugins +- Entrypoint Generator improvements + - **BREAKING CHANGE**: Renamed `"command-generator"` entrypoint type into `"entrypoint-generator"`, as well as all types related to it + - **BREAKING CHANGE**: Removed `GeneratedEntrypoint`'s `fn: () => void` + - `actions: GeneratedEntrypointAction[]` field now is required to have at least one element + - It is now possible to specify label displayed on bottom row panel for primary action + - **BREAKING CHANGE**: Renamed `GeneratedEntrypointAction`'s `fn` field into `run` + - It is now possible to have `GeneratedEntrypointAction` which opens view instead of running command by specifying `view` field with value of React `FC` type instead of `run` + - Renamed `GeneratorProps` to `GeneratorContext` + - Added `pluginPreferences` and `entrypointPreferences` properties to `GeneratorContext` to access preferences from Entrypoint Generator + - Added `get: (id: string) => GeneratedEntrypoint | undefined` function to `GeneratorContext` to get added entrypoint + - Added `getAll: () => GeneratedEntrypoint[]` function to `GeneratorContext` to get all added entrypoints + - Generated Entrypoints can now have accessories similar to `` component +- Removed `pluginPreferences` and `entrypointPreferences` +- Add `usePluginPreferences` and `useEntrypointPreferences` React Hooks +- Unified primary and secondary action execution in `` and `` + - **BREAKING CHANGE**: Removed `onClick` property on `` and `` components + - **BREAKING CHANGE**: `` and `` now has how `id: string` required property + - If primary or secondary action is executed when `` and `` is focused, `onAction` handler first parameter will be value of `id` prop of focused item +- Added `onItemFocusChange?: (itemId: string | undefined) => void` property on `` and ``. Function is called when focused item changes +- **BREAKING CHANGE**: Renamed `Image` type to `ImageLike` to avoid conflict with `` component +- When entrypoint is enabled/disabled or preference value is changed whole plugin runtime is restarted instead of just reloading the search index +- It is now possible to control whether the action closes main window by returning `{ close: true }` object from `onAction` property function + - For `` view and commands (including generated commands) action always closes window without possibility to keep it open +- Improved rejected promise error log + +### UI/UX Improvements +- On macOS main window now uses native window decorations +- Show name of generator entrypoint near plugin name of entrypoints generated by it in main view search results +- Improved styling of action panel popup + - Tweaked padding between sections + - Added shadow around it +- Tweak height of `` to be slightly taller +- Values of fields in `` are now positioned on the same row as labels + +### Fixes +- Fixed one thread having close to 100% CPU usage while main window is hidden +- Fixed icons in main search view sometimes not loading when window is opened or disappearing after scrolling +- Fixed commands in `permissions.exec.command` in Plugin Manifest not being resolved properly +- Fixed zombie processes being left over after plugin runtime is stopped +- Fixed `npm run dev` failing to reload in some cases +- Fixed `` not displaying the image +- Fixed image in `` being too big so labels are not shown +- Fixed action not being run if `` or `` view has focused `` + ## [12] - 2024-12-22 ### General @@ -54,7 +143,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ### Plugin API - Added `` component in `` and `` which is text input field above content of the respective view - `"command-generator"` entrypoints have been reworked - - Now it is possible to update list of generated entrypoints (add or remove) after the main entrypoint generator function has finished running + - Now it is possible to update list of generated entrypoints (add or remove) after the main command generator function has finished running - **BREAKING CHANGE**: Command Generator entrypoint function now accepts an object with `add: (id: string, data: GeneratedCommand) => void` and `remove: (id: string) => void` functions - **BREAKING CHANGE**: Command Generator entrypoint function now should return nothing or a cleanup function e.g. close file watcher. Currently, it is called when disabling/enabling any of entrypoints in plugin, but it is not called when whole plugin is stopped - While generator function itself is running (given that the function is async) the loading bar and "Indexing..." text in bottom panel will be shown in main window From 7c440c5dcd46a9bd15f9d9ce854f10134bfade39 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 18 Jan 2025 22:34:38 +0100 Subject: [PATCH 306/540] Improve config and theme config parsing error logs --- rust/server/src/plugins/config_reader.rs | 3 ++- rust/server/src/plugins/theme.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rust/server/src/plugins/config_reader.rs b/rust/server/src/plugins/config_reader.rs index 3b33c52..ae918a0 100644 --- a/rust/server/src/plugins/config_reader.rs +++ b/rust/server/src/plugins/config_reader.rs @@ -49,7 +49,7 @@ impl ConfigReader { Ok(config_content) => { toml::from_str(&config_content) .unwrap_or_else(|err| { - tracing::error!("Unable to parse config, error: {:?}", err); + tracing::error!("Unable to parse config, error: {:#}", err); ApplicationConfig::default() }) @@ -68,6 +68,7 @@ impl ConfigReader { } #[derive(Debug, Deserialize, Default)] +#[serde(deny_unknown_fields)] pub struct ApplicationConfig { main_window: Option // #[serde(default)] diff --git a/rust/server/src/plugins/theme.rs b/rust/server/src/plugins/theme.rs index 1cae175..71face3 100644 --- a/rust/server/src/plugins/theme.rs +++ b/rust/server/src/plugins/theme.rs @@ -158,7 +158,7 @@ pub fn read_theme_file(theme_file: PathBuf) -> Option { match parse_theme(&value) { Ok(value) => Some(value), Err(err) => { - tracing::warn!("Unable to parse theme file: {:?} - {:#}", theme_file, err); + tracing::error!("Unable to parse theme config file: {:#}", err); None } } @@ -221,6 +221,7 @@ pub struct ConfigThemeContentBorder { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct ConfigTheme { pub mode: ConfigThemeMode, // value of tint/tones/shades/whatever you have, from lower to higher From 66bf1c861c3dcebe07a900311e5124dbd8e20f0c Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 18 Jan 2025 22:42:32 +0100 Subject: [PATCH 307/540] Move image download example in dev_plugin to separate entrypoint to not slow down detail-view --- dev_plugin/gauntlet.toml | 6 ++++++ dev_plugin/src/detail-img-download-view.tsx | 13 +++++++++++++ dev_plugin/src/detail-view.tsx | 1 - 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 dev_plugin/src/detail-img-download-view.tsx diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index dae63c1..1123176 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -32,6 +32,12 @@ type = 'bool' default = true description = "test bool description" +[[entrypoint]] +id = 'detail-img-download-view' +name = 'Detail Img Download view' +path = 'src/detail-img-download-view.tsx' +type = 'view' +description = """""" [[entrypoint]] id = 'detail-view' diff --git a/dev_plugin/src/detail-img-download-view.tsx b/dev_plugin/src/detail-img-download-view.tsx new file mode 100644 index 0000000..f2a3b90 --- /dev/null +++ b/dev_plugin/src/detail-img-download-view.tsx @@ -0,0 +1,13 @@ +import { ReactElement } from 'react'; +import { Detail } from "@project-gauntlet/api/components"; + +export default function DetailView(): ReactElement { + return ( + + + + + + ); +}; + diff --git a/dev_plugin/src/detail-view.tsx b/dev_plugin/src/detail-view.tsx index ef92776..208b2ca 100644 --- a/dev_plugin/src/detail-view.tsx +++ b/dev_plugin/src/detail-view.tsx @@ -124,7 +124,6 @@ export default function DetailView(): ReactElement { H5 Title H6 Title - Code block Test From 8d66cac69fe0841b8f6bdbeb75ec23c0e5e44e4f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 11:21:15 +0100 Subject: [PATCH 308/540] Fix `npm run dev` failing because of missing log files when run for the first time --- rust/plugin_runtime/src/deno.rs | 8 ++------ rust/server/src/plugins/js.rs | 6 ++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index ea29d59..f7f2d7c 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -322,9 +322,7 @@ pub async fn start_js_runtime( let stdout = if let Some(stdout_file) = init.stdout_file { let stdout_file = PathBuf::from(stdout_file); - std::fs::create_dir_all(stdout_file.parent().unwrap())?; - - let out_log_file = File::create(stdout_file)?; + let out_log_file = File::open(stdout_file)?; StdioPipe::file(out_log_file) } else { @@ -334,9 +332,7 @@ pub async fn start_js_runtime( let stderr = if let Some(stderr_file) = init.stderr_file { let stderr_file = PathBuf::from(stderr_file); - std::fs::create_dir_all(stderr_file.parent().unwrap())?; - - let err_log_file = File::create(stderr_file)?; + let err_log_file = File::open(stderr_file)?; StdioPipe::file(err_log_file) } else { diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index a846d71..90eeb61 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -154,11 +154,17 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run let (stdout_file, stderr_file) = if dev_plugin { let (stdout_file, stderr_file) = data.dirs.plugin_log_files(&plugin_uuid); + std::fs::create_dir_all(stdout_file.parent().unwrap())?; + File::create(&stdout_file)?; + let stdout_file = stdout_file .to_str() .context("non-uft8 paths are not supported")? .to_string(); + std::fs::create_dir_all(stderr_file.parent().unwrap())?; + File::create(&stderr_file)?; + let stderr_file = stderr_file.to_str() .context("non-uft8 paths are not supported")? .to_string(); From 085b5ce7e36698db16216aa99a0a76670c4536b9 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 11:45:15 +0100 Subject: [PATCH 309/540] Revert "Fix `npm run dev` failing because of missing log files when run for the first time" This reverts commit 8d66cac69fe0841b8f6bdbeb75ec23c0e5e44e4f. --- rust/plugin_runtime/src/deno.rs | 8 ++++++-- rust/server/src/plugins/js.rs | 6 ------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index f7f2d7c..ea29d59 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -322,7 +322,9 @@ pub async fn start_js_runtime( let stdout = if let Some(stdout_file) = init.stdout_file { let stdout_file = PathBuf::from(stdout_file); - let out_log_file = File::open(stdout_file)?; + std::fs::create_dir_all(stdout_file.parent().unwrap())?; + + let out_log_file = File::create(stdout_file)?; StdioPipe::file(out_log_file) } else { @@ -332,7 +334,9 @@ pub async fn start_js_runtime( let stderr = if let Some(stderr_file) = init.stderr_file { let stderr_file = PathBuf::from(stderr_file); - let err_log_file = File::open(stderr_file)?; + std::fs::create_dir_all(stderr_file.parent().unwrap())?; + + let err_log_file = File::create(stderr_file)?; StdioPipe::file(err_log_file) } else { diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 90eeb61..a846d71 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -154,17 +154,11 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run let (stdout_file, stderr_file) = if dev_plugin { let (stdout_file, stderr_file) = data.dirs.plugin_log_files(&plugin_uuid); - std::fs::create_dir_all(stdout_file.parent().unwrap())?; - File::create(&stdout_file)?; - let stdout_file = stdout_file .to_str() .context("non-uft8 paths are not supported")? .to_string(); - std::fs::create_dir_all(stderr_file.parent().unwrap())?; - File::create(&stderr_file)?; - let stderr_file = stderr_file.to_str() .context("non-uft8 paths are not supported")? .to_string(); From 4478f4a99e21b09b46c88ad519370708c72d1386 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 11:53:26 +0100 Subject: [PATCH 310/540] Fix `npm run dev` failing because of missing log files when run for the first time --- rust/plugin_runtime/src/deno.rs | 8 ++------ rust/server/src/plugins/js.rs | 6 ++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index ea29d59..3abfafe 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -322,9 +322,7 @@ pub async fn start_js_runtime( let stdout = if let Some(stdout_file) = init.stdout_file { let stdout_file = PathBuf::from(stdout_file); - std::fs::create_dir_all(stdout_file.parent().unwrap())?; - - let out_log_file = File::create(stdout_file)?; + let out_log_file = File::options().write(true).open(stdout_file)?; StdioPipe::file(out_log_file) } else { @@ -334,9 +332,7 @@ pub async fn start_js_runtime( let stderr = if let Some(stderr_file) = init.stderr_file { let stderr_file = PathBuf::from(stderr_file); - std::fs::create_dir_all(stderr_file.parent().unwrap())?; - - let err_log_file = File::create(stderr_file)?; + let err_log_file = File::options().write(true).open(stderr_file)?; StdioPipe::file(err_log_file) } else { diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index a846d71..90eeb61 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -154,11 +154,17 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run let (stdout_file, stderr_file) = if dev_plugin { let (stdout_file, stderr_file) = data.dirs.plugin_log_files(&plugin_uuid); + std::fs::create_dir_all(stdout_file.parent().unwrap())?; + File::create(&stdout_file)?; + let stdout_file = stdout_file .to_str() .context("non-uft8 paths are not supported")? .to_string(); + std::fs::create_dir_all(stderr_file.parent().unwrap())?; + File::create(&stderr_file)?; + let stderr_file = stderr_file.to_str() .context("non-uft8 paths are not supported")? .to_string(); From 6f7732ce3896fbd67aa488fb231a7689534f6c0a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 12:06:14 +0100 Subject: [PATCH 311/540] Pass CommandContext object to commands which contains plugin and entrypoint preferences --- dev_plugin/src/command-a.ts | 8 +++++++- js/api/src/helpers.ts | 5 +++++ js/core/src/core.tsx | 17 ++++++++++++++--- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/dev_plugin/src/command-a.ts b/dev_plugin/src/command-a.ts index fb6381d..1ce4de0 100644 --- a/dev_plugin/src/command-a.ts +++ b/dev_plugin/src/command-a.ts @@ -1,7 +1,13 @@ -export default function Command() { +import { CommandContext } from "@project-gauntlet/api/helpers"; + +export default function Command({ pluginPreferences, entrypointPreferences }: CommandContext) { const env = Deno.env.get("LD_LIBRARY_PATH"); console.log("LD_LIBRARY_PATH:", env); + console.log("pluginPreferences:"); + console.dir(pluginPreferences); + console.log("entrypointPreferences:"); + console.dir(entrypointPreferences); const command = new Deno.Command("echo", { args: ["test"], diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index 210f745..fa9c3b4 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -68,6 +68,11 @@ export type GeneratorContext

= { entrypointPreferences: E, }; +export type CommandContext

= { + pluginPreferences: P, + entrypointPreferences: E, +}; + export const Clipboard: Clipboard = { read: async function (): Promise<{ "text/plain"?: string | undefined; "image/png"?: ArrayBuffer | undefined; }> { const data = await clipboard_read(); diff --git a/js/core/src/core.tsx b/js/core/src/core.tsx index 2f3aa74..aa4b2a4 100644 --- a/js/core/src/core.tsx +++ b/js/core/src/core.tsx @@ -3,7 +3,10 @@ import { runEntrypointGenerators, runGeneratedEntrypoint, runGeneratedEntrypoint import { reloadSearchIndex } from "./search-index"; import { closeView, handleEvent, handlePluginViewKeyboardEvent, renderInlineView, renderView } from "./render"; import { - entrypoint_preferences_required, op_entrypoint_names, + entrypoint_preferences_required, + get_entrypoint_preferences, + get_plugin_preferences, + op_entrypoint_names, op_inline_view_entrypoint_id, op_log_trace, op_plugin_get_pending_event, @@ -97,8 +100,16 @@ export async function runPluginLoop() { break; } - const command: () => Promise | void = (await import(`gauntlet:entrypoint?${pluginEvent.entrypointId}`)).default; - command() + type CommandContext

= { + pluginPreferences: P, + entrypointPreferences: E, + }; + + const pluginPreferences = get_plugin_preferences(); + const entrypointPreferences = get_entrypoint_preferences(pluginEvent.entrypointId); + + const command: (context: CommandContext) => Promise | void = (await import(`gauntlet:entrypoint?${pluginEvent.entrypointId}`)).default; + command({ pluginPreferences, entrypointPreferences }) } catch (e) { console.error("Error occurred when running a command", pluginEvent.entrypointId, e) } From d861b730efce1ff274a7bab61a5ec36219d29e71 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 12:09:58 +0100 Subject: [PATCH 312/540] Update CHANGELOG.md, README.md and THEME.md --- CHANGELOG.md | 11 ++++++++--- README.md | 43 ++++++++++++++----------------------------- docs/THEME.md | 45 +++++++++++---------------------------------- 3 files changed, 33 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0f4991..6b917c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ### General - Window Tracking - Gauntlet now tracks opened windows and assigns them to specific application entry in results - - If application has window open, primary action now instead focuses the window, or if there are multiple opens view which contains list of windows that can be focused + - If application has window open, primary action now instead focuses the window + - If there are multiple windows open, primary action opens view which contains list of windows that can be focused - If application has window open, it is still possible to open new application instance by using separate new action - It is experimental, and it is possible to disable window tracking by unchecking checkbox in Application entrypoint preferences in Settings UI - Currently supported on @@ -36,6 +37,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git - `Active Monitor` - Windows opens on monitor which has currently focused window - Currently supported only on macOS +- Improve config and theme config error parsing logs ### Theming - Themes have been reworked @@ -66,8 +68,10 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git - Added `get: (id: string) => GeneratedEntrypoint | undefined` function to `GeneratorContext` to get added entrypoint - Added `getAll: () => GeneratedEntrypoint[]` function to `GeneratorContext` to get all added entrypoints - Generated Entrypoints can now have accessories similar to `` component -- Removed `pluginPreferences` and `entrypointPreferences` -- Add `usePluginPreferences` and `useEntrypointPreferences` React Hooks +- Removed `pluginPreferences` and `entrypointPreferences` helper functions +- Added `usePluginPreferences` and `useEntrypointPreferences` React Hooks +- Command function now receives `CommandContext` as first argument + - Object contains `pluginPreferences` and `entrypointPreferences` properties to access preferences from Command - Unified primary and secondary action execution in `` and `` - **BREAKING CHANGE**: Removed `onClick` property on `` and `` components - **BREAKING CHANGE**: `` and `` now has how `id: string` required property @@ -97,6 +101,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git - Fixed `` not displaying the image - Fixed image in `` being too big so labels are not shown - Fixed action not being run if `` or `` view has focused `` +- Fixed `npm run dev` failing because of missing log files when run for the first time ## [12] - 2024-12-22 diff --git a/README.md b/README.md index d349ee1..c312929 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ -Web-first cross-platform application launcher with React-based plugins. +Web-first cross-platform application launcher with React-based plugins > [!NOTE] > Launcher is in active development, expect bugs, missing features, incomplete ux, etc. @@ -71,7 +71,6 @@ https://github.com/user-attachments/assets/19964ed6-9cd9-48d4-9835-6be04de14b66 - Application plugin depends on `gtk-launch` - macOS - Windows - - Bundled "Applications" plugin is not yet implemented. See [#9](https://github.com/project-gauntlet/gauntlet/issues/9) ##### Planned features @@ -216,7 +215,7 @@ Main window can be opened using global shortcut or CLI command: name = 'Plugin Name' description = """ Plugin description -""" # required +""" [[preferences]] # plugin preference name = 'testBool' @@ -229,8 +228,9 @@ enum_values = [{ label = 'Item', value = 'item'}] # defines list of available en id = 'ui-view' # id for entrypoint name = 'UI view' # name of entrypoint path = 'src/ui-view.tsx' # path to file, default export is expected to be function React Function Component +icon = 'icon.png' # optional, path to file inside assets dir type = 'view' -description = 'Some entrypoint description' # required +description = 'Some entrypoint description' [[entrypoint.preferences]] # entrypoint preference name = 'boolPreference' @@ -248,21 +248,21 @@ id = 'command-a' name = 'Command A' path = 'src/command-a.ts' # path to file, the whole file is a js script type = 'command' -description = 'Some entrypoint description' # required +description = 'Some entrypoint description' [[entrypoint]] id = 'command-generator' name = 'Command generator' path = 'src/command-generator.ts' type = 'command-generator' -description = 'Some entrypoint description' # required +description = 'Some entrypoint description' [[entrypoint]] id = 'inline-view' name = 'Inline view' path = 'src/inline-view.tsx' type = 'inline-view' -description = 'Some entrypoint description' # required +description = 'Some entrypoint description' [permissions] network = ["github.com", "example.com:8833"] @@ -311,8 +311,6 @@ The Application has a simple command line interface - `gauntlet --minimized` - starts server without opening main window - `gauntlet open` - opens application window, can be used instead of global shortcut - `gauntlet settings` - settings, plugin installation and removal, preferences, etc -- `gauntlet generate-sample-simple-theme` - generate sample of simple theme. See: [THEME.md](./docs/THEME.md) -- `gauntlet generate-sample-complex-theme` - generate sample of complex theme. See: [THEME.md](./docs/THEME.md) ### Dev Tools @@ -336,10 +334,8 @@ See [THEME.md](./docs/THEME.md) ## Architecture -The Application consists of three parts: server, frontend and settings. -Server is an application that exposes gRPC server. -All plugins run on server. -Each plugin in its own sandboxed Deno Worker. +The Application consists of 4 parts: server, frontend, plugin runtime and settings. +Each plugin runs in separate plugin runtime in separate OS process. Each plugin is its own sandboxed Deno Worker. In plugin manifest it is possible to configure permissions which will allow plugin to have access to filesystem, network, environment variables or subprocess execution. Server saves plugins themselves and state of plugins into SQLite database. @@ -347,23 +343,20 @@ Server saves plugins themselves and state of plugins into SQLite database. Frontend is GUI module that uses [iced-rs](https://github.com/iced-rs/iced) as a GUI framework. It is run in the same process as a server. Plugins can create UI using [React](https://github.com/facebook/react). -Server implements custom React Reconciler (similar to React Native) which renders GUI components to frontend. -Server listens on signals from frontend, so when user opens view defined by plugin, frontend sends an open-view request. -Server then receives it, runs React render and React Reconciler -makes requests to the frontend containing information what actually should be rendered. +Plugin Runtime implements custom React Reconciler (similar to React Native) which renders GUI components to frontend. +Plugin Runtime listens on signals from frontend, so when user opens view defined by plugin, frontend sends an open-view request. +Plugin Runtime then receives it, runs React render and React Reconciler makes requests to the frontend containing information what actually should be rendered. When a user interacts with the UI by clicking button or entering text into form, frontend sends events to server to see whether any re-renders are needed. -Settings is a GUI application runs in separate process that communicates with server via gRPC using a simple request-response approach. +Settings is a GUI application runs in separate process that communicates with server using a simple request-response approach. -Simplified gRPC communication: +Simplified communication: ![](docs/architecture.png) Components: ![](docs/architecture-blocks.png) -Each component runs in a separate thread. Main thread is the thread that renders GUI. Each component has its own tokio runtime instance. - Plugins (or rather its compiled state: manifest, js code and assets) are distributed via Git repository in `gauntlet/release` branch (similar to GitHub Pages). Which means there is no one central place required for plugin distribution. And to install plugin all you need is Git repository url. @@ -402,13 +395,6 @@ Relevant CLI commands: - contains log files created by plugin development - `.desktop` files at locations defined by [Desktop Entry Specification](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html) -Application and Dev Tools use temporary directories: - -- Rust: [tempfile crate](https://crates.io/crates/tempfile) -- JS: [NodeJS mkdtemp](https://nodejs.org/api/fs.html#fspromisesmkdtempprefix-options) - -X11 API is used to add global shortcut - Client and Setting applications have GUI and therefore use all the usual graphics-related stuff from X11. Wayland support requires LayerShell protocol `zwlr_layer_shell_v1`. @@ -442,7 +428,6 @@ But the new version release needs to be done via GitHub Actions If you'd like to help build Gauntlet you can do it in more ways than just contributing code: - Reporting a bug or UI/UX problem - Creating a plugin -- Creating and contributing a theme - see [#16](https://github.com/project-gauntlet/gauntlet/issues/16) If you are looking for things to do see pinned [issues](https://github.com/project-gauntlet/gauntlet/issues). diff --git a/docs/THEME.md b/docs/THEME.md index b911de6..98af1b0 100644 --- a/docs/THEME.md +++ b/docs/THEME.md @@ -1,44 +1,21 @@ # Gauntlet Theming -Gauntlet has extensive theming possibilities +Currently, in Gauntlet with themes it is possible to change (list is likely be extended with future updates): +- Colors of text and background +- Window border color, width and radius +- Border radius of components in content -There are 2 types of themes: -- Simple Theme - - Global colors - - Global border width and radius -- Complex Theme - - Colors on per widget type bases - - Paddings and spacing on per widget type bases - - Borders colors, width and radius on per widget type bases +Theming is only affects main window and doesn't affect settings -Unfortunately due to the internally invasive nature of themes, it is perpetually unstable feature. -Themes are versioned and only one version is supported by application at the same time. -Meaning if there were some changes made in the release and theme version was incremented, -theme will stop working until it is updated. -This may change in the future +Theme config file is in TOML format -Current version: -- Simple Theme: `4` -- Complex Theme: `4` - -Theming is only applied to main window and doesn't affect settings - -### Creating a custom theme - -Gauntlet provides 2 CLI commands to generate sample: `generate-sample-simple-theme` and `generate-sample-complex-theme`. Sample is just a default theme that has been saved to file. - -Running the command will create sample file, print location of that sample file -and will print location to which theme file will need to be saved to be detected by application +Theme config file locations: +- Windows: `C:\Users\Username\AppData\Roaming\Gauntlet\config\theme.toml` +- Linux: `$XDG_CONFIG_HOME/gauntlet/theme.toml` +- macOS: `$HOME/Library/Application Support/dev.project-gauntlet.gauntlet/theme.toml` Currently, theme change is only applied after application restart Any errors in theme parsing will be shown in application logs -#### Linux -- `gauntlet generate-sample-simple-theme` -- `gauntlet generate-sample-complex-theme` - -#### macOS -Note: the binary is not on the PATH -- `/Applications/Gauntlet.app/Contents/MacOS/Gauntlet generate-sample-simple-theme` -- `/Applications/Gauntlet.app/Contents/MacOS/Gauntlet generate-sample-complex-theme` +See bundled themes for examples [here](./../bundled_themes) From c64dfbfe65787d9ff16f6cb2b470429838456189 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 12:10:16 +0100 Subject: [PATCH 313/540] Bump version in nix overlay --- nix/overlay.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/overlay.nix b/nix/overlay.nix index ce69b6a..0d86e90 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -5,7 +5,7 @@ }: { flake.overlays.default = final: _: let # These must be updated following the instructions in ./nix/README.md when dependencies are updated or the version bumped - version = "v12"; + version = "v13"; npmDepsHash = "sha256-rk5aLdXoSpqMNcSVIRJTKN1KqtddVPiKkZ1YWZ+n5m8="; RUSTY_V8_ARCHIVE = fetchRustyV8 "130.0.2" { aarch64-darwin = "sha256-aWZ/4Q4Wttx37xOdBmTCPGP+eYGhr4CM1UkYq8pC7Qs="; From b0d030dccfc3dcfebaf681c960d6e9a38f9fb43f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 11:32:44 +0000 Subject: [PATCH 314/540] Prepare for v13 release --- CHANGELOG.md | 2 ++ VERSION | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b917c1..c0fb0ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +## [13] - 2025-01-19 + ### General - Window Tracking - Gauntlet now tracks opened windows and assigns them to specific application entry in results diff --git a/VERSION b/VERSION index 3cacc0b..ca7bf83 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -12 \ No newline at end of file +13 \ No newline at end of file From 60a9dccb1d5e72385adde79d2c464526199620c6 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 12:36:41 +0100 Subject: [PATCH 315/540] Fix git pull in wrong place in publish pipeline --- js/build/src/main.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/js/build/src/main.ts b/js/build/src/main.ts index c6c5295..922ef4d 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -85,10 +85,13 @@ async function doPublishInit() { } async function doPublishLinux() { - console.log("Publishing Gauntlet... Linux...") - const projectRoot = getProjectRoot() + const git = simpleGit(projectRoot); + + console.log("git pull...") + await git.pull() + const arch = 'x86_64-unknown-linux-gnu'; buildSize(projectRoot, arch) @@ -102,11 +105,6 @@ async function doBuildLinux() { const arch = 'x86_64-unknown-linux-gnu'; const projectRoot = getProjectRoot(); - const git = simpleGit(projectRoot); - - console.log("git pull...") - await git.pull() - await doBuild(projectRoot, arch) packageForLinux(projectRoot, arch) } From 7bf36f9d8d917ee13bfb66084d536783867c04a2 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 12:36:47 +0100 Subject: [PATCH 316/540] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c312929..743aae7 100644 --- a/README.md +++ b/README.md @@ -251,10 +251,10 @@ type = 'command' description = 'Some entrypoint description' [[entrypoint]] -id = 'command-generator' -name = 'Command generator' -path = 'src/command-generator.ts' -type = 'command-generator' +id = 'entrypoint-generator' +name = 'Entrypoint generator' +path = 'src/entrypoint-generator.ts' +type = 'entrypoint-generator' description = 'Some entrypoint description' [[entrypoint]] From 2a7e21c4707e47adc0a6bf39767708bb19eb3af1 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 13:17:13 +0100 Subject: [PATCH 317/540] Fix broken publish pipeline --- js/build/src/main.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/build/src/main.ts b/js/build/src/main.ts index 922ef4d..b52c977 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -288,7 +288,7 @@ async function makeRepoChanges(projectRoot: string): Promise<{ releaseNotes: str } function packageForLinux(projectRoot: string, arch: string): { filePath: string; fileName: string } { - const releaseDirPath = path.join(projectRoot, 'target', arch, 'release'); + const releaseDirPath = path.join(projectRoot, 'target', arch, 'release-size'); const assetsDirPath = path.join(projectRoot, 'assets', 'linux'); const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet'); @@ -331,7 +331,7 @@ function packageForLinux(projectRoot: string, arch: string): { filePath: string; } async function packageForMacos(projectRoot: string, arch: string, sign: boolean, notarize: boolean): Promise<{ filePath: string; fileName: string }> { - const releaseDirPath = path.join(projectRoot, 'target', arch, 'release'); + const releaseDirPath = path.join(projectRoot, 'target', arch, 'release-size'); const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet'); const outFileName = "gauntlet-aarch64-macos.dmg" const outFilePath = path.join(releaseDirPath, outFileName); @@ -444,7 +444,7 @@ async function packageForMacos(projectRoot: string, arch: string, sign: boolean, } async function packageForWindows(projectRoot: string, arch: string): Promise<{ filePath: string; fileName: string }> { - const releaseDirPath = path.join(projectRoot, 'target', arch, 'release'); + const releaseDirPath = path.join(projectRoot, 'target', arch, 'release-size'); const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet.exe'); const outFileName = "gauntlet-x86_64-windows.msi" const outFilePath = path.join(releaseDirPath, outFileName); From c4e2b763c2d99cf36d9d96cf7e44ebf91cd2eb7c Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 13:29:13 +0100 Subject: [PATCH 318/540] Fix broken build pipeline after previous commit --- js/build/src/main.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/js/build/src/main.ts b/js/build/src/main.ts index b52c977..69be32d 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -96,7 +96,7 @@ async function doPublishLinux() { buildSize(projectRoot, arch) - const { fileName, filePath } = packageForLinux(projectRoot, arch) + const { fileName, filePath } = packageForLinux(projectRoot, arch, 'release-size') await addFileToRelease(filePath, fileName) } @@ -106,7 +106,7 @@ async function doBuildLinux() { const projectRoot = getProjectRoot(); await doBuild(projectRoot, arch) - packageForLinux(projectRoot, arch) + packageForLinux(projectRoot, arch, 'release') } async function doPublishMacOS() { @@ -121,7 +121,7 @@ async function doPublishMacOS() { buildSize(projectRoot, arch) - const { fileName, filePath } = await packageForMacos(projectRoot, arch, true, true) + const { fileName, filePath } = await packageForMacos(projectRoot, arch, 'release-size', true, true) await addFileToRelease(filePath, fileName) } @@ -131,7 +131,7 @@ async function doBuildMacOS() { const arch = 'aarch64-apple-darwin'; await doBuild(projectRoot, arch) - await packageForMacos(projectRoot, arch, false, false) + await packageForMacos(projectRoot, arch, 'release', false, false) } async function doPublishWindows() { @@ -146,7 +146,7 @@ async function doPublishWindows() { buildSize(projectRoot, arch) - const { fileName, filePath } = await packageForWindows(projectRoot, arch) + const { fileName, filePath } = await packageForWindows(projectRoot, arch, 'release-size') await addFileToRelease(filePath, fileName) } @@ -156,7 +156,7 @@ async function doBuildWindows() { const arch = 'x86_64-pc-windows-msvc'; await doBuild(projectRoot, arch) - await packageForWindows(projectRoot, arch) + await packageForWindows(projectRoot, arch, 'release') } async function doPublishFinal() { @@ -287,8 +287,8 @@ async function makeRepoChanges(projectRoot: string): Promise<{ releaseNotes: str } } -function packageForLinux(projectRoot: string, arch: string): { filePath: string; fileName: string } { - const releaseDirPath = path.join(projectRoot, 'target', arch, 'release-size'); +function packageForLinux(projectRoot: string, arch: string, profile: string): { filePath: string; fileName: string } { + const releaseDirPath = path.join(projectRoot, 'target', arch, profile); const assetsDirPath = path.join(projectRoot, 'assets', 'linux'); const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet'); @@ -330,8 +330,8 @@ function packageForLinux(projectRoot: string, arch: string): { filePath: string; } } -async function packageForMacos(projectRoot: string, arch: string, sign: boolean, notarize: boolean): Promise<{ filePath: string; fileName: string }> { - const releaseDirPath = path.join(projectRoot, 'target', arch, 'release-size'); +async function packageForMacos(projectRoot: string, arch: string, profile: string, sign: boolean, notarize: boolean): Promise<{ filePath: string; fileName: string }> { + const releaseDirPath = path.join(projectRoot, 'target', arch, profile); const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet'); const outFileName = "gauntlet-aarch64-macos.dmg" const outFilePath = path.join(releaseDirPath, outFileName); @@ -443,8 +443,8 @@ async function packageForMacos(projectRoot: string, arch: string, sign: boolean, } } -async function packageForWindows(projectRoot: string, arch: string): Promise<{ filePath: string; fileName: string }> { - const releaseDirPath = path.join(projectRoot, 'target', arch, 'release-size'); +async function packageForWindows(projectRoot: string, arch: string, profile: string): Promise<{ filePath: string; fileName: string }> { + const releaseDirPath = path.join(projectRoot, 'target', arch, profile); const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet.exe'); const outFileName = "gauntlet-x86_64-windows.msi" const outFilePath = path.join(releaseDirPath, outFileName); From 5f42835c1b40a2cc1a94089237466059ca4aedc7 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 15:51:39 +0100 Subject: [PATCH 319/540] Fix typo in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0fb0ba..5c69846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git - On macOS theme is now auto-selected based on system theme - On macOS window can now be dragged to change its position - Window position is saved and will be used after restart -- Binary side has been reduced by around 40% (contributed by @davfsa) +- Binary size has been reduced by around 40% (contributed by @davfsa) - Added `main_window.close_on_unfocus` boolean option to config file to disable "close on unfocus" functionality of main window - Intended to be used when using "window focus follows mouse" functionality of OS, Desktop Environment or Window Manager - Added option in Settings UI to choose were main window appears when opening it From c0e68afc21b1a934ec75af3b72337f8c56615c80 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 18:46:33 +0100 Subject: [PATCH 320/540] Fix build scripts when running locally --- js/build/src/main.ts | 57 +++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/js/build/src/main.ts b/js/build/src/main.ts index 69be32d..3b5311d 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -58,10 +58,10 @@ program.command('build-windows') await program.parseAsync(process.argv); -async function doBuild(projectRoot: string, arch: string) { +async function doBuild(projectRoot: string, arch: string, profile: string) { console.log("Building Gauntlet...") - build(projectRoot, arch) + build(projectRoot, arch, profile) } async function doPublishInit() { @@ -93,10 +93,10 @@ async function doPublishLinux() { await git.pull() const arch = 'x86_64-unknown-linux-gnu'; + const profile = 'release-size'; - buildSize(projectRoot, arch) - - const { fileName, filePath } = packageForLinux(projectRoot, arch, 'release-size') + build(projectRoot, arch, profile) + const { fileName, filePath } = packageForLinux(projectRoot, arch, profile) await addFileToRelease(filePath, fileName) } @@ -104,9 +104,10 @@ async function doPublishLinux() { async function doBuildLinux() { const arch = 'x86_64-unknown-linux-gnu'; const projectRoot = getProjectRoot(); + const profile = 'release'; - await doBuild(projectRoot, arch) - packageForLinux(projectRoot, arch, 'release') + await doBuild(projectRoot, arch, profile) + packageForLinux(projectRoot, arch, profile) } async function doPublishMacOS() { @@ -118,10 +119,10 @@ async function doPublishMacOS() { await git.pull() const arch = 'aarch64-apple-darwin'; + const profile = 'release-size'; - buildSize(projectRoot, arch) - - const { fileName, filePath } = await packageForMacos(projectRoot, arch, 'release-size', true, true) + build(projectRoot, arch, profile) + const { fileName, filePath } = await packageForMacos(projectRoot, arch, profile, true, true) await addFileToRelease(filePath, fileName) } @@ -129,9 +130,10 @@ async function doPublishMacOS() { async function doBuildMacOS() { const projectRoot = getProjectRoot(); const arch = 'aarch64-apple-darwin'; + const profile = 'release'; - await doBuild(projectRoot, arch) - await packageForMacos(projectRoot, arch, 'release', false, false) + await doBuild(projectRoot, arch, profile) + await packageForMacos(projectRoot, arch, profile, false, false) } async function doPublishWindows() { @@ -143,10 +145,11 @@ async function doPublishWindows() { await git.pull() const arch = 'x86_64-pc-windows-msvc'; + const profile = 'release-size'; - buildSize(projectRoot, arch) + build(projectRoot, arch, profile) - const { fileName, filePath } = await packageForWindows(projectRoot, arch, 'release-size') + const { fileName, filePath } = await packageForWindows(projectRoot, arch, profile) await addFileToRelease(filePath, fileName) } @@ -154,9 +157,10 @@ async function doPublishWindows() { async function doBuildWindows() { const projectRoot = getProjectRoot(); const arch = 'x86_64-pc-windows-msvc'; + const profile = 'release'; - await doBuild(projectRoot, arch) - await packageForWindows(projectRoot, arch, 'release') + await doBuild(projectRoot, arch, profile) + await packageForWindows(projectRoot, arch, profile) } async function doPublishFinal() { @@ -174,28 +178,15 @@ async function doPublishFinal() { await publishNpmPackage(projectRoot) } -function build(projectRoot: string, arch: string) { +function build(projectRoot: string, arch: string, profile: string) { buildJs(projectRoot) - buildRust(projectRoot, arch) + buildRust(projectRoot, arch, profile) } -function buildSize(projectRoot: string, arch: string) { - buildJs(projectRoot) - - buildRustSize(projectRoot, arch) -} - -function buildRust(projectRoot: string, arch: string) { +function buildRust(projectRoot: string, arch: string, profile: string) { console.log("Building rust...") - spawnWithErrors('cargo', ['build', '--release', '--features', 'release', '--target', arch], { - cwd: projectRoot - }); -} - -function buildRustSize(projectRoot: string, arch: string) { - console.log("Building rust...") - spawnWithErrors('cargo', ['build', '--profile', 'release-size', '--features', 'release', '--target', arch], { + spawnWithErrors('cargo', ['build', '--profile', profile, '--features', 'release', '--target', arch], { cwd: projectRoot }); } From a285efc47c02e978de2af30a279ecfe3ee92a6d3 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:52:29 +0100 Subject: [PATCH 321/540] Use thin LTO instead of fat LTO which breaks mouse events on macOS --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c69846..9478f88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Fixed mouse actions like scrolling or clicking not working on macOS + ## [13] - 2025-01-19 ### General diff --git a/Cargo.toml b/Cargo.toml index 0a86411..a467fec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ scenario_runner = ["gauntlet-cli/scenario_runner"] [profile.release-size] inherits = "release" opt-level = "s" -lto = true +lto = "thin" strip = true [patch.crates-io] From 1d9da7ec5b063aa8bab8037af2190d3702031ce2 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 20:02:18 +0100 Subject: [PATCH 322/540] Bump version in nix overlay --- nix/overlay.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/overlay.nix b/nix/overlay.nix index 0d86e90..2774817 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -5,7 +5,7 @@ }: { flake.overlays.default = final: _: let # These must be updated following the instructions in ./nix/README.md when dependencies are updated or the version bumped - version = "v13"; + version = "v14"; npmDepsHash = "sha256-rk5aLdXoSpqMNcSVIRJTKN1KqtddVPiKkZ1YWZ+n5m8="; RUSTY_V8_ARCHIVE = fetchRustyV8 "130.0.2" { aarch64-darwin = "sha256-aWZ/4Q4Wttx37xOdBmTCPGP+eYGhr4CM1UkYq8pC7Qs="; From 39046aabf98a1192197d99ce1a540a184a388a1e Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:03:19 +0000 Subject: [PATCH 323/540] Prepare for v14 release --- CHANGELOG.md | 2 ++ VERSION | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9478f88..c5e5e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +## [14] - 2025-01-19 + - Fixed mouse actions like scrolling or clicking not working on macOS ## [13] - 2025-01-19 diff --git a/VERSION b/VERSION index ca7bf83..da2d398 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -13 \ No newline at end of file +14 \ No newline at end of file From 19ff11a501356f4f3ee3c6413e8b51c9a5e5c1cd Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 20 Jan 2025 18:40:24 +0100 Subject: [PATCH 324/540] Remove warning from nix README.md because the issue has been fixed --- nix/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/nix/README.md b/nix/README.md index 9839d57..0c78963 100644 --- a/nix/README.md +++ b/nix/README.md @@ -6,8 +6,6 @@ The Nix package derivation is currently defined for all [default systems](https: Here's how to reference the package derivation (and explicitly pin it) in your `flake.nix`: -**NOTE**: Currently there is an issue with release process causing commit with version tag to have mismatched hash. Please use next commit which updates the hash in following format: `github:project-gauntlet/gauntlet/` - ``` nix { inputs.gauntlet.url = github:project-gauntlet/gauntlet/; From c6761e80b2511d7511ddd7675a5c42de5f404072 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 28 Jan 2025 21:51:15 +0100 Subject: [PATCH 325/540] Use dpi crate from crates.io to avoid multiple instances of same version for cargo vendor --- Cargo.lock | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb38c75..d017c76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3087,11 +3087,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" -[[package]] -name = "dpi" -version = "0.1.1" -source = "git+https://github.com/project-gauntlet/winit.git?rev=b0adb1dd33d82c5fd129c3f9ff5bf2f319955ad1#b0adb1dd33d82c5fd129c3f9ff5bf2f319955ad1" - [[package]] name = "dprint-swc-ext" version = "0.20.0" @@ -5167,7 +5162,7 @@ dependencies = [ [[package]] name = "iced" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" dependencies = [ "iced_core", "iced_futures", @@ -5195,7 +5190,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" dependencies = [ "bitflags 2.6.0", "bytes", @@ -5222,7 +5217,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" dependencies = [ "futures", "iced_core", @@ -5236,7 +5231,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5288,7 +5283,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -5300,7 +5295,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" dependencies = [ "bytes", "iced_core", @@ -5321,7 +5316,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" dependencies = [ "bytemuck", "cosmic-text", @@ -5336,7 +5331,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5355,7 +5350,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" dependencies = [ "iced_renderer", "iced_runtime", @@ -5369,7 +5364,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#815320b573a5d4e4b7442fa29ea8ba69f1acc15c" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" dependencies = [ "iced_futures", "iced_graphics", @@ -6564,7 +6559,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" dependencies = [ "crossbeam-channel", - "dpi 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dpi", "gtk", "keyboard-types", "objc2", @@ -12511,7 +12506,7 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" version = "0.30.99" -source = "git+https://github.com/project-gauntlet/winit.git?rev=b0adb1dd33d82c5fd129c3f9ff5bf2f319955ad1#b0adb1dd33d82c5fd129c3f9ff5bf2f319955ad1" +source = "git+https://github.com/project-gauntlet/winit.git?rev=316726c86d75a751d40677e6d081cc93e917118c#316726c86d75a751d40677e6d081cc93e917118c" dependencies = [ "ahash 0.8.11", "android-activity", @@ -12525,7 +12520,7 @@ dependencies = [ "core-foundation 0.9.4", "core-graphics 0.23.2", "cursor-icon", - "dpi 0.1.1 (git+https://github.com/project-gauntlet/winit.git?rev=b0adb1dd33d82c5fd129c3f9ff5bf2f319955ad1)", + "dpi", "js-sys", "libc", "memmap2 0.9.5", From 2c3a9f9dd381b56aad732d76df58dc21256ef8dc Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 29 Jan 2025 23:25:30 +0100 Subject: [PATCH 326/540] Fix error when registering shortcut erroring whole settings window instead of adding an icon --- rust/common/src/rpc/backend_api.rs | 10 +++--- rust/common/src/rpc/backend_server.rs | 9 +++-- rust/management_client/src/ui.rs | 3 ++ rust/management_client/src/views/general.rs | 40 +++++++++++++++++---- schema/backend.proto | 1 + 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index 9e315a3..7876aeb 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -374,7 +374,7 @@ impl BackendApi { Ok(()) } - pub async fn set_global_shortcut(&mut self, shortcut: Option) -> Result<(), BackendApiError> { + pub async fn set_global_shortcut(&mut self, shortcut: Option) -> Result, BackendApiError> { let request = RpcSetGlobalShortcutRequest { shortcut: shortcut.map(|shortcut| { RpcShortcut { @@ -387,10 +387,12 @@ impl BackendApi { }) }; - self.client.set_global_shortcut(Request::new(request)) - .await?; + let error = self.client.set_global_shortcut(Request::new(request)) + .await? + .into_inner() + .error; - Ok(()) + Ok(error) } pub async fn get_global_shortcut(&mut self) -> Result<(Option, Option), BackendApiError> { diff --git a/rust/common/src/rpc/backend_server.rs b/rust/common/src/rpc/backend_server.rs index edacb16..e4b5b68 100644 --- a/rust/common/src/rpc/backend_server.rs +++ b/rust/common/src/rpc/backend_server.rs @@ -251,11 +251,14 @@ impl RpcBackend for RpcBackendServerImpl { } }); - self.server.set_global_shortcut(shortcut) + let error = self.server.set_global_shortcut(shortcut) .await - .map_err(|err| Status::internal(format!("{:#}", err)))?; + .map_err(|err| format!("{:#}", err)) + .err(); - Ok(Response::new(RpcSetGlobalShortcutResponse::default())) + Ok(Response::new(RpcSetGlobalShortcutResponse { + error + })) } async fn get_global_shortcut(&self, _request: Request) -> Result, Status> { diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index 316300f..7a48918 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -194,6 +194,9 @@ fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task { ManagementAppMsg::HandleBackendError(err) } + ManagementAppGeneralMsgOut::SetGlobalShortcutResponse { shortcut, shortcut_error } => { + ManagementAppMsg::General(ManagementAppGeneralMsgIn::SetGlobalShortcutResponse { shortcut, shortcut_error }) + } } }) } diff --git a/rust/management_client/src/views/general.rs b/rust/management_client/src/views/general.rs index 35f6dc3..0741c6a 100644 --- a/rust/management_client/src/views/general.rs +++ b/rust/management_client/src/views/general.rs @@ -26,6 +26,10 @@ pub enum ManagementAppGeneralMsgIn { CapturingChanged(bool), ThemeChanged(SettingsTheme), WindowPositionModeChanged(WindowPositionMode), + SetGlobalShortcutResponse { + shortcut: Option, + shortcut_error: Option + }, InitSetting { theme: SettingsTheme, window_position_mode: WindowPositionMode, @@ -38,6 +42,10 @@ pub enum ManagementAppGeneralMsgIn { #[derive(Debug, Clone)] pub enum ManagementAppGeneralMsgOut { Noop, + SetGlobalShortcutResponse { + shortcut: Option, + shortcut_error: Option + }, HandleBackendError(BackendApiError) } @@ -63,16 +71,28 @@ impl ManagementAppGeneralState { match message { ManagementAppGeneralMsgIn::ShortcutCaptured(shortcut) => { - self.current_shortcut = shortcut.clone(); - let mut backend_api = backend_api.clone(); - Task::perform(async move { - backend_api.set_global_shortcut(shortcut) - .await?; + Task::perform( + { + let shortcut = shortcut.clone(); - Ok(()) - }, |result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop)) + async move { + let error = backend_api.set_global_shortcut(shortcut) + .await?; + + Ok(error) + } + }, + move |result| { + let shortcut = shortcut.clone(); + + handle_backend_error(result, move |shortcut_error| ManagementAppGeneralMsgOut::SetGlobalShortcutResponse { + shortcut, + shortcut_error, + }) + }, + ) } ManagementAppGeneralMsgIn::Noop => { Task::none() @@ -115,6 +135,12 @@ impl ManagementAppGeneralState { Ok(()) }, |result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop)) } + ManagementAppGeneralMsgIn::SetGlobalShortcutResponse { shortcut, shortcut_error } => { + self.current_shortcut = shortcut; + self.current_shortcut_error = shortcut_error; + + Task::none() + } } } diff --git a/schema/backend.proto b/schema/backend.proto index c0cbfd8..fc6954c 100644 --- a/schema/backend.proto +++ b/schema/backend.proto @@ -87,6 +87,7 @@ message RpcSetGlobalShortcutRequest { } message RpcSetGlobalShortcutResponse { + optional string error = 1; } message RpcGetGlobalShortcutRequest { From 792baa7e6aa6262416d18babda2ef81eb3cd3f30 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 29 Jan 2025 23:27:03 +0100 Subject: [PATCH 327/540] Fix global shortcut unregistration failing if previous shortcut registration failed --- rust/client/src/ui/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index d255944..e99d984 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -2009,9 +2009,16 @@ fn assign_global_shortcut( if let Some(shortcut) = shortcut { let hotkey = convert_physical_shortcut_to_hotkey(shortcut); - *hotkey_guard = Some(hotkey); + match global_hotkey_manager.register(hotkey) { + Ok(()) => { + *hotkey_guard = Some(hotkey); + } + Err(err) => { + *hotkey_guard = None; - global_hotkey_manager.register(hotkey)?; + Err(err)? + } + } } Ok(()) From 5e42d0d6bc6de9c9f345328a8e789244fb0714c6 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 1 Feb 2025 14:55:48 +0100 Subject: [PATCH 328/540] Use null files as stderr/stdout/stdin instead of inheriting from parent. Fixes plugin runtime failing to start on windows when using subsystem windows --- dev_plugin/src/detail-view.tsx | 4 ++++ rust/plugin_runtime/src/deno.rs | 24 +++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/dev_plugin/src/detail-view.tsx b/dev_plugin/src/detail-view.tsx index 208b2ca..44b76bb 100644 --- a/dev_plugin/src/detail-view.tsx +++ b/dev_plugin/src/detail-view.tsx @@ -63,6 +63,10 @@ export default function DetailView(): ReactElement { console.error("DetailView error") + const buf = new Uint8Array(100); + Deno.stdin.read(buf) + .then(value => console.log(`read from stdin: ${value}`)); + useEffect(() => { return () => { console.log("DetailView useEffect destructor called") diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index 3abfafe..5120fd4 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -326,7 +326,13 @@ pub async fn start_js_runtime( StdioPipe::file(out_log_file) } else { - StdioPipe::inherit() + #[cfg(not(windows))] + let stdout = StdioPipe::file(File::options().write(true).open("/dev/null")?); + + #[cfg(windows)] + let stdout = StdioPipe::file(File::options().write(true).open("nul")?); + + stdout }; let stderr = if let Some(stderr_file) = init.stderr_file { @@ -336,9 +342,21 @@ pub async fn start_js_runtime( StdioPipe::file(err_log_file) } else { - StdioPipe::inherit() + #[cfg(not(windows))] + let stderr = StdioPipe::file(File::options().write(true).open("/dev/null")?); + + #[cfg(windows)] + let stderr = StdioPipe::file(File::options().write(true).open("nul")?); + + stderr }; + #[cfg(not(windows))] + let stdin = StdioPipe::file(File::options().read(true).open("/dev/null")?); + + #[cfg(windows)] + let stdin = StdioPipe::file(File::options().read(true).open("nul")?); + std::fs::create_dir_all(&init.plugin_cache_dir) .context("Unable to create plugin cache directory")?; @@ -429,7 +447,7 @@ pub async fn start_js_runtime( should_break_on_first_statement: false, origin_storage_dir: Some(PathBuf::from(init.local_storage_dir)), stdio: Stdio { - stdin: StdioPipe::inherit(), + stdin, stdout, stderr, }, From 373909cc68408eca6834d0df646e503cd0106bbe Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 1 Feb 2025 19:10:38 +0100 Subject: [PATCH 329/540] Add panic hook to log backtrace when plugin runtime panics for easier debugging --- rust/common/src/dirs.rs | 14 +++- rust/common/src/lib.rs | 2 + rust/management_client/src/views/plugins.rs | 4 +- rust/server/src/lib.rs | 74 ++++++++++++++++++--- rust/server/src/plugins/js.rs | 5 +- rust/server/src/plugins/mod.rs | 3 +- 6 files changed, 85 insertions(+), 17 deletions(-) diff --git a/rust/common/src/dirs.rs b/rust/common/src/dirs.rs index f11cee3..b49a152 100644 --- a/rust/common/src/dirs.rs +++ b/rust/common/src/dirs.rs @@ -84,8 +84,20 @@ impl Dirs { cache_dir } + pub fn logs_dir(&self) -> PathBuf { + self.state_dir().join("logs") + } + + pub fn server_crash_log_file(&self) -> PathBuf { + self.logs_dir().join("crash.txt") + } + + pub fn plugin_crash_log_file(&self, plugin_uuid: &str) -> PathBuf { + self.logs_dir().join(&plugin_uuid).join("crash.txt") + } + pub fn plugin_log_files(&self, plugin_uuid: &str) -> (PathBuf, PathBuf) { - let plugin_dir = self.state_dir().join("logs").join(&plugin_uuid); + let plugin_dir = self.logs_dir().join(&plugin_uuid); let out_log_file = plugin_dir.join("stdout.txt"); let err_log_file = plugin_dir.join("stderr.txt"); diff --git a/rust/common/src/lib.rs b/rust/common/src/lib.rs index 0220604..a0379af 100644 --- a/rust/common/src/lib.rs +++ b/rust/common/src/lib.rs @@ -6,6 +6,8 @@ pub mod scenario_convert; pub mod scenario_model; pub mod dirs; +pub const SETTINGS_ENV: &'static str = "__GAUNTLET_INTERNAL_SETTINGS__"; + #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type")] pub enum SettingsEnvData { diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index ba17703..f6d01f8 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -6,7 +6,7 @@ use iced::{padding, Alignment, Length, Padding, Task}; use iced::widget::{button, column, container, row, scrollable, text, text_input, value, vertical_rule}; use iced::widget::text::Shaping; use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; -use gauntlet_common::{settings_env_data_from_string, SettingsEnvData}; +use gauntlet_common::{settings_env_data_from_string, SettingsEnvData, SETTINGS_ENV}; use gauntlet_common::model::{EntrypointId, PluginId, PluginPreferenceUserData, SettingsPlugin}; use gauntlet_common::rpc::backend_api::{BackendApi, BackendApiError}; @@ -53,8 +53,6 @@ pub struct ManagementAppPluginsState { selected_item: SelectedItem, } -const SETTINGS_ENV: &'static str = "GAUNTLET_INTERNAL_SETTINGS"; - impl ManagementAppPluginsState { pub fn new(backend_api: Option) -> Self { let settings_env_data = std::env::var(SETTINGS_ENV) diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index 49a6b48..ce43a13 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -1,27 +1,36 @@ -use std::rc::Rc; -use std::sync::Arc; -use vergen_pretty::vergen_pretty_env; +use crate::plugins::ApplicationManager; +use crate::rpc::BackendServerImpl; +use crate::search::SearchIndex; use gauntlet_client::{open_window, start_client}; +use gauntlet_common::dirs::Dirs; use gauntlet_common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; use gauntlet_common::rpc::backend_api::BackendApi; use gauntlet_common::rpc::backend_server::start_backend_server; use gauntlet_common::{settings_env_data_from_string, settings_env_data_to_string, SettingsEnvData}; use gauntlet_plugin_runtime::run_plugin_runtime; use gauntlet_utils::channel::{channel, RequestReceiver, RequestSender}; -use crate::plugins::ApplicationManager; -use crate::rpc::BackendServerImpl; -use crate::search::SearchIndex; +use std::backtrace::Backtrace; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use vergen_pretty::vergen_pretty_env; pub mod rpc; pub(in crate) mod search; pub(in crate) mod plugins; pub(in crate) mod model; -const SETTINGS_ENV: &'static str = "GAUNTLET_INTERNAL_SETTINGS"; -const PLUGIN_RUNTIME_ENV: &'static str = "GAUNTLET_INTERNAL_PLUGIN_RUNTIME"; +const PLUGIN_CONNECT_ENV: &'static str = "__GAUNTLET_INTERNAL_PLUGIN_CONNECT__"; +const PLUGIN_UUID_ENV: &'static str = "__GAUNTLET_INTERNAL_PLUGIN_UUID__"; pub fn start(minimized: bool) { - if let Ok(socket_name) = std::env::var(PLUGIN_RUNTIME_ENV) { + #[cfg(not(feature = "release"))] + register_panic_hook(std::env::var(PLUGIN_UUID_ENV).ok()); + + if let Ok(socket_name) = std::env::var(PLUGIN_CONNECT_ENV) { run_plugin_runtime(socket_name); return; @@ -260,3 +269,50 @@ async fn handle_request(application_manager: Arc, request_da Ok(response_data) } + + +#[cfg(not(feature = "release"))] +fn register_panic_hook(plugin_runtime: Option) { + unsafe { + std::env::set_var("RUST_BACKTRACE", "1"); + }; + + let dirs = Dirs::new(); + + let crash_file = match plugin_runtime { + None => dirs.server_crash_log_file(), + Some(plugin_uuid) => dirs.plugin_crash_log_file(&plugin_uuid), + }; + + let _ = std::fs::remove_file(&crash_file); + + std::panic::set_hook(Box::new(move |panic_info| { + let payload = panic_info.payload(); + + let payload = if let Some(&s) = payload.downcast_ref::<&'static str>() { + s + } else if let Some(s) = payload.downcast_ref::() { + s.as_str() + } else { + "Box" + }; + + let location = panic_info.location().map(|l| l.to_string()); + let backtrace = Backtrace::capture(); + + let crash_file = File::options() + .create(true) + .append(true) + .open(&crash_file); + + if let Ok(mut crash_file) = crash_file { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .map(|duration| duration.as_millis().to_string()) + .unwrap_or("Unknown".to_string()); + + let _ = crash_file.write_all(format!("Panic on {}\nPayload: {}\nLocation: {:?}\nBacktrace: {}\n", now, payload, location, backtrace).as_bytes()); + } + })); +} \ No newline at end of file diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 90eeb61..c1a536d 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -35,7 +35,7 @@ use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepositor use crate::plugins::icon_cache::IconCache; use crate::plugins::run_status::RunStatusGuard; use crate::search::{SearchIndex, SearchIndexItem, SearchIndexItemAction, SearchIndexItemActionActionType}; -use crate::{PLUGIN_RUNTIME_ENV, SETTINGS_ENV}; +use crate::{PLUGIN_CONNECT_ENV, PLUGIN_UUID_ENV}; use crate::plugins::image_gatherer::ImageGatherer; pub struct PluginRuntimeData { @@ -264,7 +264,8 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run #[cfg(not(feature = "scenario_runner"))] let mut runtime_process = std::process::Command::new(current_exe) - .env(PLUGIN_RUNTIME_ENV, name_str) + .env(PLUGIN_CONNECT_ENV, name_str) + .env(PLUGIN_UUID_ENV, plugin_uuid.clone()) .spawn() .context("start plugin runtime process")?; diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 3912751..3264292 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -9,7 +9,7 @@ use tokio::runtime::Handle; use gauntlet_common::model::{DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, SettingsTheme, UiPropertyValue, UiRequestData, UiResponseData, UiSetupData, UiWidgetId, WindowPositionMode}; use gauntlet_common::rpc::frontend_api::FrontendApi; -use gauntlet_common::{settings_env_data_to_string, SettingsEnvData}; +use gauntlet_common::{settings_env_data_to_string, SettingsEnvData, SETTINGS_ENV}; use gauntlet_utils::channel::RequestSender; use gauntlet_common::dirs::Dirs; use gauntlet_plugin_runtime::{JsPluginCode, JsPluginPermissions, JsPluginPermissionsExec, JsPluginPermissionsFileSystem, JsPluginPermissionsMainSearchBar}; @@ -23,7 +23,6 @@ use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; use crate::plugins::settings::Settings; use crate::search::SearchIndex; -use crate::SETTINGS_ENV; pub mod js; mod data_db_repository; From c3c0ac3cfecc4c3eb52ce9a3e88625f002e9e0b2 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 1 Feb 2025 19:56:48 +0100 Subject: [PATCH 330/540] Fix build --- rust/server/src/rpc.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index 599b020..c422631 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -7,7 +7,6 @@ use gauntlet_common::rpc::backend_server::BackendServer; use crate::plugins::ApplicationManager; use crate::search::SearchIndex; -use crate::SETTINGS_ENV; pub struct BackendServerImpl { pub application_manager: Arc, From f9eeeb6ffd3d8bb1c9fe637e3922f6ce0edfa0bb Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 1 Feb 2025 21:08:08 +0100 Subject: [PATCH 331/540] Update winit fork --- Cargo.lock | 95 +++++++++++++----------------------------------------- 1 file changed, 22 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d017c76..c5d87f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1154,20 +1154,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "calloop" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" -dependencies = [ - "bitflags 2.6.0", - "log", - "polling", - "rustix", - "slab", - "thiserror 1.0.69", -] - [[package]] name = "calloop" version = "0.13.0" @@ -1195,18 +1181,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "calloop-wayland-source" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" -dependencies = [ - "calloop 0.12.4", - "rustix", - "wayland-backend", - "wayland-client", -] - [[package]] name = "calloop-wayland-source" version = "0.3.0" @@ -4133,7 +4107,7 @@ dependencies = [ "regex", "resvg", "serde", - "smithay-client-toolkit 0.19.2", + "smithay-client-toolkit", "tokio", "tokio-util", "tracing", @@ -5162,7 +5136,7 @@ dependencies = [ [[package]] name = "iced" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" dependencies = [ "iced_core", "iced_futures", @@ -5190,7 +5164,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" dependencies = [ "bitflags 2.6.0", "bytes", @@ -5217,7 +5191,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" dependencies = [ "futures", "iced_core", @@ -5231,7 +5205,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5283,7 +5257,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -5295,7 +5269,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" dependencies = [ "bytes", "iced_core", @@ -5316,7 +5290,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" dependencies = [ "bytemuck", "cosmic-text", @@ -5331,7 +5305,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5350,7 +5324,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" dependencies = [ "iced_renderer", "iced_runtime", @@ -5364,7 +5338,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#a703123a16ab3da006e1b5e0c22aa8b86fcacd84" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" dependencies = [ "iced_futures", "iced_graphics", @@ -9009,14 +8983,14 @@ dependencies = [ [[package]] name = "sctk-adwaita" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7555fcb4f753d095d734fdefebb0ad8c98478a21db500492d87c55913d3b0086" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" dependencies = [ "ab_glyph", "log", "memmap2 0.9.5", - "smithay-client-toolkit 0.18.1", + "smithay-client-toolkit", "tiny-skia", ] @@ -9442,31 +9416,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "smithay-client-toolkit" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" -dependencies = [ - "bitflags 2.6.0", - "calloop 0.12.4", - "calloop-wayland-source 0.2.0", - "cursor-icon", - "libc", - "log", - "memmap2 0.9.5", - "rustix", - "thiserror 1.0.69", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols 0.31.2", - "wayland-protocols-wlr 0.2.0", - "wayland-scanner", - "xkeysym", -] - [[package]] name = "smithay-client-toolkit" version = "0.19.2" @@ -9502,7 +9451,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" dependencies = [ "libc", - "smithay-client-toolkit 0.19.2", + "smithay-client-toolkit", "wayland-backend", ] @@ -11810,14 +11759,14 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.2.0" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" +checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" dependencies = [ "bitflags 2.6.0", "wayland-backend", "wayland-client", - "wayland-protocols 0.31.2", + "wayland-protocols 0.32.5", "wayland-scanner", ] @@ -12506,7 +12455,7 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" version = "0.30.99" -source = "git+https://github.com/project-gauntlet/winit.git?rev=316726c86d75a751d40677e6d081cc93e917118c#316726c86d75a751d40677e6d081cc93e917118c" +source = "git+https://github.com/project-gauntlet/winit.git?rev=49690da86351375d2e2ebe8b891cdb90a019b996#49690da86351375d2e2ebe8b891cdb90a019b996" dependencies = [ "ahash 0.8.11", "android-activity", @@ -12514,7 +12463,7 @@ dependencies = [ "bitflags 2.6.0", "block2", "bytemuck", - "calloop 0.12.4", + "calloop 0.13.0", "cfg_aliases 0.2.1", "concurrent-queue", "core-foundation 0.9.4", @@ -12536,7 +12485,7 @@ dependencies = [ "redox_syscall 0.4.1", "rustix", "sctk-adwaita", - "smithay-client-toolkit 0.18.1", + "smithay-client-toolkit", "smol_str", "tracing", "unicode-segmentation", @@ -12544,7 +12493,7 @@ dependencies = [ "wasm-bindgen-futures", "wayland-backend", "wayland-client", - "wayland-protocols 0.31.2", + "wayland-protocols 0.32.5", "wayland-protocols-plasma", "web-sys", "web-time", From d06d691a1acb11b233baa84167f71213c68c2bac Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 1 Feb 2025 21:35:15 +0100 Subject: [PATCH 332/540] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5e5e1b..80876c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Fix no plugins starting on Windows in release mode +- Fix all global shortcut registrations failing if one shortcut registration failed +- Fix error when registering shortcut erroring whole settings window instead of adding an icon + ## [14] - 2025-01-19 - Fixed mouse actions like scrolling or clicking not working on macOS From 478c72f69c118c1d2e718afe41d63e80fd36018b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:48:01 +0100 Subject: [PATCH 333/540] Use macOS theme on all platforms by default, instead of legacy theme --- rust/server/src/plugins/settings.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/rust/server/src/plugins/settings.rs b/rust/server/src/plugins/settings.rs index 51020fc..df1f68e 100644 --- a/rust/server/src/plugins/settings.rs +++ b/rust/server/src/plugins/settings.rs @@ -185,15 +185,10 @@ impl Settings { } fn autodetect_theme(&self) -> UiTheme { - match OS { - "macos" => { - match dark_light::detect() { - Mode::Dark => self.themes.macos_dark_theme.clone(), - Mode::Light => self.themes.macos_light_theme.clone(), - Mode::Default => self.themes.macos_dark_theme.clone() - } - } - _ => self.themes.legacy_theme.clone() + match dark_light::detect() { + Mode::Dark => self.themes.macos_dark_theme.clone(), + Mode::Light => self.themes.macos_light_theme.clone(), + Mode::Default => self.themes.macos_dark_theme.clone() } } } From 2d083074b1206feec53845b8617551cd61af3628 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 2 Feb 2025 17:10:54 +0100 Subject: [PATCH 334/540] Run cargo fmt. Add github workflow to validate --- .github/workflows/format.yaml | 12 + .github/workflows/release.yaml | 2 - rust/cli/src/lib.rs | 12 +- rust/client/build.rs | 57 +- rust/client/src/global_shortcut.rs | 11 +- rust/client/src/lib.rs | 24 +- rust/client/src/model.rs | 8 +- rust/client/src/ui/client_context.rs | 69 +- .../src/ui/custom_widgets/loading_bar.rs | 8 +- rust/client/src/ui/grid_navigation.rs | 475 +++-- rust/client/src/ui/hud/mod.rs | 43 +- rust/client/src/ui/mod.rs | 1736 ++++++++++------- rust/client/src/ui/scroll_handle.rs | 25 +- rust/client/src/ui/search_list.rs | 104 +- rust/client/src/ui/state/main_view.rs | 5 +- rust/client/src/ui/state/mod.rs | 171 +- rust/client/src/ui/state/plugin_view.rs | 5 +- rust/client/src/ui/sys_tray.rs | 46 +- rust/client/src/ui/theme/button.rs | 207 +- rust/client/src/ui/theme/checkbox.rs | 25 +- rust/client/src/ui/theme/container.rs | 203 +- rust/client/src/ui/theme/date_picker.rs | 13 +- rust/client/src/ui/theme/grid.rs | 13 +- rust/client/src/ui/theme/image.rs | 12 +- rust/client/src/ui/theme/loading_bar.rs | 6 +- rust/client/src/ui/theme/mod.rs | 131 +- rust/client/src/ui/theme/pick_list.rs | 18 +- rust/client/src/ui/theme/row.rs | 52 +- rust/client/src/ui/theme/rule.rs | 7 +- rust/client/src/ui/theme/scrollable.rs | 26 +- rust/client/src/ui/theme/space.rs | 12 +- rust/client/src/ui/theme/text.rs | 162 +- rust/client/src/ui/theme/text_input.rs | 43 +- rust/client/src/ui/theme/tooltip.rs | 15 +- rust/client/src/ui/widget.rs | 1615 ++++++++------- rust/client/src/ui/widget_container.rs | 78 +- rust/common/build.rs | 376 +++- rust/common/src/dirs.rs | 17 +- rust/common/src/lib.rs | 14 +- rust/common/src/model.rs | 156 +- rust/common/src/rpc/backend_api.rs | 309 ++- rust/common/src/rpc/backend_server.rs | 350 ++-- rust/common/src/rpc/frontend_api.rs | 92 +- rust/common/src/rpc/grpc.rs | 1 - rust/common/src/rpc/grpc_convert.rs | 477 +++-- rust/common/src/rpc/mod.rs | 2 +- rust/common/src/scenario_model.rs | 31 +- rust/common_ui/src/lib.rs | 101 +- rust/component_model/src/lib.rs | 1211 +++++++----- rust/component_model/src/main.rs | 8 +- rust/management_client/src/components/mod.rs | 2 +- .../src/components/shortcut_selector.rs | 92 +- rust/management_client/src/lib.rs | 6 +- rust/management_client/src/theme.rs | 16 +- rust/management_client/src/theme/button.rs | 29 +- rust/management_client/src/theme/checkbox.rs | 13 +- rust/management_client/src/theme/container.rs | 15 +- .../src/theme/number_input.rs | 14 +- rust/management_client/src/theme/pick_list.rs | 13 +- rust/management_client/src/theme/rule.rs | 3 +- .../management_client/src/theme/scrollable.rs | 29 +- .../src/theme/shortcut_selector.rs | 10 +- rust/management_client/src/theme/table.rs | 9 +- rust/management_client/src/theme/text.rs | 12 +- .../management_client/src/theme/text_input.rs | 47 +- rust/management_client/src/ui.rs | 438 ++--- rust/management_client/src/views/general.rs | 216 +- rust/management_client/src/views/mod.rs | 2 +- rust/management_client/src/views/plugins.rs | 398 ++-- .../src/views/plugins/preferences.rs | 390 ++-- .../src/views/plugins/table.rs | 230 ++- rust/plugin_runtime/src/api.rs | 155 +- rust/plugin_runtime/src/assets.rs | 24 +- rust/plugin_runtime/src/clipboard.rs | 34 +- rust/plugin_runtime/src/component_model.rs | 18 +- rust/plugin_runtime/src/deno.rs | 280 ++- .../src/entrypoint_generators.rs | 12 +- rust/plugin_runtime/src/environment.rs | 24 +- rust/plugin_runtime/src/events.rs | 46 +- rust/plugin_runtime/src/lib.rs | 198 +- rust/plugin_runtime/src/logs.rs | 62 +- rust/plugin_runtime/src/model.rs | 58 +- rust/plugin_runtime/src/permissions.rs | 108 +- rust/plugin_runtime/src/plugin_data.rs | 8 +- .../src/plugins/applications.rs | 60 +- .../src/plugins/applications/linux/mod.rs | 96 +- .../applications/linux/wayland/cosmic.rs | 103 +- .../plugins/applications/linux/wayland/mod.rs | 116 +- .../plugins/applications/linux/wayland/wlr.rs | 85 +- .../src/plugins/applications/linux/x11.rs | 385 ++-- .../src/plugins/applications/macos.rs | 244 ++- .../src/plugins/applications/windows.rs | 56 +- rust/plugin_runtime/src/plugins/numbat.rs | 24 +- rust/plugin_runtime/src/plugins/settings.rs | 2 +- rust/plugin_runtime/src/preferences.rs | 49 +- rust/plugin_runtime/src/search.rs | 21 +- rust/plugin_runtime/src/ui.rs | 240 ++- rust/scenario_runner/src/frontend_mock.rs | 107 +- rust/scenario_runner/src/lib.rs | 10 +- rust/scenario_runner/src/model.rs | 7 +- rust/server/build.rs | 4 +- rust/server/src/lib.rs | 177 +- rust/server/src/model.rs | 392 +++- rust/server/src/plugins/clipboard.rs | 51 +- rust/server/src/plugins/config_reader.rs | 38 +- rust/server/src/plugins/data_db_repository.rs | 412 ++-- rust/server/src/plugins/download_status.rs | 15 +- rust/server/src/plugins/frecency.rs | 12 +- rust/server/src/plugins/icon_cache.rs | 6 +- rust/server/src/plugins/image_gatherer.rs | 31 +- rust/server/src/plugins/js.rs | 715 ++++--- rust/server/src/plugins/loader.rs | 629 ++++-- rust/server/src/plugins/mod.rs | 413 ++-- rust/server/src/plugins/run_status.rs | 11 +- rust/server/src/plugins/runtime.rs | 1 + rust/server/src/plugins/settings.rs | 86 +- rust/server/src/plugins/theme.rs | 84 +- rust/server/src/rpc.rs | 112 +- rust/server/src/search.rs | 197 +- rust/utils/src/channel.rs | 24 +- rust/utils/src/lib.rs | 2 +- rustfmt.toml | 9 + 122 files changed, 10076 insertions(+), 6557 deletions(-) create mode 100644 .github/workflows/format.yaml create mode 100644 rustfmt.toml diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml new file mode 100644 index 0000000..ed2bfbc --- /dev/null +++ b/.github/workflows/format.yaml @@ -0,0 +1,12 @@ +name: Format +on: [push, pull_request] +jobs: + all: + runs-on: ubuntu-latest + steps: + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + - uses: actions/checkout@v4 + - name: Check format + run: cargo +nightly fmt --all -- --check --verbose \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8cb295b..a75c4f9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -30,8 +30,6 @@ jobs: github-release-id: ${{ steps.init-step.outputs.github-release-id }} steps: - uses: actions/checkout@v4 - with: - submodules: true - uses: actions/setup-node@v4 with: node-version: 22 diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs index 7290531..c46bd28 100644 --- a/rust/cli/src/lib.rs +++ b/rust/cli/src/lib.rs @@ -1,4 +1,5 @@ -use anyhow::{anyhow, Context}; +use anyhow::anyhow; +use anyhow::Context; use clap::Parser; use gauntlet_client::open_window; use gauntlet_management_client::start_management_client; @@ -52,8 +53,7 @@ pub fn init() { #[cfg(target_os = "macos")] fn setup_auto_launch_macos() -> anyhow::Result<()> { - let app_path = std::env::current_exe() - .context("Unable to get current_exe from env")?; + let app_path = std::env::current_exe().context("Unable to get current_exe from env")?; // expect Gauntlet.app in path according to macos app bundle structure let app_path_fn = || { @@ -66,13 +66,11 @@ fn setup_auto_launch_macos() -> anyhow::Result<()> { } }; - let app_path = app_path_fn() - .ok_or(anyhow!("Unexpected executable path: {:?}", &app_path))?; + let app_path = app_path_fn().ok_or(anyhow!("Unexpected executable path: {:?}", &app_path))?; setup_auto_launch(app_path) } - #[cfg(target_os = "windows")] fn setup_auto_launch_windows() -> anyhow::Result<()> { let app_path = std::env::current_exe() @@ -95,4 +93,4 @@ fn setup_auto_launch(app_path: String) -> anyhow::Result<()> { .and_then(|auto| auto.enable())?; Ok(()) -} \ No newline at end of file +} diff --git a/rust/client/build.rs b/rust/client/build.rs index dee3262..7acf5a9 100644 --- a/rust/client/build.rs +++ b/rust/client/build.rs @@ -3,9 +3,13 @@ use std::fs::File; use std::io::Write; use std::path::Path; -use convert_case::{Case, Casing}; - -use gauntlet_component_model::{create_component_model, Component, ComponentName, Property, PropertyType}; +use convert_case::Case; +use convert_case::Casing; +use gauntlet_component_model::create_component_model; +use gauntlet_component_model::Component; +use gauntlet_component_model::ComponentName; +use gauntlet_component_model::Property; +use gauntlet_component_model::PropertyType; fn main() -> anyhow::Result<()> { let out_dir = env::var("OUT_DIR")?; @@ -20,10 +24,14 @@ fn main() -> anyhow::Result<()> { Component::Standard { name, props, .. } => { for prop in props { let PropertyType::Function { arguments } = &prop.property_type else { - continue + continue; }; - output.push_str(&format!("fn create_{}_{}_event(\n", name.to_string().to_case(Case::Snake), prop.name.to_case(Case::Snake))); + output.push_str(&format!( + "fn create_{}_{}_event(\n", + name.to_string().to_case(Case::Snake), + prop.name.to_case(Case::Snake) + )); output.push_str(" widget_id: UiWidgetId,\n"); for arg in arguments { @@ -34,7 +42,7 @@ fn main() -> anyhow::Result<()> { output.push_str(" crate::model::UiViewEvent::View {\n"); output.push_str(" widget_id,\n"); output.push_str(&format!(" event_name: \"{}\".to_owned(),\n", prop.name)); - output.push_str(" event_arguments: vec![\n",); + output.push_str(" event_arguments: vec![\n"); for arg in arguments { match arg.property_type { @@ -42,21 +50,30 @@ fn main() -> anyhow::Result<()> { if arg.optional { output.push_str(&format!(" {}.map(|{}| gauntlet_common::model::UiPropertyValue::String({})).unwrap_or_else(|| gauntlet_common::model::UiPropertyValue::Undefined),\n", arg.name, arg.name, arg.name)); } else { - output.push_str(&format!(" gauntlet_common::model::UiPropertyValue::String({}),\n", arg.name)); + output.push_str(&format!( + " gauntlet_common::model::UiPropertyValue::String({}),\n", + arg.name + )); } } PropertyType::Number => { if arg.optional { output.push_str(&format!(" {}.map(|{}| gauntlet_common::model::UiPropertyValue::Number({})).unwrap_or_else(|| gauntlet_common::model::UiPropertyValue::Undefined),\n", arg.name, arg.name, arg.name)); } else { - output.push_str(&format!(" gauntlet_common::model::UiPropertyValue::Number({}),\n", arg.name)); + output.push_str(&format!( + " gauntlet_common::model::UiPropertyValue::Number({}),\n", + arg.name + )); } } PropertyType::Boolean => { if arg.optional { output.push_str(&format!(" {}.map(|{}| gauntlet_common::model::UiPropertyValue::Bool({})).unwrap_or_else(|| gauntlet_common::model::UiPropertyValue::Undefined),\n", arg.name, arg.name, arg.name)); } else { - output.push_str(&format!(" gauntlet_common::model::UiPropertyValue::Bool({}),\n", arg.name)); + output.push_str(&format!( + " gauntlet_common::model::UiPropertyValue::Bool({}),\n", + arg.name + )); } } _ => { @@ -87,8 +104,18 @@ fn generate_file>(path: P, text: &str) -> std::io::Result<()> { fn generate_type(property: &Property, name: &ComponentName) -> String { match property.optional { - true => generate_optional_type(&property.property_type, format!("{}{}", name, &property.name.to_case(Case::Pascal))), - false => generate_required_type(&property.property_type, Some(format!("{}{}", name, &property.name.to_case(Case::Pascal)))) + true => { + generate_optional_type( + &property.property_type, + format!("{}{}", name, &property.name.to_case(Case::Pascal)), + ) + } + false => { + generate_required_type( + &property.property_type, + Some(format!("{}{}", name, &property.name.to_case(Case::Pascal))), + ) + } } } @@ -107,9 +134,9 @@ fn generate_required_type(property_type: &PropertyType, union_name: Option { match union_name { None => panic!("should not be used"), - Some(union_name) => union_name + Some(union_name) => union_name, } - }, - PropertyType::Array { item } => format!("Vec<{}>", generate_required_type(item, union_name)) + } + PropertyType::Array { item } => format!("Vec<{}>", generate_required_type(item, union_name)), } -} \ No newline at end of file +} diff --git a/rust/client/src/global_shortcut.rs b/rust/client/src/global_shortcut.rs index bcc0693..02e9707 100644 --- a/rust/client/src/global_shortcut.rs +++ b/rust/client/src/global_shortcut.rs @@ -1,8 +1,12 @@ -use global_hotkey::hotkey::{Code, HotKey, Modifiers}; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::PhysicalShortcut; +use global_hotkey::hotkey::Code; +use global_hotkey::hotkey::HotKey; +use global_hotkey::hotkey::Modifiers; use iced::futures::channel::mpsc::Sender; use iced::futures::SinkExt; use tokio::runtime::Handle; -use gauntlet_common::model::{PhysicalKey, PhysicalShortcut}; + use crate::ui::AppMsg; pub fn register_listener(msg_sender: Sender) { @@ -22,7 +26,6 @@ pub fn register_listener(msg_sender: Sender) { } pub fn convert_physical_shortcut_to_hotkey(shortcut: PhysicalShortcut) -> HotKey { - let modifiers: Modifiers = { let mut modifiers = Modifiers::empty(); @@ -232,4 +235,4 @@ pub fn convert_physical_shortcut_to_hotkey(shortcut: PhysicalShortcut) -> HotKey }; HotKey::new(Some(modifiers), code) -} \ No newline at end of file +} diff --git a/rust/client/src/lib.rs b/rust/client/src/lib.rs index 08a740e..279b22e 100644 --- a/rust/client/src/lib.rs +++ b/rust/client/src/lib.rs @@ -1,12 +1,17 @@ use gauntlet_common::dirs::Dirs; -use gauntlet_common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; +use gauntlet_common::model::BackendRequestData; +use gauntlet_common::model::BackendResponseData; +use gauntlet_common::model::UiRequestData; +use gauntlet_common::model::UiResponseData; use gauntlet_common::rpc::backend_api::BackendApi; -use gauntlet_utils::channel::{RequestReceiver, RequestSender}; +use gauntlet_utils::channel::RequestReceiver; +use gauntlet_utils::channel::RequestSender; + use crate::ui::GauntletComplexTheme; -pub(in crate) mod ui; -pub(in crate) mod model; pub mod global_shortcut; +pub(crate) mod model; +pub(crate) mod ui; pub fn start_client( minimized: bool, @@ -28,9 +33,7 @@ pub fn open_window() { Ok(mut backend_api) => { tracing::info!("Server is already running, opening window..."); - backend_api.show_window() - .await - .expect("Unknown error") + backend_api.show_window().await.expect("Unknown error") } Err(_) => { tracing::error!("Unable to connect to server. Please check if you have Gauntlet running on your PC") @@ -48,15 +51,10 @@ pub fn open_settings_window() { let result = BackendApi::new().await; match result { - Ok(mut backend_api) => { - backend_api.show_settings_window() - .await - .expect("Unknown error") - } + Ok(mut backend_api) => backend_api.show_settings_window().await.expect("Unknown error"), Err(_) => { tracing::error!("Unable to connect to server. Please check if you have Gauntlet running on your PC") } } }) } - diff --git a/rust/client/src/model.rs b/rust/client/src/model.rs index 981e5e5..1b013d4 100644 --- a/rust/client/src/model.rs +++ b/rust/client/src/model.rs @@ -1,4 +1,6 @@ -use gauntlet_common::model::{UiPropertyValue, UiWidgetId}; +use gauntlet_common::model::UiPropertyValue; +use gauntlet_common::model::UiWidgetId; + use crate::ui::AppMsg; #[derive(Debug, Clone)] @@ -9,9 +11,9 @@ pub enum UiViewEvent { event_arguments: Vec, }, Open { - href: String + href: String, }, AppEvent { - event: AppMsg + event: AppMsg, }, } diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index 3609536..75660b8 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -1,12 +1,20 @@ -use crate::model::UiViewEvent; -use crate::ui::widget::{ActionPanel, ComponentWidgetEvent}; -use crate::ui::widget_container::PluginWidgetContainer; -use crate::ui::AppMsg; -use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiWidgetId}; -use iced::Task; use std::collections::HashMap; use std::sync::Arc; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::RootWidget; +use gauntlet_common::model::UiRenderLocation; +use gauntlet_common::model::UiWidgetId; +use iced::Task; + +use crate::model::UiViewEvent; +use crate::ui::widget::ActionPanel; +use crate::ui::widget::ComponentWidgetEvent; +use crate::ui::widget_container::PluginWidgetContainer; +use crate::ui::AppMsg; + pub struct ClientContext { inline_views: Vec<(PluginId, PluginWidgetContainer)>, // Vec to have stable ordering inline_view_shortcuts: HashMap>, @@ -27,8 +35,7 @@ impl ClientContext { } pub fn get_first_inline_view_container(&self) -> Option<&PluginWidgetContainer> { - self.inline_views.first() - .map(|(_, container)| container) + self.inline_views.first().map(|(_, container)| container) } pub fn get_first_inline_view_action_panel(&self) -> Option { @@ -43,7 +50,8 @@ impl ClientContext { } pub fn get_inline_view_container(&self, plugin_id: &PluginId) -> &PluginWidgetContainer { - self.inline_views.iter() + self.inline_views + .iter() .find(|(id, _)| id == plugin_id) .map(|(_, container)| container) .expect("there should always be container for plugin at this point") @@ -54,7 +62,8 @@ impl ClientContext { let (_, container) = &mut self.inline_views[index]; container } else { - self.inline_views.push((plugin_id.clone(), PluginWidgetContainer::new())); + self.inline_views + .push((plugin_id.clone(), PluginWidgetContainer::new())); let (_, container) = self.inline_views.last_mut().expect("getting just pushed item"); container } @@ -84,11 +93,29 @@ impl ClientContext { plugin_id: &PluginId, plugin_name: &str, entrypoint_id: &EntrypointId, - entrypoint_name: &str + entrypoint_name: &str, ) -> AppMsg { match render_location { - UiRenderLocation::InlineView => self.get_mut_inline_view_container(plugin_id).replace_view(container, images, plugin_id, plugin_name, entrypoint_id, entrypoint_name), - UiRenderLocation::View => self.get_mut_view_container().replace_view(container, images, plugin_id, plugin_name, entrypoint_id, entrypoint_name) + UiRenderLocation::InlineView => { + self.get_mut_inline_view_container(plugin_id).replace_view( + container, + images, + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + ) + } + UiRenderLocation::View => { + self.get_mut_view_container().replace_view( + container, + images, + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + ) + } } } @@ -96,7 +123,7 @@ impl ClientContext { self.inline_view_shortcuts = shortcuts; } - pub fn clear_all_inline_views(&mut self) { + pub fn clear_all_inline_views(&mut self) { self.inline_views.clear() } @@ -106,10 +133,18 @@ impl ClientContext { } } - pub fn handle_event(&mut self, render_location: UiRenderLocation, plugin_id: &PluginId, event: ComponentWidgetEvent) -> Option { + pub fn handle_event( + &mut self, + render_location: UiRenderLocation, + plugin_id: &PluginId, + event: ComponentWidgetEvent, + ) -> Option { match render_location { - UiRenderLocation::InlineView => self.get_mut_inline_view_container(&plugin_id).handle_event(plugin_id.clone(), event), - UiRenderLocation::View => self.get_mut_view_container().handle_event(plugin_id.clone(), event) + UiRenderLocation::InlineView => { + self.get_mut_inline_view_container(&plugin_id) + .handle_event(plugin_id.clone(), event) + } + UiRenderLocation::View => self.get_mut_view_container().handle_event(plugin_id.clone(), event), } } diff --git a/rust/client/src/ui/custom_widgets/loading_bar.rs b/rust/client/src/ui/custom_widgets/loading_bar.rs index 1afeb0d..43f04b9 100644 --- a/rust/client/src/ui/custom_widgets/loading_bar.rs +++ b/rust/client/src/ui/custom_widgets/loading_bar.rs @@ -1,3 +1,6 @@ +use std::time::Duration; +use std::time::Instant; + use iced::advanced::layout::Limits; use iced::advanced::layout::Node; use iced::advanced::renderer; @@ -10,15 +13,15 @@ use iced::advanced::Shell; use iced::advanced::Widget; use iced::event::Status; use iced::mouse::Cursor; +use iced::window; use iced::Border; +use iced::Color; use iced::Element; use iced::Event; use iced::Length; use iced::Rectangle; use iced::Shadow; use iced::Size; -use iced::{window, Color}; -use std::time::{Duration, Instant}; pub struct LoadingBar<'a, Theme> where @@ -227,7 +230,6 @@ pub trait Catalog { fn style(&self, class: &Self::Class<'_>) -> Style; } - impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where Renderer: renderer::Renderer + 'a, diff --git a/rust/client/src/ui/grid_navigation.rs b/rust/client/src/ui/grid_navigation.rs index 547e2ab..12ccf4b 100644 --- a/rust/client/src/ui/grid_navigation.rs +++ b/rust/client/src/ui/grid_navigation.rs @@ -1,11 +1,12 @@ -use std::ops::{Div, Rem}; +use std::ops::Div; +use std::ops::Rem; #[derive(Debug)] pub struct GridSectionData { pub start_index: usize, pub start_row_index: usize, pub amount_in_section: usize, - pub width: usize + pub width: usize, } struct GridRowData { @@ -27,20 +28,33 @@ pub struct GridItemOffset { pub offset: usize, } -fn grid_row_data(mut amount_per_section_total: Vec, current_index: usize) -> (Option, GridCurrentRowData, Option) { +fn grid_row_data( + mut amount_per_section_total: Vec, + current_index: usize, +) -> (Option, GridCurrentRowData, Option) { let mut previous_section_index: Option = None; let mut current_section_index: usize = 0; - for (index, GridSectionData { start_index, amount_in_section, .. }) in amount_per_section_total.iter().enumerate() { + for ( + index, + GridSectionData { + start_index, + amount_in_section, + .. + }, + ) in amount_per_section_total.iter().enumerate() + { if start_index + amount_in_section >= (current_index + 1) { current_section_index = index; - break + break; } previous_section_index = Some(index); } let prev_section = previous_section_index.and_then(|index| amount_per_section_total.get(index)); - let current_section = amount_per_section_total.get(current_section_index).expect("guarantied to exist"); + let current_section = amount_per_section_total + .get(current_section_index) + .expect("guarantied to exist"); let next_section = amount_per_section_total.get(current_section_index + 1); let item_index_in_section = current_index - current_section.start_index; @@ -57,14 +71,14 @@ fn grid_row_data(mut amount_per_section_total: Vec, current_ind row_index, column_index: usize::rem(item_index_in_section, current_section.width), amount_in_row: if reminder == 0 { current_section.width } else { reminder }, - max_amount_in_row: current_section.width + max_amount_in_row: current_section.width, } } else { GridCurrentRowData { row_index, column_index: usize::rem(item_index_in_section, current_section.width), amount_in_row: current_section.width, - max_amount_in_row: current_section.width + max_amount_in_row: current_section.width, } } }; @@ -78,7 +92,7 @@ fn grid_row_data(mut amount_per_section_total: Vec, current_ind Some(GridRowData { row_index: current_row.row_index - 1, amount_in_row: if reminder == 0 { prev_section.width } else { reminder }, - max_amount_in_row: prev_section.width + max_amount_in_row: prev_section.width, }) } } @@ -86,7 +100,7 @@ fn grid_row_data(mut amount_per_section_total: Vec, current_ind Some(GridRowData { row_index: current_row.row_index - 1, amount_in_row: current_section.width, - max_amount_in_row: current_section.width + max_amount_in_row: current_section.width, }) } }; @@ -100,7 +114,7 @@ fn grid_row_data(mut amount_per_section_total: Vec, current_ind Some(GridRowData { row_index: current_row.row_index + 1, amount_in_row: if reminder == 0 { next_section.width } else { reminder }, - max_amount_in_row: next_section.width + max_amount_in_row: next_section.width, }) } } @@ -111,13 +125,13 @@ fn grid_row_data(mut amount_per_section_total: Vec, current_ind Some(GridRowData { row_index: current_row.row_index + 1, amount_in_row: if reminder == 0 { current_section.width } else { reminder }, - max_amount_in_row: current_section.width + max_amount_in_row: current_section.width, }) } else { Some(GridRowData { row_index: current_row.row_index + 1, amount_in_row: current_section.width, - max_amount_in_row: current_section.width + max_amount_in_row: current_section.width, }) } } @@ -154,7 +168,10 @@ pub fn grid_up_offset(current_index: usize, amount_per_section_total: Vec) -> Option { +pub fn grid_down_offset( + current_index: usize, + amount_per_section_total: Vec, +) -> Option { let (_prev_row, current_row, next_row) = grid_row_data(amount_per_section_total, current_index); match next_row { @@ -175,7 +192,6 @@ pub fn grid_down_offset(current_index: usize, amount_per_section_total: Vec()) - .sum(); + let amount = section.iter().map(|row| row.iter().sum::()).sum(); let width = section[0].len(); @@ -215,291 +229,322 @@ mod tests { #[test] fn grid_down_last_row() { - let sections_amount_width = prepare_sections( - vec![ - vec![ - //fr V - vec![1, 1, 0], - ], - ] - ); + let sections_amount_width = prepare_sections(vec![vec![ + //fr V + vec![1, 1, 0], + ]]); assert_eq!(grid_down_offset(1, sections_amount_width), None) } #[test] fn grid_down_inside_1() { - let sections_amount_width = prepare_sections( - vec![ - vec![ - //fr V - vec![1, 1, 1], - //to V - vec![1, 1, 0], - ], - ] - ); + let sections_amount_width = prepare_sections(vec![vec![ + //fr V + vec![1, 1, 1], + //to V + vec![1, 1, 0], + ]]); - assert_eq!(grid_down_offset(0, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 3 } )) + assert_eq!( + grid_down_offset(0, sections_amount_width), + Some(GridItemOffset { + row_index: 1, + offset: 3 + }) + ) } #[test] fn grid_down_inside_2() { - let sections_amount_width = prepare_sections( - vec![ - vec![ - //fr V - vec![1, 1, 1], - //to V - vec![1, 1, 0], - ], - ] - ); + let sections_amount_width = prepare_sections(vec![vec![ + //fr V + vec![1, 1, 1], + //to V + vec![1, 1, 0], + ]]); - assert_eq!(grid_down_offset(1, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 3 } )) + assert_eq!( + grid_down_offset(1, sections_amount_width), + Some(GridItemOffset { + row_index: 1, + offset: 3 + }) + ) } #[test] fn grid_down_inside_3() { - let sections_amount_width = prepare_sections( - vec![ - vec![ - //fr V - vec![1, 1, 1], - //to V - vec![1, 1, 0], - ], - ] - ); + let sections_amount_width = prepare_sections(vec![vec![ + //fr V + vec![1, 1, 1], + //to V + vec![1, 1, 0], + ]]); - assert_eq!(grid_down_offset(2, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 2 } )) + assert_eq!( + grid_down_offset(2, sections_amount_width), + Some(GridItemOffset { + row_index: 1, + offset: 2 + }) + ) } #[test] fn grid_down_inside_4() { - let sections_amount_width = prepare_sections( - vec![ - vec![ - //fr V - vec![1, 1, 1], - //to V - vec![1, 0, 0], - ], - ] - ); + let sections_amount_width = prepare_sections(vec![vec![ + //fr V + vec![1, 1, 1], + //to V + vec![1, 0, 0], + ]]); - assert_eq!(grid_down_offset(2, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 1 } )) + assert_eq!( + grid_down_offset(2, sections_amount_width), + Some(GridItemOffset { + row_index: 1, + offset: 1 + }) + ) } #[test] fn grid_down_inside_5() { - let sections_amount_width = prepare_sections( - vec![ - vec![ - vec![1, 1, 1], - //fr V - vec![1, 1, 1], - //to V - vec![1, 1, 0], - ], - ] - ); + let sections_amount_width = prepare_sections(vec![vec![ + vec![1, 1, 1], + //fr V + vec![1, 1, 1], + //to V + vec![1, 1, 0], + ]]); - assert_eq!(grid_down_offset(4, sections_amount_width), Some(GridItemOffset { row_index: 2, offset: 3 } )) + assert_eq!( + grid_down_offset(4, sections_amount_width), + Some(GridItemOffset { + row_index: 2, + offset: 3 + }) + ) } #[test] fn grid_down_outside_1() { - let sections_amount_width = prepare_sections( + let sections_amount_width = prepare_sections(vec![ vec![ - vec![ - //fr V - vec![1, 1, 1], - ], - vec![ - //to V - vec![1, 1, 1], - ], - ] - ); + //fr V + vec![1, 1, 1], + ], + vec![ + //to V + vec![1, 1, 1], + ], + ]); - assert_eq!(grid_down_offset(1, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 3 })) + assert_eq!( + grid_down_offset(1, sections_amount_width), + Some(GridItemOffset { + row_index: 1, + offset: 3 + }) + ) } #[test] fn grid_down_outside_2() { - let sections_amount_width = prepare_sections( + let sections_amount_width = prepare_sections(vec![ vec![ - vec![ - //fr V - vec![1, 1, 0], - ], - vec![ - //to V - vec![1, 1, 1], - ], - ] - ); + //fr V + vec![1, 1, 0], + ], + vec![ + //to V + vec![1, 1, 1], + ], + ]); - assert_eq!(grid_down_offset(1, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 2 })) + assert_eq!( + grid_down_offset(1, sections_amount_width), + Some(GridItemOffset { + row_index: 1, + offset: 2 + }) + ) } #[test] fn grid_down_outside_3() { - let sections_amount_width = prepare_sections( + let sections_amount_width = prepare_sections(vec![ vec![ - vec![ - //fr V - vec![1, 1, 1], - ], - vec![ - //to V - vec![1, 1, 0], - ], - vec![ - vec![1, 1, 1], - ], - ] - ); + //fr V + vec![1, 1, 1], + ], + vec![ + //to V + vec![1, 1, 0], + ], + vec![vec![1, 1, 1]], + ]); - assert_eq!(grid_down_offset(2, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 2 })) + assert_eq!( + grid_down_offset(2, sections_amount_width), + Some(GridItemOffset { + row_index: 1, + offset: 2 + }) + ) } #[test] fn grid_up_first_row() { - let sections_amount_width = prepare_sections( - vec![ - vec![ - //fr V - vec![1, 1, 0], - ], - ] - ); + let sections_amount_width = prepare_sections(vec![vec![ + //fr V + vec![1, 1, 0], + ]]); assert_eq!(grid_up_offset(1, sections_amount_width), None) } #[test] fn grid_up_inside_1() { - let sections_amount_width = prepare_sections( - vec![ - vec![ - //to V - vec![1, 1, 1], - //fr V - vec![1, 1, 0], - ], - ] - ); + let sections_amount_width = prepare_sections(vec![vec![ + //to V + vec![1, 1, 1], + //fr V + vec![1, 1, 0], + ]]); - assert_eq!(grid_up_offset(4, sections_amount_width), Some(GridItemOffset { row_index: 0, offset: 3 })) + assert_eq!( + grid_up_offset(4, sections_amount_width), + Some(GridItemOffset { + row_index: 0, + offset: 3 + }) + ) } #[test] fn grid_up_inside_2() { - let sections_amount_width = prepare_sections( - vec![ - vec![ - //to V - vec![1, 1, 1], - //fr V - vec![1, 1, 0], - ], - ] - ); + let sections_amount_width = prepare_sections(vec![vec![ + //to V + vec![1, 1, 1], + //fr V + vec![1, 1, 0], + ]]); - assert_eq!(grid_up_offset(3, sections_amount_width), Some(GridItemOffset { row_index: 0, offset: 3 })) + assert_eq!( + grid_up_offset(3, sections_amount_width), + Some(GridItemOffset { + row_index: 0, + offset: 3 + }) + ) } #[test] fn grid_up_inside_3() { - let sections_amount_width = prepare_sections( - vec![ - vec![ - //to V - vec![1, 1, 1], - //fr V - vec![1, 1, 1], - ], - ] - ); + let sections_amount_width = prepare_sections(vec![vec![ + //to V + vec![1, 1, 1], + //fr V + vec![1, 1, 1], + ]]); - assert_eq!(grid_up_offset(5, sections_amount_width), Some(GridItemOffset { row_index: 0, offset: 3 })) + assert_eq!( + grid_up_offset(5, sections_amount_width), + Some(GridItemOffset { + row_index: 0, + offset: 3 + }) + ) } #[test] fn grid_up_outside_1() { - let sections_amount_width = prepare_sections( + let sections_amount_width = prepare_sections(vec![ vec![ - vec![ - //to V - vec![1, 1, 1], - ], - vec![ - //fr V - vec![1, 1, 1], - ], - ] - ); + //to V + vec![1, 1, 1], + ], + vec![ + //fr V + vec![1, 1, 1], + ], + ]); - assert_eq!(grid_up_offset(4, sections_amount_width), Some(GridItemOffset { row_index: 0, offset: 3 })) + assert_eq!( + grid_up_offset(4, sections_amount_width), + Some(GridItemOffset { + row_index: 0, + offset: 3 + }) + ) } #[test] fn grid_up_outside_2() { - let sections_amount_width = prepare_sections( + let sections_amount_width = prepare_sections(vec![ vec![ - vec![ - //to V - vec![1, 1, 0], - ], - vec![ - //fr V - vec![1, 1, 1], - ], - ] - ); + //to V + vec![1, 1, 0], + ], + vec![ + //fr V + vec![1, 1, 1], + ], + ]); - assert_eq!(grid_up_offset(4, sections_amount_width), Some(GridItemOffset { row_index: 0, offset: 3 })) + assert_eq!( + grid_up_offset(4, sections_amount_width), + Some(GridItemOffset { + row_index: 0, + offset: 3 + }) + ) } - #[test] fn grid_up_outside_3() { - let sections_amount_width = prepare_sections( + let sections_amount_width = prepare_sections(vec![ vec![ - vec![ - //to V - vec![1, 1, 0], - ], - vec![ - //fr V - vec![1, 0, 0], - ], - ] - ); + //to V + vec![1, 1, 0], + ], + vec![ + //fr V + vec![1, 0, 0], + ], + ]); - assert_eq!(grid_up_offset(2, sections_amount_width), Some(GridItemOffset { row_index: 0, offset: 2 })) + assert_eq!( + grid_up_offset(2, sections_amount_width), + Some(GridItemOffset { + row_index: 0, + offset: 2 + }) + ) } #[test] fn grid_up_outside_4() { - let sections_amount_width = prepare_sections( + let sections_amount_width = prepare_sections(vec![ + vec![vec![1, 1, 1]], vec![ - vec![ - vec![1, 1, 1], - ], - vec![ - //to V - vec![1, 1, 0], - ], - vec![ - //fr V - vec![1, 1, 1], - ], - ] - ); + //to V + vec![1, 1, 0], + ], + vec![ + //fr V + vec![1, 1, 1], + ], + ]); - assert_eq!(grid_up_offset(7, sections_amount_width), Some(GridItemOffset { row_index: 1, offset: 3 })) + assert_eq!( + grid_up_offset(7, sections_amount_width), + Some(GridItemOffset { + row_index: 1, + offset: 3 + }) + ) } -} \ No newline at end of file +} diff --git a/rust/client/src/ui/hud/mod.rs b/rust/client/src/ui/hud/mod.rs index 520b225..c5a48f5 100644 --- a/rust/client/src/ui/hud/mod.rs +++ b/rust/client/src/ui/hud/mod.rs @@ -1,16 +1,20 @@ -use iced::window::{Level, Position, Settings}; -use iced::{window, Point, Size, Task}; use std::convert; use std::time::Duration; + +use iced::window; +use iced::window::Level; +use iced::window::Position; +use iced::window::Settings; +use iced::Point; +use iced::Size; +use iced::Task; + use crate::ui::AppMsg; const HUD_WINDOW_WIDTH: f32 = 400.0; const HUD_WINDOW_HEIGHT: f32 = 40.0; -pub fn show_hud_window( - #[cfg(target_os = "linux")] - wayland: bool, -) -> Task { +pub fn show_hud_window(#[cfg(target_os = "linux")] wayland: bool) -> Task { #[cfg(target_os = "linux")] if wayland { open_wayland() @@ -26,7 +30,10 @@ fn open_non_wayland() -> Task { let settings = Settings { size: Size::new(HUD_WINDOW_WIDTH, HUD_WINDOW_HEIGHT), position: Position::SpecificWith(|window, screen| { - Point::new((screen.width - window.width) / 2.0, (screen.height - window.height) / 1.25) + Point::new( + (screen.width - window.width) / 2.0, + (screen.height - window.height) / 1.25, + ) }), resizable: false, decorations: false, @@ -54,9 +61,14 @@ fn open_wayland() -> Task { let settings = layer_shell_settings(); Task::batch([ - Task::done(AppMsg::LayerShell(crate::ui::layer_shell::LayerShellAppMsg::NewLayerShell { id, settings })), - sleep_for_2_seconds(id) - .then(|id| Task::done(AppMsg::LayerShell(crate::ui::layer_shell::LayerShellAppMsg::RemoveWindow(id)))) + Task::done(AppMsg::LayerShell( + crate::ui::layer_shell::LayerShellAppMsg::NewLayerShell { id, settings }, + )), + sleep_for_2_seconds(id).then(|id| { + Task::done(AppMsg::LayerShell( + crate::ui::layer_shell::LayerShellAppMsg::RemoveWindow(id), + )) + }), ]) } @@ -75,9 +87,12 @@ fn layer_shell_settings() -> iced_layershell::reexport::NewLayerShellSettings { } fn sleep_for_2_seconds(id: window::Id) -> Task { - Task::perform(async move { - tokio::time::sleep(Duration::from_secs(2)).await; + Task::perform( + async move { + tokio::time::sleep(Duration::from_secs(2)).await; - id - }, convert::identity) + id + }, + convert::identity, + ) } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index e99d984..f4af368 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -1,63 +1,139 @@ +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::Mutex as StdMutex; +use std::sync::Mutex; +use std::sync::RwLock as StdRwLock; + use anyhow::anyhow; +use client_context::ClientContext; +use gauntlet_common::model::BackendRequestData; +use gauntlet_common::model::BackendResponseData; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::KeyboardEventOrigin; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::RootWidget; +use gauntlet_common::model::RootWidgetMembers; +use gauntlet_common::model::SearchResult; +use gauntlet_common::model::SearchResultEntrypointAction; +use gauntlet_common::model::SearchResultEntrypointActionType; +use gauntlet_common::model::SearchResultEntrypointType; +use gauntlet_common::model::UiRenderLocation; +use gauntlet_common::model::UiRequestData; +use gauntlet_common::model::UiResponseData; +use gauntlet_common::model::UiSetupData; +use gauntlet_common::model::UiTheme; +use gauntlet_common::model::UiWidgetId; +use gauntlet_common::model::WindowPositionMode; +use gauntlet_common::rpc::backend_api::BackendApi; +use gauntlet_common::rpc::backend_api::BackendForFrontendApi; +use gauntlet_common::rpc::backend_api::BackendForFrontendApiError; +use gauntlet_common::scenario_convert::ui_render_location_from_scenario; +use gauntlet_common::scenario_model::ScenarioFrontendEvent; +use gauntlet_common::scenario_model::ScenarioUiRenderLocation; +use gauntlet_common_ui::physical_key_model; +use gauntlet_utils::channel::RequestReceiver; +use gauntlet_utils::channel::RequestSender; +use gauntlet_utils::channel::Responder; use global_hotkey::hotkey::HotKey; use global_hotkey::GlobalHotKeyManager; use iced::advanced::graphics::core::SmolStr; use iced::advanced::layout::Limits; +use iced::alignment::Horizontal; +use iced::alignment::Vertical; +use iced::event; +use iced::executor; +use iced::font; +use iced::futures; use iced::futures::channel::mpsc::Sender; use iced::futures::SinkExt; -use iced::keyboard::key::{Named, Physical}; -use iced::keyboard::{Key, Modifiers}; -use iced::widget::scrollable::{scroll_to, AbsoluteOffset}; +use iced::keyboard; +use iced::keyboard::key::Named; +use iced::keyboard::key::Physical; +use iced::keyboard::Key; +use iced::keyboard::Modifiers; +use iced::stream; +use iced::widget::button; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_rule; +use iced::widget::horizontal_space; +use iced::widget::row; +use iced::widget::scrollable; +use iced::widget::scrollable::scroll_to; +use iced::widget::scrollable::AbsoluteOffset; +use iced::widget::text; use iced::widget::text::Shaping; +use iced::widget::text_input; use iced::widget::text_input::focus; -use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, Space}; -use iced::window::{Level, Mode, Position, Screenshot}; -use iced::{event, executor, font, futures, keyboard, stream, window, Alignment, Event, Font, Length, Padding, Pixels, Point, Renderer, Settings, Size, Subscription, Task}; -use std::collections::HashMap; -use std::fs; -use std::path::{Path, PathBuf}; -use std::rc::Rc; -use std::sync::{Arc, Mutex as StdMutex, Mutex, RwLock as StdRwLock}; -use iced::alignment::{Horizontal, Vertical}; +use iced::widget::Space; +use iced::window; +use iced::window::Level; +use iced::window::Mode; +use iced::window::Position; +use iced::window::Screenshot; +use iced::Alignment; +use iced::Event; +use iced::Font; +use iced::Length; +use iced::Padding; +use iced::Pixels; +use iced::Point; +use iced::Renderer; +use iced::Settings; +use iced::Size; +use iced::Subscription; +use iced::Task; use iced_fonts::BOOTSTRAP_FONT_BYTES; use serde::Deserialize; -use tokio::sync::{Mutex as TokioMutex, RwLock as TokioRwLock}; - -use client_context::ClientContext; -use gauntlet_common::model::{BackendRequestData, BackendResponseData, EntrypointId, UiTheme, KeyboardEventOrigin, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchResult, SearchResultEntrypointAction, SearchResultEntrypointActionType, SearchResultEntrypointType, UiRenderLocation, UiRequestData, UiResponseData, UiSetupData, UiWidgetId, WindowPositionMode}; -use gauntlet_common::rpc::backend_api::{BackendApi, BackendForFrontendApi, BackendForFrontendApiError}; -use gauntlet_common::scenario_convert::{ui_render_location_from_scenario}; -use gauntlet_common::scenario_model::{ScenarioFrontendEvent, ScenarioUiRenderLocation}; -use gauntlet_common_ui::physical_key_model; -use gauntlet_utils::channel::{RequestReceiver, RequestSender, Responder}; +use tokio::sync::Mutex as TokioMutex; +use tokio::sync::RwLock as TokioRwLock; use crate::model::UiViewEvent; use crate::ui::search_list::search_list; -use crate::ui::theme::container::{ContainerStyle, ContainerStyleInner}; +use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::container::ContainerStyleInner; use crate::ui::theme::text_input::TextInputStyle; -use crate::ui::theme::{Element, ThemableWidget}; -use crate::ui::widget::{render_root, ActionPanel, ActionPanelItem, ComponentWidgetEvent}; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::render_root; +use crate::ui::widget::ActionPanel; +use crate::ui::widget::ActionPanelItem; +use crate::ui::widget::ComponentWidgetEvent; -mod search_list; -mod widget; -mod theme; mod client_context; -mod widget_container; +mod custom_widgets; +mod grid_navigation; +mod hud; +mod scroll_handle; +mod search_list; +mod state; #[cfg(any(target_os = "macos", target_os = "windows"))] mod sys_tray; -mod custom_widgets; -mod scroll_handle; -mod state; -mod hud; -mod grid_navigation; +mod theme; +mod widget; +mod widget_container; -use crate::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; +pub use theme::GauntletComplexTheme; + +use crate::global_shortcut::convert_physical_shortcut_to_hotkey; +use crate::global_shortcut::register_listener; use crate::ui::custom_widgets::loading_bar::LoadingBar; use crate::ui::hud::show_hud_window; use crate::ui::scroll_handle::ScrollHandle; -use crate::ui::state::{ErrorViewData, Focus, GlobalState, LoadingBarState, MainViewState, PluginViewData, PluginViewState}; +use crate::ui::state::ErrorViewData; +use crate::ui::state::Focus; +use crate::ui::state::GlobalState; +use crate::ui::state::LoadingBarState; +use crate::ui::state::MainViewState; +use crate::ui::state::PluginViewData; +use crate::ui::state::PluginViewState; use crate::ui::widget_container::PluginWidgetContainer; -pub use theme::GauntletComplexTheme; pub struct AppModel { // logic @@ -84,7 +160,7 @@ pub struct AppModel { global_state: GlobalState, search_results: Vec, loading_bar_state: HashMap<(PluginId, EntrypointId), ()>, - hud_display: Option + hud_display: Option, } #[cfg(target_os = "linux")] @@ -107,7 +183,7 @@ pub enum AppMsg { plugin_name: String, entrypoint_id: EntrypointId, entrypoint_name: String, - action_index: usize + action_index: usize, }, RunCommand { plugin_id: PluginId, @@ -116,7 +192,7 @@ pub enum AppMsg { RunGeneratedEntrypoint { plugin_id: PluginId, entrypoint_id: EntrypointId, - action_index: usize + action_index: usize, }, RunSearchItemAction(SearchResult, usize), RunPluginAction { @@ -155,68 +231,92 @@ pub enum AppMsg { ShowWindow, HideWindow, ToggleActionPanel { - keyboard: bool + keyboard: bool, }, ShowPreferenceRequiredView { plugin_id: PluginId, entrypoint_id: EntrypointId, plugin_preferences_required: bool, - entrypoint_preferences_required: bool + entrypoint_preferences_required: bool, }, OpenSettingsPreferences { plugin_id: PluginId, entrypoint_id: Option, }, OnOpenView { - action_shortcuts: HashMap + action_shortcuts: HashMap, }, ShowPluginErrorView { plugin_id: PluginId, entrypoint_id: EntrypointId, - render_location: UiRenderLocation + render_location: UiRenderLocation, }, Screenshot { - save_path: String + save_path: String, }, ScreenshotDone { save_path: String, - screenshot: Screenshot + screenshot: Screenshot, }, ShowBackendError(BackendForFrontendApiError), ClosePluginView(PluginId), OpenPluginView(PluginId, EntrypointId), InlineViewShortcuts { - shortcuts: HashMap> + shortcuts: HashMap>, }, ShowHud { - display: String + display: String, }, OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus, - OnPrimaryActionMainViewNoPanel { search_result: SearchResult }, - OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result: SearchResult }, + OnPrimaryActionMainViewNoPanel { + search_result: SearchResult, + }, + OnSecondaryActionMainViewNoPanelKeyboardWithFocus { + search_result: SearchResult, + }, OnSecondaryActionMainViewNoPanelKeyboardWithoutFocus, - OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { search_result: SearchResult, widget_id: UiWidgetId }, - OnAnyActionMainViewInlineViewPanelKeyboardWithFocus { widget_id: UiWidgetId }, - OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id: UiWidgetId, id: Option }, - OnAnyActionPluginViewAnyPanelKeyboardWithFocus { widget_id: UiWidgetId, id: Option }, - OnAnyActionPluginViewAnyPanel { widget_id: UiWidgetId, id: Option }, - OnAnyActionMainViewSearchResultPanelMouse { widget_id: UiWidgetId }, - OnPrimaryActionMainViewActionPanelMouse { widget_id: UiWidgetId }, + OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { + search_result: SearchResult, + widget_id: UiWidgetId, + }, + OnAnyActionMainViewInlineViewPanelKeyboardWithFocus { + widget_id: UiWidgetId, + }, + OnAnyActionPluginViewNoPanelKeyboardWithFocus { + widget_id: UiWidgetId, + id: Option, + }, + OnAnyActionPluginViewAnyPanelKeyboardWithFocus { + widget_id: UiWidgetId, + id: Option, + }, + OnAnyActionPluginViewAnyPanel { + widget_id: UiWidgetId, + id: Option, + }, + OnAnyActionMainViewSearchResultPanelMouse { + widget_id: UiWidgetId, + }, + OnPrimaryActionMainViewActionPanelMouse { + widget_id: UiWidgetId, + }, ResetMainViewState, - OnAnyActionMainViewNoPanelKeyboardAtIndex { index: usize }, + OnAnyActionMainViewNoPanelKeyboardAtIndex { + index: usize, + }, SetGlobalShortcut { shortcut: Option, - responder: Arc>>> + responder: Arc>>>, }, UpdateLoadingBar { plugin_id: PluginId, entrypoint_id: EntrypointId, - show: bool + show: bool, }, PendingPluginViewLoadingBar, ShowPluginViewLoadingBar, FocusPluginViewSearchBar { - widget_id: UiWidgetId + widget_id: UiWidgetId, }, #[cfg(target_os = "linux")] LayerShell(layer_shell::LayerShellAppMsg), @@ -224,10 +324,10 @@ pub enum AppMsg { plugin_id: PluginId, }, SetTheme { - theme: UiTheme + theme: UiTheme, }, SetWindowPositionMode { - mode: WindowPositionMode + mode: WindowPositionMode, }, } @@ -237,7 +337,7 @@ impl TryInto for AppMsg fn try_into(self) -> Result { match self { Self::LayerShell(msg) => msg.try_into().map_err(|msg| Self::LayerShell(msg)), - _ => Err(self) + _ => Err(self), } } } @@ -282,7 +382,6 @@ fn window_settings(visible: bool, position: Position) -> window::Settings { } } - #[cfg(target_os = "linux")] fn layer_shell_settings() -> iced_layershell::reexport::NewLayerShellSettings { iced_layershell::reexport::NewLayerShellSettings { @@ -303,7 +402,7 @@ fn open_main_window_non_wayland(minimized: bool, window_position_file: &PathBuf) if let Some((x, y)) = data.split_once(":") { match (x.parse(), y.parse()) { (Ok(x), Ok(y)) => Some(Position::Specific(Point::new(x, y))), - _ => None + _ => None, } } else { None @@ -314,21 +413,29 @@ fn open_main_window_non_wayland(minimized: bool, window_position_file: &PathBuf) let (main_window_id, open_task) = window::open(window_settings(!minimized, position)); - (main_window_id, Task::batch([ - open_task.map(|_| AppMsg::Noop), - window::gain_focus(main_window_id), - window::change_level(main_window_id, Level::AlwaysOnTop), - ])) + ( + main_window_id, + Task::batch([ + open_task.map(|_| AppMsg::Noop), + window::gain_focus(main_window_id), + window::change_level(main_window_id, Level::AlwaysOnTop), + ]), + ) } #[cfg(target_os = "linux")] fn open_main_window_wayland(id: window::Id) -> (window::Id, Task) { let settings = layer_shell_settings(); - (id, Task::done(AppMsg::LayerShell(layer_shell::LayerShellAppMsg::NewLayerShell { id, settings }))) + ( + id, + Task::done(AppMsg::LayerShell(layer_shell::LayerShellAppMsg::NewLayerShell { + id, + settings, + })), + ) } - pub fn run( minimized: bool, frontend_receiver: RequestReceiver, @@ -396,8 +503,7 @@ fn run_wayland( } #[cfg(target_os = "linux")] -fn wayland_remove_id_info(_state: &mut AppModel, _id: window::Id) { -} +fn wayland_remove_id_info(_state: &mut AppModel, _id: window::Id) {} fn new( frontend_receiver: RequestReceiver, @@ -407,8 +513,7 @@ fn new( ) -> (AppModel, Task) { let mut backend_api = BackendForFrontendApi::new(backend_sender); - let setup_data = futures::executor::block_on(backend_api.setup_data()) - .expect("Unable to setup frontend"); + let setup_data = futures::executor::block_on(backend_api.setup_data()).expect("Unable to setup frontend"); let theme = GauntletComplexTheme::new(setup_data.theme); @@ -416,20 +521,19 @@ fn new( let current_hotkey = Arc::new(StdMutex::new(None)); - let global_hotkey_manager = GlobalHotKeyManager::new() - .expect("unable to create global hot key manager"); + let global_hotkey_manager = GlobalHotKeyManager::new().expect("unable to create global hot key manager"); let assignment_result = assign_global_shortcut(&global_hotkey_manager, ¤t_hotkey, setup_data.global_shortcut); - futures::executor::block_on(backend_api.setup_response(assignment_result.map_err(|err| format!("{:#}", err)).err())) - .expect("Unable to setup frontend"); + futures::executor::block_on( + backend_api.setup_response(assignment_result.map_err(|err| format!("{:#}", err)).err()), + ) + .expect("Unable to setup frontend"); - let mut tasks = vec![ - font::load(BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded), - ]; + let mut tasks = vec![font::load(BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded)]; #[cfg(target_os = "linux")] - let (main_window_id, open_task) = if wayland { + let (main_window_id, open_task) = if wayland { let id = window::Id::unique(); if minimized { @@ -447,34 +551,39 @@ fn new( tasks.push(open_task); let global_state = if cfg!(feature = "scenario_runner") { - let gen_in = std::env::var("GAUNTLET_SCREENSHOT_GEN_IN") - .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_IN"); + let gen_in = std::env::var("GAUNTLET_SCREENSHOT_GEN_IN").expect("Unable to read GAUNTLET_SCREENSHOT_GEN_IN"); - println!("Reading scenario file at: {}", gen_in); + println!("Reading scenario file at: {}", gen_in); - let gen_in = fs::read_to_string(gen_in) - .expect("Unable to read file at GAUNTLET_SCREENSHOT_GEN_IN"); + let gen_in = fs::read_to_string(gen_in).expect("Unable to read file at GAUNTLET_SCREENSHOT_GEN_IN"); - let gen_out = std::env::var("GAUNTLET_SCREENSHOT_GEN_OUT") - .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_OUT"); + let gen_out = std::env::var("GAUNTLET_SCREENSHOT_GEN_OUT").expect("Unable to read GAUNTLET_SCREENSHOT_GEN_OUT"); - let gen_name = std::env::var("GAUNTLET_SCREENSHOT_GEN_NAME") - .expect("Unable to read GAUNTLET_SCREENSHOT_GEN_NAME"); + let gen_name = + std::env::var("GAUNTLET_SCREENSHOT_GEN_NAME").expect("Unable to read GAUNTLET_SCREENSHOT_GEN_NAME"); - let event: ScenarioFrontendEvent = serde_json::from_str(&gen_in) - .expect("GAUNTLET_SCREENSHOT_GEN_IN is not valid json"); + let event: ScenarioFrontendEvent = + serde_json::from_str(&gen_in).expect("GAUNTLET_SCREENSHOT_GEN_IN is not valid json"); - tasks.push( - Task::perform( - async { - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - }, - move |_| AppMsg::Screenshot { save_path: gen_out.clone() }, - ) - ); + tasks.push(Task::perform( + async { + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + }, + move |_| { + AppMsg::Screenshot { + save_path: gen_out.clone(), + } + }, + )); match event { - ScenarioFrontendEvent::ReplaceView { entrypoint_id, render_location, top_level_view, container, images } => { + ScenarioFrontendEvent::ReplaceView { + entrypoint_id, + render_location, + top_level_view, + container, + images, + } => { let plugin_id = PluginId::from_string("__SCREENSHOT_GEN___"); let entrypoint_id = EntrypointId::from_string(entrypoint_id); @@ -488,26 +597,30 @@ fn new( render_location, top_level_view, container: Arc::new(container), - images + images, }; tasks.push(Task::done(msg)); match render_location { UiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique()), - UiRenderLocation::View => GlobalState::new_plugin( - PluginViewData { + UiRenderLocation::View => { + GlobalState::new_plugin(PluginViewData { top_level_view, plugin_id, plugin_name: "Screenshot Gen".to_string(), entrypoint_id, entrypoint_name: gen_name, action_shortcuts: Default::default(), - }, - ) + }) + } } } - ScenarioFrontendEvent::ShowPreferenceRequiredView { entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { + ScenarioFrontendEvent::ShowPreferenceRequiredView { + entrypoint_id, + plugin_preferences_required, + entrypoint_preferences_required, + } => { let error_view = ErrorViewData::PreferenceRequired { plugin_id: PluginId::from_string("__SCREENSHOT_GEN___"), entrypoint_id: EntrypointId::from_string(entrypoint_id), @@ -517,7 +630,10 @@ fn new( GlobalState::new_error(error_view) } - ScenarioFrontendEvent::ShowPluginErrorView { entrypoint_id, render_location: _ } => { + ScenarioFrontendEvent::ShowPluginErrorView { + entrypoint_id, + render_location: _, + } => { let error_view = ErrorViewData::PluginError { plugin_id: PluginId::from_string("__SCREENSHOT_GEN___"), entrypoint_id: EntrypointId::from_string(entrypoint_id), @@ -572,9 +688,17 @@ fn title(state: &AppModel, window: window::Id) -> String { fn update(state: &mut AppModel, message: AppMsg) -> Task { match message { - AppMsg::OpenView { plugin_id, plugin_name, entrypoint_id, entrypoint_name } => { + AppMsg::OpenView { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + } => { match &mut state.global_state { - GlobalState::MainView { pending_plugin_view_data, .. } => { + GlobalState::MainView { + pending_plugin_view_data, + .. + } => { *pending_plugin_view_data = Some(PluginViewData { top_level_view: true, plugin_id: plugin_id.clone(), @@ -586,20 +710,25 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::batch([ state.open_plugin_view(plugin_id, entrypoint_id), - Task::done(AppMsg::PendingPluginViewLoadingBar) + Task::done(AppMsg::PendingPluginViewLoadingBar), ]) } - GlobalState::ErrorView { .. } => { - Task::none() - } - GlobalState::PluginView { .. } => { - Task::none() - } + GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PluginView { .. } => Task::none(), } } - AppMsg::OpenGeneratedView { plugin_id, plugin_name, entrypoint_id, entrypoint_name, action_index } => { + AppMsg::OpenGeneratedView { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + action_index, + } => { match &mut state.global_state { - GlobalState::MainView { pending_plugin_view_data, .. } => { + GlobalState::MainView { + pending_plugin_view_data, + .. + } => { *pending_plugin_view_data = Some(PluginViewData { top_level_view: true, plugin_id: plugin_id.clone(), @@ -611,44 +740,52 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::batch([ state.run_generated_entrypoint(plugin_id, entrypoint_id, action_index), - Task::done(AppMsg::PendingPluginViewLoadingBar) + Task::done(AppMsg::PendingPluginViewLoadingBar), ]) } - GlobalState::ErrorView { .. } => { - Task::none() - } - GlobalState::PluginView { .. } => { - Task::none() - } + GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PluginView { .. } => Task::none(), } } - AppMsg::RunCommand { plugin_id, entrypoint_id } => { - Task::batch([ - state.hide_window(), - state.run_command(plugin_id, entrypoint_id), - ]) - } - AppMsg::RunGeneratedEntrypoint { plugin_id, entrypoint_id, action_index } => { + AppMsg::RunCommand { + plugin_id, + entrypoint_id, + } => Task::batch([state.hide_window(), state.run_command(plugin_id, entrypoint_id)]), + AppMsg::RunGeneratedEntrypoint { + plugin_id, + entrypoint_id, + action_index, + } => { Task::batch([ state.hide_window(), state.run_generated_entrypoint(plugin_id, entrypoint_id, action_index), ]) } - AppMsg::RunPluginAction { render_location, plugin_id, widget_id, id } => { - let widget_event = ComponentWidgetEvent::RunAction { - widget_id, - id - }; + AppMsg::RunPluginAction { + render_location, + plugin_id, + widget_id, + id, + } => { + let widget_event = ComponentWidgetEvent::RunAction { widget_id, id }; match render_location { UiRenderLocation::InlineView => { Task::batch([ state.hide_window(), - Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + Task::done(AppMsg::WidgetEvent { + widget_event, + plugin_id, + render_location, + }), ]) } UiRenderLocation::View => { - Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, render_location }) + Task::done(AppMsg::WidgetEvent { + widget_event, + plugin_id, + render_location, + }) } } } @@ -663,7 +800,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } else { Task::none() } - }, + } SearchResultEntrypointType::View => { if action_index == 0 { Task::done(AppMsg::OpenView { @@ -675,7 +812,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } else { Task::none() } - }, + } SearchResultEntrypointType::Generated => { let action = &search_result.entrypoint_actions[action_index]; match &action.action_type { @@ -696,7 +833,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { }) } } - }, + } } } AppMsg::PromptChanged(mut new_prompt) => { @@ -704,7 +841,11 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } else { match &mut state.global_state { - GlobalState::MainView { focused_search_result, sub_state, ..} => { + GlobalState::MainView { + focused_search_result, + sub_state, + .. + } => { new_prompt.truncate(100); // search query uses regex so just to be safe truncate the prompt state.prompt = new_prompt.clone(); @@ -722,15 +863,11 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } AppMsg::UpdateSearchResults => { match &state.global_state { - GlobalState::MainView { .. } => { - state.search(state.prompt.clone(), false) - } - _ => Task::none() + GlobalState::MainView { .. } => state.search(state.prompt.clone(), false), + _ => Task::none(), } } - AppMsg::PromptSubmit => { - state.global_state.primary(&state.client_context, &state.search_results) - }, + AppMsg::PromptSubmit => state.global_state.primary(&state.client_context, &state.search_results), AppMsg::SetSearchResults(new_search_results) => { state.search_results = new_search_results; @@ -744,7 +881,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { render_location, top_level_view, container, - images + images, } => { let has_children = container.content.is_some(); @@ -762,17 +899,21 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { top_level_view, has_children, render_location, - }) + }), ]) } AppMsg::HandleRenderPluginUI { top_level_view, has_children, - render_location + render_location, } => { match &mut state.global_state { - GlobalState::MainView { pending_plugin_view_data, focused_search_result, pending_plugin_view_loading_bar, .. } => { - + GlobalState::MainView { + pending_plugin_view_data, + focused_search_result, + pending_plugin_view_loading_bar, + .. + } => { if let LoadingBarState::Pending = pending_plugin_view_loading_bar { *pending_plugin_view_loading_bar = LoadingBarState::Off; } @@ -798,16 +939,13 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { }; if let UiRenderLocation::InlineView = render_location { - Task::batch([ - command, - state.inline_view_shortcuts() - ]) + Task::batch([command, state.inline_view_shortcuts()]) } else { command } } GlobalState::ErrorView { .. } => Task::none(), - GlobalState::PluginView { plugin_view_data, ..} => { + GlobalState::PluginView { plugin_view_data, .. } => { plugin_view_data.top_level_view = top_level_view; Task::none() @@ -816,68 +954,128 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } AppMsg::IcedEvent(window_id, Event::Keyboard(event)) => { if window_id != state.main_window_id { - return Task::none() + return Task::none(); } match event { - keyboard::Event::KeyPressed { key, modifiers, physical_key, text, .. } => { - tracing::debug!("Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", key, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo()); + keyboard::Event::KeyPressed { + key, + modifiers, + physical_key, + text, + .. + } => { + tracing::debug!( + "Key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", + key, + modifiers.shift(), + modifiers.control(), + modifiers.alt(), + modifiers.logo() + ); match key { - Key::Named(Named::ArrowUp) => state.global_state.up(&mut state.client_context, &state.search_results), - Key::Named(Named::ArrowDown) => state.global_state.down(&mut state.client_context, &state.search_results), - Key::Named(Named::ArrowLeft) => state.global_state.left(&mut state.client_context, &state.search_results), - Key::Named(Named::ArrowRight) => state.global_state.right(&mut state.client_context, &state.search_results), + Key::Named(Named::ArrowUp) => { + state.global_state.up(&mut state.client_context, &state.search_results) + } + Key::Named(Named::ArrowDown) => { + state + .global_state + .down(&mut state.client_context, &state.search_results) + } + Key::Named(Named::ArrowLeft) => { + state + .global_state + .left(&mut state.client_context, &state.search_results) + } + Key::Named(Named::ArrowRight) => { + state + .global_state + .right(&mut state.client_context, &state.search_results) + } Key::Named(Named::Escape) => state.global_state.back(&state.client_context), Key::Named(Named::Tab) if !modifiers.shift() => state.global_state.next(&state.client_context), - Key::Named(Named::Tab) if modifiers.shift() => state.global_state.previous(&state.client_context), + Key::Named(Named::Tab) if modifiers.shift() => { + state.global_state.previous(&state.client_context) + } Key::Named(Named::Enter) => { if modifiers.logo() || modifiers.alt() || modifiers.control() { Task::none() // to avoid not wanted "enter" presses } else { if modifiers.shift() { // for main view, also fired in cases where main text field is not focused - state.global_state.secondary(&state.client_context, &state.search_results) + state + .global_state + .secondary(&state.client_context, &state.search_results) } else { state.global_state.primary(&state.client_context, &state.search_results) } } - }, + } Key::Named(Named::Backspace) => { match &mut state.global_state { - GlobalState::MainView { sub_state, search_field_id, .. } => { + GlobalState::MainView { + sub_state, + search_field_id, + .. + } => { match sub_state { - MainViewState::None => AppModel::backspace_prompt(&mut state.prompt, search_field_id.clone()), + MainViewState::None => { + AppModel::backspace_prompt(&mut state.prompt, search_field_id.clone()) + } MainViewState::SearchResultActionPanel { .. } => Task::none(), - MainViewState::InlineViewActionPanel { .. } => Task::none() + MainViewState::InlineViewActionPanel { .. } => Task::none(), } } GlobalState::ErrorView { .. } => Task::none(), GlobalState::PluginView { sub_state, .. } => { match sub_state { - PluginViewState::None => { - state.client_context.backspace_text() - } - PluginViewState::ActionPanel { .. } => Task::none() + PluginViewState::None => state.client_context.backspace_text(), + PluginViewState::ActionPanel { .. } => Task::none(), } } } - }, + } _ => { let Physical::Code(physical_key) = physical_key else { - return Task::none() + return Task::none(); }; match &mut state.global_state { - GlobalState::MainView { sub_state, search_field_id, focused_search_result, .. } => { + GlobalState::MainView { + sub_state, + search_field_id, + focused_search_result, + .. + } => { match sub_state { MainViewState::None => { match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) + Some(PhysicalShortcut { + physical_key: PhysicalKey::KeyK, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + }) => { + Task::perform(async {}, |_| { + AppMsg::ToggleActionPanel { keyboard: true } + }) } - Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - if modifier_shift || modifier_control || modifier_alt || modifier_meta { - if let Some(search_item) = focused_search_result.get(&state.search_results) { + Some(PhysicalShortcut { + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + }) => { + if modifier_shift + || modifier_control + || modifier_alt + || modifier_meta + { + if let Some(search_item) = + focused_search_result.get(&state.search_results) + { if search_item.entrypoint_actions.len() > 0 { state.handle_main_view_keyboard_event( search_item.plugin_id.clone(), @@ -886,7 +1084,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { modifier_shift, modifier_control, modifier_alt, - modifier_meta + modifier_meta, ) } else { Task::none() @@ -897,24 +1095,56 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { modifier_shift, modifier_control, modifier_alt, - modifier_meta + modifier_meta, ) } } else { - AppModel::append_prompt(&mut state.prompt, text, search_field_id.clone(), modifiers) + AppModel::append_prompt( + &mut state.prompt, + text, + search_field_id.clone(), + modifiers, + ) } } - _ => AppModel::append_prompt(&mut state.prompt, text, search_field_id.clone(), modifiers) + _ => { + AppModel::append_prompt( + &mut state.prompt, + text, + search_field_id.clone(), + modifiers, + ) + } } } MainViewState::SearchResultActionPanel { .. } => { match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) + Some(PhysicalShortcut { + physical_key: PhysicalKey::KeyK, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + }) => { + Task::perform(async {}, |_| { + AppMsg::ToggleActionPanel { keyboard: true } + }) } - Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - if modifier_shift || modifier_control || modifier_alt || modifier_meta { - if let Some(search_item) = focused_search_result.get(&state.search_results) { + Some(PhysicalShortcut { + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + }) => { + if modifier_shift + || modifier_control + || modifier_alt + || modifier_meta + { + if let Some(search_item) = + focused_search_result.get(&state.search_results) + { if search_item.entrypoint_actions.len() > 0 { state.handle_main_view_keyboard_event( search_item.plugin_id.clone(), @@ -923,7 +1153,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { modifier_shift, modifier_control, modifier_alt, - modifier_meta + modifier_meta, ) } else { Task::none() @@ -935,28 +1165,46 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } } - _ => Task::none() + _ => Task::none(), } } MainViewState::InlineViewActionPanel { .. } => { match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) + Some(PhysicalShortcut { + physical_key: PhysicalKey::KeyK, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + }) => { + Task::perform(async {}, |_| { + AppMsg::ToggleActionPanel { keyboard: true } + }) } - Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { - if modifier_shift || modifier_control || modifier_alt || modifier_meta { + Some(PhysicalShortcut { + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + }) => { + if modifier_shift + || modifier_control + || modifier_alt + || modifier_meta + { state.handle_inline_plugin_view_keyboard_event( physical_key, modifier_shift, modifier_control, modifier_alt, - modifier_meta + modifier_meta, ) } else { Task::none() } } - _ => Task::none() + _ => Task::none(), } } } @@ -964,12 +1212,28 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { GlobalState::ErrorView { .. } => Task::none(), GlobalState::PluginView { sub_state, .. } => { match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, modifier_control: false, modifier_alt: true, modifier_meta: false }) => { - Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }) - } - Some(PhysicalShortcut { physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta }) => { + Some(PhysicalShortcut { + physical_key: PhysicalKey::KeyK, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + }) => Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }), + Some(PhysicalShortcut { + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + }) => { if modifier_shift || modifier_control || modifier_alt || modifier_meta { - state.handle_plugin_view_keyboard_event(physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + state.handle_plugin_view_keyboard_event( + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + ) } else { match sub_state { PluginViewState::None => { @@ -980,38 +1244,38 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } } - PluginViewState::ActionPanel { .. } => Task::none() + PluginViewState::ActionPanel { .. } => Task::none(), } } } - _ => Task::none() + _ => Task::none(), } } } } } } - _ => Task::none() + _ => Task::none(), } } AppMsg::IcedEvent(window_id, Event::Window(window::Event::Focused)) => { if !state.close_on_unfocus { - return Task::none() + return Task::none(); } if window_id != state.main_window_id { - return Task::none() + return Task::none(); } state.on_focused() } AppMsg::IcedEvent(window_id, Event::Window(window::Event::Unfocused)) => { if !state.close_on_unfocus { - return Task::none() + return Task::none(); } if window_id != state.main_window_id { - return Task::none() + return Task::none(); } if state.wayland { @@ -1022,7 +1286,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } AppMsg::IcedEvent(window_id, Event::Window(window::Event::Moved(point))) => { if window_id != state.main_window_id { - return Task::none() + return Task::none(); } let _ = fs::create_dir_all(state.window_position_file.parent().unwrap()); @@ -1031,11 +1295,19 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } AppMsg::IcedEvent(_, _) => Task::none(), - AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::Noop, .. } => Task::none(), - AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => state.global_state.back(&state.client_context), - AppMsg::WidgetEvent { widget_event, plugin_id, render_location } => { - state.handle_plugin_event(widget_event, plugin_id, render_location) - } + AppMsg::WidgetEvent { + widget_event: ComponentWidgetEvent::Noop, + .. + } => Task::none(), + AppMsg::WidgetEvent { + widget_event: ComponentWidgetEvent::PreviousView, + .. + } => state.global_state.back(&state.client_context), + AppMsg::WidgetEvent { + widget_event, + plugin_id, + render_location, + } => state.handle_plugin_event(widget_event, plugin_id, render_location), AppMsg::Noop => Task::none(), AppMsg::FontLoaded(result) => { result.expect("unable to load font"); @@ -1047,7 +1319,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { plugin_id, entrypoint_id, plugin_preferences_required, - entrypoint_preferences_required + entrypoint_preferences_required, } => { GlobalState::error( &mut state.global_state, @@ -1059,7 +1331,11 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { }, ) } - AppMsg::ShowPluginErrorView { plugin_id, entrypoint_id, .. } => { + AppMsg::ShowPluginErrorView { + plugin_id, + entrypoint_id, + .. + } => { GlobalState::error( &mut state.global_state, ErrorViewData::PluginError { @@ -1073,16 +1349,20 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { &mut state.global_state, match err { BackendForFrontendApiError::TimeoutError => ErrorViewData::BackendTimeout, - BackendForFrontendApiError::Internal { display } => ErrorViewData::UnknownError { display } - } + BackendForFrontendApiError::Internal { display } => ErrorViewData::UnknownError { display }, + }, ) } - AppMsg::OpenSettingsPreferences { plugin_id, entrypoint_id, } => { - state.open_settings_window_preferences(plugin_id, entrypoint_id) - } + AppMsg::OpenSettingsPreferences { + plugin_id, + entrypoint_id, + } => state.open_settings_window_preferences(plugin_id, entrypoint_id), AppMsg::OnOpenView { action_shortcuts } => { match &mut state.global_state { - GlobalState::MainView { pending_plugin_view_data, .. } => { + GlobalState::MainView { + pending_plugin_view_data, + .. + } => { match pending_plugin_view_data { None => {} Some(pending_plugin_view_data) => { @@ -1090,8 +1370,8 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } }; } - GlobalState::ErrorView { .. } => { }, - GlobalState::PluginView { plugin_view_data, ..} => { + GlobalState::ErrorView { .. } => {} + GlobalState::PluginView { plugin_view_data, .. } => { plugin_view_data.action_shortcuts = action_shortcuts; } } @@ -1104,11 +1384,12 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { fs::create_dir_all(Path::new(&save_path).parent().expect("no parent?")) .expect("unable to create scenario out directories"); - window::screenshot(state.main_window_id) - .map(move |screenshot| AppMsg::ScreenshotDone { + window::screenshot(state.main_window_id).map(move |screenshot| { + AppMsg::ScreenshotDone { save_path: save_path.clone(), screenshot, - }) + } + }) } AppMsg::ScreenshotDone { save_path, screenshot } => { println!("Saving screenshot at: {}", save_path); @@ -1118,12 +1399,9 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { tokio::task::spawn_blocking(move || { let save_dir = Path::new(&save_path); - let save_parent_dir = save_dir - .parent() - .expect("save_path has no parent"); + let save_parent_dir = save_dir.parent().expect("save_path has no parent"); - fs::create_dir_all(save_parent_dir) - .expect("unable to create save_parent_dir"); + fs::create_dir_all(save_parent_dir).expect("unable to create save_parent_dir"); image::save_buffer_with_format( &save_path, @@ -1131,17 +1409,23 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { screenshot.size.width, screenshot.size.height, image::ColorType::Rgba8, - image::ImageFormat::Png + image::ImageFormat::Png, ) - }).await - .expect("Unable to save screenshot") + }) + .await + .expect("Unable to save screenshot") }, |_| (), - ).then(|_| iced::exit()) + ) + .then(|_| iced::exit()) } AppMsg::ToggleActionPanel { keyboard } => { match &mut state.global_state { - GlobalState::MainView { sub_state, focused_search_result, .. } => { + GlobalState::MainView { + sub_state, + focused_search_result, + .. + } => { match sub_state { MainViewState::None => { if let Some(search_item) = focused_search_result.get(&state.search_results) { @@ -1162,17 +1446,13 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } } - GlobalState::ErrorView { .. } => { }, + GlobalState::ErrorView { .. } => {} GlobalState::PluginView { sub_state, .. } => { state.client_context.toggle_action_panel(); match sub_state { - PluginViewState::None => { - PluginViewState::action_panel(sub_state, keyboard) - } - PluginViewState::ActionPanel { .. } => { - PluginViewState::initial(sub_state) - } + PluginViewState::None => PluginViewState::action_panel(sub_state, keyboard), + PluginViewState::ActionPanel { .. } => PluginViewState::initial(sub_state), } } } @@ -1191,12 +1471,15 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { AppMsg::OnSecondaryActionMainViewNoPanelKeyboardWithFocus { search_result } => { Task::done(AppMsg::RunSearchItemAction(search_result, 1)) } - AppMsg::OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id } => { + AppMsg::OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { + search_result, + widget_id, + } => { let index = widget_id; Task::batch([ Task::done(AppMsg::RunSearchItemAction(search_result, index)), - Task::done(AppMsg::ResetMainViewState) + Task::done(AppMsg::ResetMainViewState), ]) } AppMsg::OnAnyActionMainViewInlineViewPanelKeyboardWithFocus { widget_id } => { @@ -1210,11 +1493,11 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { render_location: UiRenderLocation::InlineView, plugin_id, widget_id, - id: None - }) + id: None, + }), ]) } - None => Task::none() + None => Task::none(), } } AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id, id } => { @@ -1227,14 +1510,16 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { render_location: UiRenderLocation::View, plugin_id: state.client_context.get_view_plugin_id(), widget_id, - id - }) + id, + }), ]) } AppMsg::OnPrimaryActionMainViewActionPanelMouse { widget_id: _ } => { // widget_id here is always 0 match &state.global_state { - GlobalState::MainView { focused_search_result, .. } => { + GlobalState::MainView { + focused_search_result, .. + } => { if let Some(search_result) = focused_search_result.get(&state.search_results) { let search_result = search_result.clone(); Task::done(AppMsg::OnPrimaryActionMainViewNoPanel { search_result }) @@ -1243,7 +1528,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } GlobalState::PluginView { .. } => Task::none(), - GlobalState::ErrorView { .. } => Task::none() + GlobalState::ErrorView { .. } => Task::none(), } } AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index } => { @@ -1259,10 +1544,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { render_location: UiRenderLocation::InlineView, plugin_id, widget_id, - id: None + id: None, }) } - None => Task::none() + None => Task::none(), } } else { Task::none() @@ -1270,24 +1555,29 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } AppMsg::OnAnyActionMainViewSearchResultPanelMouse { widget_id } => { match &mut state.global_state { - GlobalState::MainView { focused_search_result, sub_state, .. } => { + GlobalState::MainView { + focused_search_result, + sub_state, + .. + } => { match sub_state { MainViewState::None => Task::none(), MainViewState::SearchResultActionPanel { .. } => { if let Some(search_result) = focused_search_result.get(&state.search_results) { let search_result = search_result.clone(); - Task::done(AppMsg::OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id }) + Task::done(AppMsg::OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { + search_result, + widget_id, + }) } else { Task::none() } } - MainViewState::InlineViewActionPanel { .. } => { - Task::none() - } + MainViewState::InlineViewActionPanel { .. } => Task::none(), } } GlobalState::ErrorView { .. } => Task::none(), - GlobalState::PluginView { .. } => Task::none() + GlobalState::PluginView { .. } => Task::none(), } } AppMsg::OnAnyActionPluginViewAnyPanel { widget_id, id } => { @@ -1295,15 +1585,11 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { render_location: UiRenderLocation::View, plugin_id: state.client_context.get_view_plugin_id(), widget_id, - id + id, }) } - AppMsg::OpenPluginView(plugin_id, entrypoint_id) => { - state.open_plugin_view(plugin_id, entrypoint_id) - } - AppMsg::ClosePluginView(plugin_id) => { - state.close_plugin_view(plugin_id) - } + AppMsg::OpenPluginView(plugin_id, entrypoint_id) => state.open_plugin_view(plugin_id, entrypoint_id), + AppMsg::ClosePluginView(plugin_id) => state.close_plugin_view(plugin_id), AppMsg::InlineViewShortcuts { shortcuts } => { state.client_context.set_inline_view_shortcuts(shortcuts); @@ -1332,15 +1618,9 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { tracing::info!("Registering new global shortcut: {:?}", shortcut); let run = || { - let global_hotkey_manager = state.global_hotkey_manager - .read() - .expect("lock is poisoned"); + let global_hotkey_manager = state.global_hotkey_manager.read().expect("lock is poisoned"); - assign_global_shortcut( - &global_hotkey_manager, - &state.current_hotkey, - shortcut - ) + assign_global_shortcut(&global_hotkey_manager, &state.current_hotkey, shortcut) }; // responder is not clone and send, and we need to consume it @@ -1362,7 +1642,11 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } - AppMsg::UpdateLoadingBar { plugin_id, entrypoint_id, show } => { + AppMsg::UpdateLoadingBar { + plugin_id, + entrypoint_id, + show, + } => { if show { state.loading_bar_state.insert((plugin_id, entrypoint_id), ()); } else { @@ -1372,18 +1656,29 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } AppMsg::PendingPluginViewLoadingBar => { - if let GlobalState::MainView { pending_plugin_view_loading_bar, .. } = &mut state.global_state { + if let GlobalState::MainView { + pending_plugin_view_loading_bar, + .. + } = &mut state.global_state + { *pending_plugin_view_loading_bar = LoadingBarState::Pending; } - Task::perform(async move { - tokio::time::sleep(std::time::Duration::from_millis(300)).await; + Task::perform( + async move { + tokio::time::sleep(std::time::Duration::from_millis(300)).await; - AppMsg::ShowPluginViewLoadingBar - }, std::convert::identity) + AppMsg::ShowPluginViewLoadingBar + }, + std::convert::identity, + ) } AppMsg::ShowPluginViewLoadingBar => { - if let GlobalState::MainView { pending_plugin_view_loading_bar, .. } = &mut state.global_state { + if let GlobalState::MainView { + pending_plugin_view_loading_bar, + .. + } = &mut state.global_state + { if let LoadingBarState::Pending = pending_plugin_view_loading_bar { *pending_plugin_view_loading_bar = LoadingBarState::On; } @@ -1391,9 +1686,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } - AppMsg::FocusPluginViewSearchBar { widget_id } => { - state.client_context.focus_search_bar(widget_id) - } + AppMsg::FocusPluginViewSearchBar { widget_id } => state.client_context.focus_search_bar(widget_id), #[cfg(target_os = "linux")] AppMsg::LayerShell(_) => { // handled by library @@ -1430,9 +1723,7 @@ fn view(state: &AppModel, window: window::Id) -> Element<'_, AppMsg> { fn view_hud(state: &AppModel) -> Element<'_, AppMsg> { match &state.hud_display { Some(hud_display) => { - let hud: Element<_> = text(hud_display.to_string()) - .shaping(Shaping::Advanced) - .into(); + let hud: Element<_> = text(hud_display.to_string()).shaping(Shaping::Advanced).into(); let hud = container(hud) .align_x(Horizontal::Center) @@ -1458,8 +1749,7 @@ fn view_hud(state: &AppModel) -> Element<'_, AppMsg> { } None => { // this should never be shown, but in case it does, do not make it fully transparent - container(horizontal_space()) - .themed(ContainerStyle::Hud) + container(horizontal_space()).themed(ContainerStyle::Hud) } } } @@ -1468,56 +1758,59 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { match &state.global_state { GlobalState::ErrorView { error_view } => { match error_view { - ErrorViewData::PreferenceRequired { plugin_id, entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { - + ErrorViewData::PreferenceRequired { + plugin_id, + entrypoint_id, + plugin_preferences_required, + entrypoint_preferences_required, + } => { let (description_text, msg) = match (plugin_preferences_required, entrypoint_preferences_required) { (true, true) => { // TODO do not show "entrypoint" name to user - let description_text = "Before using, plugin and entrypoint preferences need to be specified"; + let description_text = + "Before using, plugin and entrypoint preferences need to be specified"; // note: // we open plugin view and not entrypoint even though both need to be specified - let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: None }; + let msg = AppMsg::OpenSettingsPreferences { + plugin_id: plugin_id.clone(), + entrypoint_id: None, + }; (description_text, msg) } (false, true) => { // TODO do not show "entrypoint" name to user let description_text = "Before using, entrypoint preferences need to be specified"; - let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: Some(entrypoint_id.clone()) }; + let msg = AppMsg::OpenSettingsPreferences { + plugin_id: plugin_id.clone(), + entrypoint_id: Some(entrypoint_id.clone()), + }; (description_text, msg) } (true, false) => { let description_text = "Before using, plugin preferences need to be specified"; - let msg = AppMsg::OpenSettingsPreferences { plugin_id: plugin_id.clone(), entrypoint_id: None }; + let msg = AppMsg::OpenSettingsPreferences { + plugin_id: plugin_id.clone(), + entrypoint_id: None, + }; (description_text, msg) } - (false, false) => unreachable!() + (false, false) => unreachable!(), }; - let description: Element<_> = text(description_text) - .shaping(Shaping::Advanced) - .into(); + let description: Element<_> = text(description_text).shaping(Shaping::Advanced).into(); let description = container(description) .width(Length::Fill) .align_x(Horizontal::Center) .themed(ContainerStyle::PreferenceRequiredViewDescription); - let button_label: Element<_> = text("Open Settings") - .into(); + let button_label: Element<_> = text("Open Settings").into(); - let button: Element<_> = button(button_label) - .on_press(msg) - .into(); + let button: Element<_> = button(button_label).on_press(msg).into(); - let button = container(button) - .width(Length::Fill) - .align_x(Horizontal::Center) - .into(); + let button = container(button).width(Length::Fill).align_x(Horizontal::Center).into(); - let content: Element<_> = column([ - description, - button - ]).into(); + let content: Element<_> = column([description, button]).into(); let content: Element<_> = container(content) .align_x(Horizontal::Center) @@ -1529,39 +1822,27 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { content } ErrorViewData::PluginError { .. } => { - let description: Element<_> = text("Error occurred in plugin when trying to show the view") - .into(); + let description: Element<_> = text("Error occurred in plugin when trying to show the view").into(); let description = container(description) .width(Length::Fill) .align_x(Horizontal::Center) .themed(ContainerStyle::PluginErrorViewTitle); - let sub_description: Element<_> = text("Please report this to plugin author") - .into(); + let sub_description: Element<_> = text("Please report this to plugin author").into(); let sub_description = container(sub_description) .width(Length::Fill) .align_x(Horizontal::Center) .themed(ContainerStyle::PluginErrorViewDescription); - let button_label: Element<_> = text("Close") - .into(); + let button_label: Element<_> = text("Close").into(); - let button: Element<_> = button(button_label) - .on_press(AppMsg::HideWindow) - .into(); + let button: Element<_> = button(button_label).on_press(AppMsg::HideWindow).into(); - let button = container(button) - .width(Length::Fill) - .align_x(Horizontal::Center) - .into(); + let button = container(button).width(Length::Fill).align_x(Horizontal::Center).into(); - let content: Element<_> = column([ - description, - sub_description, - button - ]).into(); + let content: Element<_> = column([description, sub_description, button]).into(); let content: Element<_> = container(content) .align_x(Horizontal::Center) @@ -1573,8 +1854,7 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { content } ErrorViewData::UnknownError { display } => { - let description: Element<_> = text("Unknown error occurred") - .into(); + let description: Element<_> = text("Unknown error occurred").into(); let description = container(description) .width(Length::Fill) @@ -1589,36 +1869,21 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { .align_x(Horizontal::Center) .themed(ContainerStyle::PluginErrorViewDescription); - let error_description: Element<_> = text(display) - .shaping(Shaping::Advanced) - .into(); + let error_description: Element<_> = text(display).shaping(Shaping::Advanced).into(); let error_description = container(error_description) .width(Length::Fill) .themed(ContainerStyle::PluginErrorViewDescription); - let error_description = scrollable(error_description) - .width(Length::Fill) - .into(); + let error_description = scrollable(error_description).width(Length::Fill).into(); - let button_label: Element<_> = text("Close") - .into(); + let button_label: Element<_> = text("Close").into(); - let button: Element<_> = button(button_label) - .on_press(AppMsg::HideWindow) - .into(); + let button: Element<_> = button(button_label).on_press(AppMsg::HideWindow).into(); - let button = container(button) - .width(Length::Fill) - .align_x(Horizontal::Center) - .into(); + let button = container(button).width(Length::Fill).align_x(Horizontal::Center).into(); - let content: Element<_> = column([ - description, - sub_description, - error_description, - button - ]).into(); + let content: Element<_> = column([description, sub_description, error_description, button]).into(); let content: Element<_> = container(content) .align_x(Horizontal::Center) @@ -1630,39 +1895,28 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { content } ErrorViewData::BackendTimeout => { - let description: Element<_> = text("Error occurred") - .into(); + let description: Element<_> = text("Error occurred").into(); let description = container(description) .width(Length::Fill) .align_x(Horizontal::Center) .themed(ContainerStyle::PluginErrorViewTitle); - let sub_description: Element<_> = text("Backend was unable to process message in a timely manner") - .into(); + let sub_description: Element<_> = + text("Backend was unable to process message in a timely manner").into(); let sub_description = container(sub_description) .width(Length::Fill) .align_x(Horizontal::Center) .themed(ContainerStyle::PluginErrorViewDescription); - let button_label: Element<_> = text("Close") - .into(); + let button_label: Element<_> = text("Close").into(); - let button: Element<_> = button(button_label) - .on_press(AppMsg::HideWindow) - .into(); + let button: Element<_> = button(button_label).on_press(AppMsg::HideWindow).into(); - let button = container(button) - .width(Length::Fill) - .align_x(Horizontal::Center) - .into(); + let button = container(button).width(Length::Fill).align_x(Horizontal::Center).into(); - let content: Element<_> = column([ - description, - sub_description, - button - ]).into(); + let content: Element<_> = column([description, sub_description, button]).into(); let content: Element<_> = container(content) .align_x(Horizontal::Center) @@ -1675,7 +1929,13 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { } } } - GlobalState::MainView { focused_search_result, sub_state, search_field_id, pending_plugin_view_loading_bar, .. } => { + GlobalState::MainView { + focused_search_result, + sub_state, + search_field_id, + pending_plugin_view_loading_bar, + .. + } => { let input: Element<_> = text_input("Search...", &state.prompt) .on_input(AppMsg::PromptChanged) .on_submit(AppMsg::PromptSubmit) @@ -1705,157 +1965,171 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { .width(Length::Fill) .themed(ContainerStyle::MainSearchBar); - let separator = if matches!(pending_plugin_view_loading_bar, LoadingBarState::On) || !state.loading_bar_state.is_empty() { - LoadingBar::new() - .into() + let separator = if matches!(pending_plugin_view_loading_bar, LoadingBarState::On) + || !state.loading_bar_state.is_empty() + { + LoadingBar::new().into() } else { - horizontal_rule(1) - .into() + horizontal_rule(1).into() }; let inline_view = match state.client_context.get_all_inline_view_containers().first() { Some((plugin_id, container)) => { let plugin_id = plugin_id.clone(); - container.render_inline_root_widget() - .map(move |widget_event| { - AppMsg::WidgetEvent { - plugin_id: plugin_id.clone(), - render_location: UiRenderLocation::InlineView, - widget_event, - } - }) - } - None => { - horizontal_space() - .into() + container.render_inline_root_widget().map(move |widget_event| { + AppMsg::WidgetEvent { + plugin_id: plugin_id.clone(), + render_location: UiRenderLocation::InlineView, + widget_event, + } + }) } + None => horizontal_space().into(), }; - let content: Element<_> = column(vec![ - inline_view, - list, - ]).into(); + let content: Element<_> = column(vec![inline_view, list]).into(); - let (primary_action, action_panel) = if let Some(search_item) = focused_search_result.get(&state.search_results) { - let primary_shortcut = PhysicalShortcut { - physical_key: PhysicalKey::Enter, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: false, - }; - - let secondary_shortcut = PhysicalShortcut { - physical_key: PhysicalKey::Enter, - modifier_shift: true, - modifier_control: false, - modifier_alt: false, - modifier_meta: false, - }; - - let create_static = |label: &str, primary_shortcut: PhysicalShortcut, secondary_shortcut: PhysicalShortcut| { - let mut actions: Vec<_> = search_item.entrypoint_actions - .iter() - .enumerate() - .map(|(index, action)| { - let physical_shortcut = if index == 0 { - Some(secondary_shortcut.clone()) - } else { - action.shortcut.clone() - }; - - ActionPanelItem::Action { - label: action.label.clone(), - widget_id: index, - physical_shortcut, - } - }) - .collect(); - - let primary_action_widget_id = 0; - - if actions.len() == 0 { - (Some((label.to_string(), primary_action_widget_id, primary_shortcut)), None) - } else { - let label = label.to_string(); - - let primary_action = ActionPanelItem::Action { - label: label.clone(), - widget_id: primary_action_widget_id, - physical_shortcut: Some(primary_shortcut.clone()), - }; - - actions.insert(0, primary_action); - - let action_panel = ActionPanel { - title: Some(search_item.entrypoint_name.clone()), - items: actions, - }; - - (Some((label, primary_action_widget_id, primary_shortcut)), Some(action_panel)) - } - }; - - let create_generated = |label: &str, primary_shortcut: PhysicalShortcut, secondary_shortcut: PhysicalShortcut| { - let label = search_item.entrypoint_actions - .first() - .map(|action| action.label.clone()) - .unwrap_or_else(|| label.to_string()); // should never happen, because there is always at least one action - - let mut actions: Vec<_> = search_item.entrypoint_actions - .iter() - .enumerate() - .map(|(index, action)| { - let physical_shortcut = match index { - 0 => Some(primary_shortcut.clone()), - 1 => Some(secondary_shortcut.clone()), - _ => action.shortcut.clone() - }; - - ActionPanelItem::Action { - label: action.label.clone(), - widget_id: index, - physical_shortcut, - } - }) - .collect(); - - let primary_action_widget_id = 0; - - let action_panel = ActionPanel { - title: Some(search_item.entrypoint_name.clone()), - items: actions, + let (primary_action, action_panel) = + if let Some(search_item) = focused_search_result.get(&state.search_results) { + let primary_shortcut = PhysicalShortcut { + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, }; - (Some((label, primary_action_widget_id, primary_shortcut)), Some(action_panel)) - }; + let secondary_shortcut = PhysicalShortcut { + physical_key: PhysicalKey::Enter, + modifier_shift: true, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }; - match search_item.entrypoint_type { - SearchResultEntrypointType::Command => create_static("Run Command", primary_shortcut, secondary_shortcut), - SearchResultEntrypointType::View => create_static("Open View", primary_shortcut, secondary_shortcut), - SearchResultEntrypointType::Generated => create_generated("Run Command", primary_shortcut, secondary_shortcut), - } - } else { - match state.client_context.get_first_inline_view_action_panel() { - None => (None, None), - Some(action_panel) => { - match action_panel.find_first() { - None => (None, None), - Some((label, widget_id)) => { - let shortcut = PhysicalShortcut { - physical_key: PhysicalKey::Enter, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: false + let create_static = + |label: &str, primary_shortcut: PhysicalShortcut, secondary_shortcut: PhysicalShortcut| { + let mut actions: Vec<_> = search_item + .entrypoint_actions + .iter() + .enumerate() + .map(|(index, action)| { + let physical_shortcut = if index == 0 { + Some(secondary_shortcut.clone()) + } else { + action.shortcut.clone() + }; + + ActionPanelItem::Action { + label: action.label.clone(), + widget_id: index, + physical_shortcut, + } + }) + .collect(); + + let primary_action_widget_id = 0; + + if actions.len() == 0 { + ( + Some((label.to_string(), primary_action_widget_id, primary_shortcut)), + None, + ) + } else { + let label = label.to_string(); + + let primary_action = ActionPanelItem::Action { + label: label.clone(), + widget_id: primary_action_widget_id, + physical_shortcut: Some(primary_shortcut.clone()), }; - (Some((label, widget_id, shortcut)), Some(action_panel)) + actions.insert(0, primary_action); + + let action_panel = ActionPanel { + title: Some(search_item.entrypoint_name.clone()), + items: actions, + }; + + ( + Some((label, primary_action_widget_id, primary_shortcut)), + Some(action_panel), + ) + } + }; + + let create_generated = + |label: &str, primary_shortcut: PhysicalShortcut, secondary_shortcut: PhysicalShortcut| { + let label = search_item + .entrypoint_actions + .first() + .map(|action| action.label.clone()) + .unwrap_or_else(|| label.to_string()); // should never happen, because there is always at least one action + + let mut actions: Vec<_> = search_item + .entrypoint_actions + .iter() + .enumerate() + .map(|(index, action)| { + let physical_shortcut = match index { + 0 => Some(primary_shortcut.clone()), + 1 => Some(secondary_shortcut.clone()), + _ => action.shortcut.clone(), + }; + + ActionPanelItem::Action { + label: action.label.clone(), + widget_id: index, + physical_shortcut, + } + }) + .collect(); + + let primary_action_widget_id = 0; + + let action_panel = ActionPanel { + title: Some(search_item.entrypoint_name.clone()), + items: actions, + }; + + ( + Some((label, primary_action_widget_id, primary_shortcut)), + Some(action_panel), + ) + }; + + match search_item.entrypoint_type { + SearchResultEntrypointType::Command => { + create_static("Run Command", primary_shortcut, secondary_shortcut) + } + SearchResultEntrypointType::View => { + create_static("Open View", primary_shortcut, secondary_shortcut) + } + SearchResultEntrypointType::Generated => { + create_generated("Run Command", primary_shortcut, secondary_shortcut) + } + } + } else { + match state.client_context.get_first_inline_view_action_panel() { + None => (None, None), + Some(action_panel) => { + match action_panel.find_first() { + None => (None, None), + Some((label, widget_id)) => { + let shortcut = PhysicalShortcut { + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }; + + (Some((label, widget_id, shortcut)), Some(action_panel)) + } } } } - } - }; + }; let toast_text = if !state.loading_bar_state.is_empty() { Some("Indexing...") @@ -1881,7 +2155,9 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { || AppMsg::Noop, ) } - MainViewState::SearchResultActionPanel { focused_action_item, .. } => { + MainViewState::SearchResultActionPanel { + focused_action_item, .. + } => { render_root( true, input, @@ -1898,7 +2174,9 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { || AppMsg::Noop, ) } - MainViewState::InlineViewActionPanel { focused_action_item, .. } => { + MainViewState::InlineViewActionPanel { + focused_action_item, .. + } => { render_root( true, input, @@ -1924,18 +2202,29 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { root } - GlobalState::PluginView { plugin_view_data, sub_state, .. } => { - let PluginViewData { plugin_id, action_shortcuts, .. } = plugin_view_data; + GlobalState::PluginView { + plugin_view_data, + sub_state, + .. + } => { + let PluginViewData { + plugin_id, + action_shortcuts, + .. + } = plugin_view_data; let view_container = state.client_context.get_view_container(); - let container_element = view_container - .render_root_widget(sub_state, action_shortcuts) - .map(|widget_event| AppMsg::WidgetEvent { - plugin_id: plugin_id.clone(), - render_location: UiRenderLocation::View, - widget_event, - }); + let container_element = + view_container + .render_root_widget(sub_state, action_shortcuts) + .map(|widget_event| { + AppMsg::WidgetEvent { + plugin_id: plugin_id.clone(), + render_location: UiRenderLocation::View, + widget_event, + } + }); let element: Element<_> = container(container_element) .width(Length::Fill) @@ -1955,40 +2244,45 @@ fn subscription(state: &AppModel) -> Subscription { struct RequestLoop; struct GlobalShortcutListener; - let events_subscription = event::listen_with(|event, status, window_id| match status { - event::Status::Ignored => Some(AppMsg::IcedEvent(window_id, event)), - event::Status::Captured => match &event { - Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Named(Named::Escape), .. }) => Some(AppMsg::IcedEvent(window_id, event)), - _ => None + let events_subscription = event::listen_with(|event, status, window_id| { + match status { + event::Status::Ignored => Some(AppMsg::IcedEvent(window_id, event)), + event::Status::Captured => { + match &event { + Event::Keyboard(keyboard::Event::KeyPressed { + key: Key::Named(Named::Escape), + .. + }) => Some(AppMsg::IcedEvent(window_id, event)), + _ => None, + } + } } }); Subscription::batch([ Subscription::run_with_id( std::any::TypeId::of::(), - stream::channel( - 10, - |sender| async move { + stream::channel(10, |sender| { + async move { register_listener(sender.clone()); std::future::pending::<()>().await; unreachable!() - }, - ) + } + }), ), events_subscription, Subscription::run_with_id( std::any::TypeId::of::(), - stream::channel( - 100, - |sender| async move { + stream::channel(100, |sender| { + async move { request_loop(frontend_receiver, sender).await; panic!("request_rx was unexpectedly closed") - }, - ) - ) + } + }), + ), ]) } @@ -1997,10 +2291,7 @@ fn assign_global_shortcut( current_hotkey: &Arc>>, shortcut: Option, ) -> anyhow::Result<()> { - - let mut hotkey_guard = current_hotkey - .lock() - .expect("lock is poisoned"); + let mut hotkey_guard = current_hotkey.lock().expect("lock is poisoned"); if let Some(current_hotkey) = *hotkey_guard { global_hotkey_manager.unregister(current_hotkey)?; @@ -2047,26 +2338,21 @@ impl AppModel { #[cfg(target_os = "linux")] if self.wayland { - commands.push( - Task::done(AppMsg::LayerShell(layer_shell::LayerShellAppMsg::RemoveWindow(self.main_window_id))) - ); + commands.push(Task::done(AppMsg::LayerShell( + layer_shell::LayerShellAppMsg::RemoveWindow(self.main_window_id), + ))); } else { - commands.push( - window::change_mode(self.main_window_id, Mode::Hidden) - ); + commands.push(window::change_mode(self.main_window_id, Mode::Hidden)); }; #[cfg(not(target_os = "linux"))] - commands.push( - window::change_mode(self.main_window_id, Mode::Hidden) - ); + commands.push(window::change_mode(self.main_window_id, Mode::Hidden)); #[cfg(target_os = "macos")] unsafe { // when closing NSPanel current active application doesn't automatically become key window // is there a proper way? without doing this manually - let app = objc2_app_kit::NSWorkspace::sharedWorkspace() - .menuBarOwningApplication(); + let app = objc2_app_kit::NSWorkspace::sharedWorkspace().menuBarOwningApplication(); if let Some(app) = app { app.activateWithOptions(objc2_app_kit::NSApplicationActivationOptions::empty()); @@ -2074,7 +2360,10 @@ impl AppModel { } match &self.global_state { - GlobalState::PluginView { plugin_view_data: PluginViewData { plugin_id, .. }, .. } => { + GlobalState::PluginView { + plugin_view_data: PluginViewData { plugin_id, .. }, + .. + } => { commands.push(self.close_plugin_view(plugin_id.clone())); } GlobalState::MainView { .. } => {} @@ -2086,19 +2375,19 @@ impl AppModel { fn show_window(&mut self) -> Task { if self.opened { - return Task::none() + return Task::none(); } self.opened = true; #[cfg(target_os = "linux")] - let open_task = if self.wayland { + let open_task = if self.wayland { let (_, open_task) = open_main_window_wayland(self.main_window_id); open_task } else { Task::batch([ window::gain_focus(self.main_window_id), - window::change_mode(self.main_window_id, Mode::Windowed) + window::change_mode(self.main_window_id, Mode::Windowed), ]) }; @@ -2110,13 +2399,10 @@ impl AppModel { WindowPositionMode::Static => Task::none(), WindowPositionMode::ActiveMonitor => window::move_to_active_monitor(self.main_window_id), }, - window::change_mode(self.main_window_id, Mode::Windowed) + window::change_mode(self.main_window_id, Mode::Windowed), ]); - Task::batch([ - open_task, - self.reset_window_state() - ]) + Task::batch([open_task, self.reset_window_state()]) } fn reset_window_state(&mut self) -> Task { @@ -2130,86 +2416,54 @@ impl AppModel { fn open_plugin_view(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Task { let mut backend_client = self.backend_api.clone(); - Task::perform(async move { - let result = backend_client.request_view_render(plugin_id, entrypoint_id) - .await?; + Task::perform( + async move { + let result = backend_client.request_view_render(plugin_id, entrypoint_id).await?; - Ok(result) - }, |result| handle_backend_error(result, |action_shortcuts| AppMsg::OnOpenView { action_shortcuts })) + Ok(result) + }, + |result| handle_backend_error(result, |action_shortcuts| AppMsg::OnOpenView { action_shortcuts }), + ) } fn close_plugin_view(&self, plugin_id: PluginId) -> Task { let mut backend_client = self.backend_api.clone(); - Task::perform(async move { - backend_client.request_view_close(plugin_id) - .await?; + Task::perform( + async move { + backend_client.request_view_close(plugin_id).await?; - Ok(()) - }, |result| handle_backend_error(result, |()| AppMsg::Noop)) + Ok(()) + }, + |result| handle_backend_error(result, |()| AppMsg::Noop), + ) } fn run_command(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Task { let mut backend_client = self.backend_api.clone(); - Task::perform(async move { - backend_client.request_run_command(plugin_id, entrypoint_id) - .await?; + Task::perform( + async move { + backend_client.request_run_command(plugin_id, entrypoint_id).await?; - Ok(()) - }, |result| handle_backend_error(result, |()| AppMsg::Noop)) + Ok(()) + }, + |result| handle_backend_error(result, |()| AppMsg::Noop), + ) } - fn run_generated_entrypoint(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize) -> Task { - let mut backend_client = self.backend_api.clone(); - - Task::perform(async move { - backend_client.request_run_generated_entrypoint(plugin_id, entrypoint_id, action_index) - .await?; - - Ok(()) - }, |result| handle_backend_error(result, |()| AppMsg::Noop)) - } - - fn handle_plugin_event(&mut self, widget_event: ComponentWidgetEvent, plugin_id: PluginId, render_location: UiRenderLocation) -> Task { - let mut backend_client = self.backend_api.clone(); - - let event = self.client_context.handle_event(render_location, &plugin_id, widget_event.clone()); - - Task::perform(async move { - if let Some(event) = event { - match event { - UiViewEvent::View { widget_id, event_name, event_arguments } => { - let msg = match widget_event { - ComponentWidgetEvent::ActionClick { .. } => AppMsg::ToggleActionPanel { keyboard: false }, - _ => AppMsg::Noop - }; - - backend_client.send_view_event(plugin_id, widget_id, event_name, event_arguments) - .await?; - - Ok(msg) - } - UiViewEvent::Open { href } => { - backend_client.send_open_event(plugin_id, href) - .await?; - - Ok(AppMsg::Noop) - } - UiViewEvent::AppEvent { event } => Ok(event) - } - } else { - Ok(AppMsg::Noop) - } - }, |result| handle_backend_error(result, |msg| msg)) - } - - fn handle_main_view_keyboard_event(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Task { + fn run_generated_entrypoint( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + action_index: usize, + ) -> Task { let mut backend_client = self.backend_api.clone(); Task::perform( async move { - backend_client.send_keyboard_event(plugin_id, entrypoint_id, KeyboardEventOrigin::MainView, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + backend_client + .request_run_generated_entrypoint(plugin_id, entrypoint_id, action_index) .await?; Ok(()) @@ -2218,16 +2472,118 @@ impl AppModel { ) } - fn handle_plugin_view_keyboard_event(&self, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Task { + fn handle_plugin_event( + &mut self, + widget_event: ComponentWidgetEvent, + plugin_id: PluginId, + render_location: UiRenderLocation, + ) -> Task { + let mut backend_client = self.backend_api.clone(); + + let event = self + .client_context + .handle_event(render_location, &plugin_id, widget_event.clone()); + + Task::perform( + async move { + if let Some(event) = event { + match event { + UiViewEvent::View { + widget_id, + event_name, + event_arguments, + } => { + let msg = match widget_event { + ComponentWidgetEvent::ActionClick { .. } => { + AppMsg::ToggleActionPanel { keyboard: false } + } + _ => AppMsg::Noop, + }; + + backend_client + .send_view_event(plugin_id, widget_id, event_name, event_arguments) + .await?; + + Ok(msg) + } + UiViewEvent::Open { href } => { + backend_client.send_open_event(plugin_id, href).await?; + + Ok(AppMsg::Noop) + } + UiViewEvent::AppEvent { event } => Ok(event), + } + } else { + Ok(AppMsg::Noop) + } + }, + |result| handle_backend_error(result, |msg| msg), + ) + } + + fn handle_main_view_keyboard_event( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + physical_key: PhysicalKey, + modifier_shift: bool, + modifier_control: bool, + modifier_alt: bool, + modifier_meta: bool, + ) -> Task { + let mut backend_client = self.backend_api.clone(); + + Task::perform( + async move { + backend_client + .send_keyboard_event( + plugin_id, + entrypoint_id, + KeyboardEventOrigin::MainView, + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + ) + .await?; + + Ok(()) + }, + |result| handle_backend_error(result, |()| AppMsg::Noop), + ) + } + + fn handle_plugin_view_keyboard_event( + &self, + physical_key: PhysicalKey, + modifier_shift: bool, + modifier_control: bool, + modifier_alt: bool, + modifier_meta: bool, + ) -> Task { let mut backend_client = self.backend_api.clone(); let (plugin_id, entrypoint_id) = { - (self.client_context.get_view_plugin_id(), self.client_context.get_view_entrypoint_id()) + ( + self.client_context.get_view_plugin_id(), + self.client_context.get_view_entrypoint_id(), + ) }; Task::perform( async move { - backend_client.send_keyboard_event(plugin_id, entrypoint_id, KeyboardEventOrigin::PluginView, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + backend_client + .send_keyboard_event( + plugin_id, + entrypoint_id, + KeyboardEventOrigin::PluginView, + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + ) .await?; Ok(()) @@ -2236,21 +2592,36 @@ impl AppModel { ) } - fn handle_inline_plugin_view_keyboard_event(&self, physical_key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> Task { + fn handle_inline_plugin_view_keyboard_event( + &self, + physical_key: PhysicalKey, + modifier_shift: bool, + modifier_control: bool, + modifier_alt: bool, + modifier_meta: bool, + ) -> Task { let mut backend_client = self.backend_api.clone(); let (plugin_id, entrypoint_id) = { match self.client_context.get_first_inline_view_container() { - None => { - return Task::none() - }, - Some(container) => (container.get_plugin_id(), container.get_entrypoint_id()) + None => return Task::none(), + Some(container) => (container.get_plugin_id(), container.get_entrypoint_id()), } }; Task::perform( async move { - backend_client.send_keyboard_event(plugin_id, entrypoint_id, KeyboardEventOrigin::PluginView, physical_key, modifier_shift, modifier_control, modifier_alt, modifier_meta) + backend_client + .send_keyboard_event( + plugin_id, + entrypoint_id, + KeyboardEventOrigin::PluginView, + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + ) .await?; Ok(()) @@ -2262,38 +2633,53 @@ impl AppModel { fn search(&self, new_prompt: String, render_inline_view: bool) -> Task { let mut backend_api = self.backend_api.clone(); - Task::perform(async move { - let search_results = backend_api.search(new_prompt, render_inline_view) - .await?; + Task::perform( + async move { + let search_results = backend_api.search(new_prompt, render_inline_view).await?; - Ok(search_results) - }, |result| handle_backend_error(result, |search_results| AppMsg::SetSearchResults(search_results))) + Ok(search_results) + }, + |result| handle_backend_error(result, |search_results| AppMsg::SetSearchResults(search_results)), + ) } - fn open_settings_window_preferences(&self, plugin_id: PluginId, entrypoint_id: Option) -> Task { + fn open_settings_window_preferences( + &self, + plugin_id: PluginId, + entrypoint_id: Option, + ) -> Task { let mut backend_api = self.backend_api.clone(); - Task::perform(async move { - backend_api.open_settings_window_preferences(plugin_id, entrypoint_id) - .await?; + Task::perform( + async move { + backend_api + .open_settings_window_preferences(plugin_id, entrypoint_id) + .await?; - Ok(()) - }, |result| handle_backend_error(result, |()| AppMsg::Noop)) + Ok(()) + }, + |result| handle_backend_error(result, |()| AppMsg::Noop), + ) } fn inline_view_shortcuts(&self) -> Task { let mut backend_api = self.backend_api.clone(); - Task::perform(async move { - backend_api.inline_view_shortcuts().await - }, |result| handle_backend_error(result, |shortcuts| AppMsg::InlineViewShortcuts { shortcuts })) + Task::perform(async move { backend_api.inline_view_shortcuts().await }, |result| { + handle_backend_error(result, |shortcuts| AppMsg::InlineViewShortcuts { shortcuts }) + }) } } // these are needed to force focus the text_input in main search view when // the window is opened but text_input not focused impl AppModel { - fn append_prompt(prompt: &mut String, value: Option, search_field_id: text_input::Id, modifiers: Modifiers) -> Task { + fn append_prompt( + prompt: &mut String, + value: Option, + search_field_id: text_input::Id, + modifiers: Modifiers, + ) -> Task { if modifiers.control() || modifiers.alt() || modifiers.logo() { Task::none() } else { @@ -2306,7 +2692,7 @@ impl AppModel { Task::none() } } - None => Task::none() + None => Task::none(), } } } @@ -2323,7 +2709,7 @@ impl AppModel { fn handle_backend_error(result: Result, convert: impl FnOnce(T) -> AppMsg) -> AppMsg { match result { Ok(val) => convert(val), - Err(err) => AppMsg::ShowBackendError(err) + Err(err) => AppMsg::ShowBackendError(err), } } @@ -2345,7 +2731,7 @@ async fn request_loop( render_location, top_level_view, container, - images + images, } => { responder.respond(UiResponseData::Nothing); @@ -2357,15 +2743,13 @@ async fn request_loop( render_location, top_level_view, container: Arc::new(container), - images + images, } } UiRequestData::ClearInlineView { plugin_id } => { responder.respond(UiResponseData::Nothing); - AppMsg::ClearInlineView { - plugin_id - } + AppMsg::ClearInlineView { plugin_id } } UiRequestData::ShowWindow => { responder.respond(UiResponseData::Nothing); @@ -2381,7 +2765,7 @@ async fn request_loop( plugin_id, entrypoint_id, plugin_preferences_required, - entrypoint_preferences_required + entrypoint_preferences_required, } => { responder.respond(UiResponseData::Nothing); @@ -2389,10 +2773,14 @@ async fn request_loop( plugin_id, entrypoint_id, plugin_preferences_required, - entrypoint_preferences_required + entrypoint_preferences_required, } } - UiRequestData::ShowPluginErrorView { plugin_id, entrypoint_id, render_location } => { + UiRequestData::ShowPluginErrorView { + plugin_id, + entrypoint_id, + render_location, + } => { responder.respond(UiResponseData::Nothing); AppMsg::ShowPluginErrorView { @@ -2409,38 +2797,36 @@ async fn request_loop( UiRequestData::ShowHud { display } => { responder.respond(UiResponseData::Nothing); - AppMsg::ShowHud { - display - } + AppMsg::ShowHud { display } } UiRequestData::SetGlobalShortcut { shortcut } => { AppMsg::SetGlobalShortcut { shortcut, - responder: Arc::new(Mutex::new(Some(responder))) + responder: Arc::new(Mutex::new(Some(responder))), } } - UiRequestData::UpdateLoadingBar { plugin_id, entrypoint_id, show } => { + UiRequestData::UpdateLoadingBar { + plugin_id, + entrypoint_id, + show, + } => { responder.respond(UiResponseData::Nothing); AppMsg::UpdateLoadingBar { plugin_id, entrypoint_id, - show + show, } } UiRequestData::SetTheme { theme } => { responder.respond(UiResponseData::Nothing); - AppMsg::SetTheme { - theme, - } + AppMsg::SetTheme { theme } } UiRequestData::SetWindowPositionMode { mode } => { responder.respond(UiResponseData::Nothing); - AppMsg::SetWindowPositionMode { - mode, - } + AppMsg::SetWindowPositionMode { mode } } } }; diff --git a/rust/client/src/ui/scroll_handle.rs b/rust/client/src/ui/scroll_handle.rs index 222cd80..9c4d1a8 100644 --- a/rust/client/src/ui/scroll_handle.rs +++ b/rust/client/src/ui/scroll_handle.rs @@ -1,6 +1,10 @@ use std::marker::PhantomData; + +use iced::widget::scrollable::scroll_to; +use iced::widget::scrollable::AbsoluteOffset; +use iced::widget::scrollable::Id; use iced::Task; -use iced::widget::scrollable::{scroll_to, AbsoluteOffset, Id}; + use crate::ui::AppMsg; pub const ESTIMATED_MAIN_LIST_ITEM_HEIGHT: f32 = 38.8; @@ -38,14 +42,14 @@ impl ScrollHandle { pub fn get<'a, T>(&self, search_results: &'a [T]) -> Option<&'a T> { match self.index { None => None, - Some(index) => search_results.get(index) + Some(index) => search_results.get(index), } } pub fn focus_next(&mut self, total_item_amount: usize) -> Option> { match self.focus_next_in(total_item_amount, 1) { None => None, - Some(index) => Some(self.scroll_to(index)) + Some(index) => Some(self.scroll_to(index)), } } @@ -84,27 +88,24 @@ impl ScrollHandle { pub fn focus_previous(&mut self) -> Option> { match self.focus_previous_in(1) { None => None, - Some(index) => Some(self.scroll_to(index)) + Some(index) => Some(self.scroll_to(index)), } } pub fn focus_previous_in(&mut self, amount: usize) -> Option { - self.offset = if self.offset > 1 { - self.offset - 1 - } else { - 1 - }; + self.offset = if self.offset > 1 { self.offset - 1 } else { 1 }; match self.index.as_mut() { None => None, Some(index) => { - match index.checked_sub(amount) { // basically a check if result is >= 0 + match index.checked_sub(amount) { + // basically a check if result is >= 0 Some(new_index) => { *index = new_index; Some(new_index) } - None => None + None => None, } } } @@ -115,4 +116,4 @@ impl ScrollHandle { scroll_to(self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: pos_y }) } -} \ No newline at end of file +} diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index ee9a42e..85aa0a5 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -1,21 +1,31 @@ +use std::collections::HashMap; + +use gauntlet_common::model::IconAccessoryWidget; +use gauntlet_common::model::ImageLike; +use gauntlet_common::model::SearchResult; +use gauntlet_common::model::SearchResultAccessory; +use gauntlet_common::model::TextAccessoryWidget; +use iced::advanced::image::Handle; +use iced::widget::button; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_space; +use iced::widget::row; +use iced::widget::text; +use iced::widget::text::Shaping; +use iced::Alignment; +use iced::Length; + use crate::ui::scroll_handle::ScrollHandle; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; use crate::ui::theme::image::ImageStyle; use crate::ui::theme::space::ThemeKindSpace; use crate::ui::theme::text::TextStyle; -use crate::ui::theme::{Element, ThemableWidget}; -use crate::ui::widget::{render_icon_accessory, render_text_accessory}; -use std::collections::HashMap; - -use gauntlet_common::model::{IconAccessoryWidget, ImageLike, SearchResult, SearchResultAccessory, TextAccessoryWidget}; -use iced::advanced::image::Handle; -use iced::widget::button; -use iced::widget::row; -use iced::widget::text; -use iced::widget::text::Shaping; -use iced::widget::{column, container, horizontal_space}; -use iced::{Alignment, Length}; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::render_icon_accessory; +use crate::ui::widget::render_text_accessory; pub fn search_list<'a>( search_results: &'a [SearchResult], @@ -25,44 +35,38 @@ pub fn search_list<'a>( .iter() .enumerate() .map(|(index, search_result)| { - let main_text: Element<_> = text(&search_result.entrypoint_name) - .shaping(Shaping::Advanced) - .into(); - let main_text: Element<_> = container(main_text) - .themed(ContainerStyle::MainListItemText); + let main_text: Element<_> = text(&search_result.entrypoint_name).shaping(Shaping::Advanced).into(); + let main_text: Element<_> = container(main_text).themed(ContainerStyle::MainListItemText); - let spacer: Element<_> = horizontal_space() - .width(Length::Fill) - .into(); + let spacer: Element<_> = horizontal_space().width(Length::Fill).into(); let sub_text = match &search_result.entrypoint_generator_name { None => &search_result.plugin_name, - Some(entrypoint_generator_name) => &format!("{} - {}", entrypoint_generator_name, &search_result.plugin_name) + Some(entrypoint_generator_name) => { + &format!("{} - {}", entrypoint_generator_name, &search_result.plugin_name) + } }; let sub_text: Element<_> = text(sub_text.clone()) .shaping(Shaping::Advanced) .themed(TextStyle::MainListItemSubtext); - let sub_text: Element<_> = container(sub_text) - .themed(ContainerStyle::MainListItemSubText); // FIXME find a way to set padding based on whether the scroll bar is visible + let sub_text: Element<_> = container(sub_text).themed(ContainerStyle::MainListItemSubText); // FIXME find a way to set padding based on whether the scroll bar is visible let mut button_content = vec![]; if let Some(path) = &search_result.entrypoint_icon { - let image: Element<_> = iced::widget::image(Handle::from_bytes(path.clone())) - .themed(ImageStyle::MainListItemIcon); + let image: Element<_> = + iced::widget::image(Handle::from_bytes(path.clone())).themed(ImageStyle::MainListItemIcon); - let image: Element<_> = container(image) - .themed(ContainerStyle::MainListItemIcon); + let image: Element<_> = container(image).themed(ContainerStyle::MainListItemIcon); button_content.push(image); } else { let spacer: Element<_> = horizontal_space() // TODO replace with grayed out gauntlet icon - .themed(ThemeKindSpace::MainListItemIcon); + .themed(ThemeKindSpace::MainListItemIcon); - let spacer: Element<_> = container(spacer) - .themed(ContainerStyle::MainListItemIcon); + let spacer: Element<_> = container(spacer).themed(ContainerStyle::MainListItemIcon); button_content.push(spacer); } @@ -71,40 +75,44 @@ pub fn search_list<'a>( button_content.push(spacer); if search_result.entrypoint_accessories.len() > 0 { - let accessories: Vec> = search_result.entrypoint_accessories + let accessories: Vec> = search_result + .entrypoint_accessories .iter() .map(|accessory| { match accessory { SearchResultAccessory::TextAccessory { text, icon, tooltip } => { - render_text_accessory(&HashMap::new(), &TextAccessoryWidget { - __id__: 0, - text: text.clone(), - icon: icon.as_ref().map(|icon| ImageLike::Icons(icon.clone())), - tooltip: tooltip.clone(), - }) - }, + render_text_accessory( + &HashMap::new(), + &TextAccessoryWidget { + __id__: 0, + text: text.clone(), + icon: icon.as_ref().map(|icon| ImageLike::Icons(icon.clone())), + tooltip: tooltip.clone(), + }, + ) + } SearchResultAccessory::IconAccessory { icon, tooltip } => { - render_icon_accessory(&HashMap::new(), &IconAccessoryWidget { - __id__: 0, - icon: ImageLike::Icons(icon.clone()), - tooltip: tooltip.clone(), - }) + render_icon_accessory( + &HashMap::new(), + &IconAccessoryWidget { + __id__: 0, + icon: ImageLike::Icons(icon.clone()), + tooltip: tooltip.clone(), + }, + ) } } }) .collect(); - let accessories: Element<_> = row(accessories) - .into(); + let accessories: Element<_> = row(accessories).into(); button_content.push(accessories); } button_content.push(sub_text); - let button_content: Element<_> = row(button_content) - .align_y(Alignment::Center) - .into(); + let button_content: Element<_> = row(button_content).align_y(Alignment::Center).into(); let style = match focused_search_result.index { None => ButtonStyle::MainListItem, diff --git a/rust/client/src/ui/state/main_view.rs b/rust/client/src/ui/state/main_view.rs index 408dfc0..c9c6a2e 100644 --- a/rust/client/src/ui/state/main_view.rs +++ b/rust/client/src/ui/state/main_view.rs @@ -1,4 +1,5 @@ -use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_ACTION_ITEM_HEIGHT}; +use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::scroll_handle::ESTIMATED_ACTION_ITEM_HEIGHT; pub enum MainViewState { None, @@ -9,7 +10,7 @@ pub enum MainViewState { InlineViewActionPanel { // ephemeral state focused_action_item: ScrollHandle, - } + }, } impl MainViewState { diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index e4b3190..c461894 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -1,16 +1,22 @@ mod main_view; mod plugin_view; -use crate::ui::client_context::ClientContext; -use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_MAIN_LIST_ITEM_HEIGHT}; -pub use crate::ui::state::main_view::MainViewState; -pub use crate::ui::state::plugin_view::PluginViewState; -use crate::ui::AppMsg; -use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult}; +use std::collections::HashMap; + +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::SearchResult; use iced::widget::text_input; use iced::widget::text_input::focus; use iced::Task; -use std::collections::HashMap; + +use crate::ui::client_context::ClientContext; +use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::scroll_handle::ESTIMATED_MAIN_LIST_ITEM_HEIGHT; +pub use crate::ui::state::main_view::MainViewState; +pub use crate::ui::state::plugin_view::PluginViewState; +use crate::ui::AppMsg; pub enum GlobalState { MainView { @@ -58,7 +64,7 @@ pub enum ErrorViewData { }, BackendTimeout, UnknownError { - display: String + display: String, }, } @@ -66,10 +72,9 @@ pub enum ErrorViewData { pub enum LoadingBarState { Off, Pending, - On + On, } - impl GlobalState { pub fn new(search_field_id: text_input::Id) -> GlobalState { GlobalState::MainView { @@ -99,10 +104,7 @@ impl GlobalState { *prev_global_state = GlobalState::new(search_field_id.clone()); - Task::batch([ - focus(search_field_id), - Task::done(AppMsg::UpdateSearchResults), - ]) + Task::batch([focus(search_field_id), Task::done(AppMsg::UpdateSearchResults)]) } pub fn error(prev_global_state: &mut GlobalState, error_view_data: ErrorViewData) -> Task { @@ -133,7 +135,11 @@ pub trait Focus { impl Focus for GlobalState { fn primary(&mut self, client_context: &ClientContext, focus_list: &[SearchResult]) -> Task { match self { - GlobalState::MainView { focused_search_result, sub_state, .. } => { + GlobalState::MainView { + focused_search_result, + sub_state, + .. + } => { match sub_state { MainViewState::None => { if let Some(search_result) = focused_search_result.get(focus_list) { @@ -143,13 +149,18 @@ impl Focus for GlobalState { Task::done(AppMsg::OnPrimaryActionMainViewNoPanelKeyboardWithoutFocus) } } - MainViewState::SearchResultActionPanel { focused_action_item, .. } => { + MainViewState::SearchResultActionPanel { + focused_action_item, .. + } => { match focused_action_item.index { None => Task::none(), Some(widget_id) => { if let Some(search_result) = focused_search_result.get(&focus_list) { let search_result = search_result.clone(); - Task::done(AppMsg::OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { search_result, widget_id }) + Task::done(AppMsg::OnAnyActionMainViewSearchResultPanelKeyboardWithFocus { + search_result, + widget_id, + }) } else { Task::none() } @@ -174,28 +185,40 @@ impl Focus for GlobalState { PluginViewState::None => { if let Some(widget_id) = action_ids.get(0) { let widget_id = *widget_id; - Task::done(AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id, id: focused_item_id }) + Task::done(AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { + widget_id, + id: focused_item_id, + }) } else { Task::none() } - }, - PluginViewState::ActionPanel { focused_action_item, .. } => { + } + PluginViewState::ActionPanel { + focused_action_item, .. + } => { if let Some(widget_id) = focused_action_item.get(&action_ids) { let widget_id = *widget_id; - Task::done(AppMsg::OnAnyActionPluginViewAnyPanelKeyboardWithFocus { widget_id, id: focused_item_id }) + Task::done(AppMsg::OnAnyActionPluginViewAnyPanelKeyboardWithFocus { + widget_id, + id: focused_item_id, + }) } else { Task::none() } } } } - GlobalState::ErrorView { .. } => Task::none() + GlobalState::ErrorView { .. } => Task::none(), } } fn secondary(&mut self, client_context: &ClientContext, focus_list: &[SearchResult]) -> Task { match self { - GlobalState::MainView { focused_search_result, sub_state, .. } => { + GlobalState::MainView { + focused_search_result, + sub_state, + .. + } => { match sub_state { MainViewState::None => { if let Some(search_result) = focused_search_result.get(focus_list) { @@ -219,18 +242,21 @@ impl Focus for GlobalState { PluginViewState::None => { if let Some(widget_id) = action_ids.get(1) { let widget_id = *widget_id; - Task::done(AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { widget_id, id: focused_item_id }) + Task::done(AppMsg::OnAnyActionPluginViewNoPanelKeyboardWithFocus { + widget_id, + id: focused_item_id, + }) } else { Task::none() } - }, + } PluginViewState::ActionPanel { .. } => { // secondary does nothing when action panel is opened Task::none() } } } - GlobalState::ErrorView { .. } => Task::none() + GlobalState::ErrorView { .. } => Task::none(), } } @@ -238,9 +264,7 @@ impl Focus for GlobalState { match self { GlobalState::MainView { sub_state, .. } => { match sub_state { - MainViewState::None => { - Task::done(AppMsg::HideWindow) - } + MainViewState::None => Task::done(AppMsg::HideWindow), MainViewState::SearchResultActionPanel { .. } => { MainViewState::initial(sub_state); Task::none() @@ -252,12 +276,13 @@ impl Focus for GlobalState { } } GlobalState::PluginView { - plugin_view_data: PluginViewData { - top_level_view, - plugin_id, - entrypoint_id, - .. - }, + plugin_view_data: + PluginViewData { + top_level_view, + plugin_id, + entrypoint_id, + .. + }, sub_state, .. } => { @@ -268,7 +293,7 @@ impl Focus for GlobalState { Task::batch([ Task::done(AppMsg::ClosePluginView(plugin_id)), - GlobalState::initial(self) + GlobalState::initial(self), ]) } else { let plugin_id = plugin_id.clone(); @@ -276,14 +301,10 @@ impl Focus for GlobalState { Task::done(AppMsg::OpenPluginView(plugin_id, entrypoint_id)) } } - PluginViewState::ActionPanel { .. } => { - Task::done(AppMsg::ToggleActionPanel { keyboard: true }) - } + PluginViewState::ActionPanel { .. } => Task::done(AppMsg::ToggleActionPanel { keyboard: true }), } } - GlobalState::ErrorView { .. } => { - Task::done(AppMsg::HideWindow) - } + GlobalState::ErrorView { .. } => Task::done(AppMsg::HideWindow), } } fn next(&mut self, _client_context: &ClientContext) -> Task { @@ -302,43 +323,44 @@ impl Focus for GlobalState { } fn up(&mut self, client_context: &mut ClientContext, _focus_list: &[SearchResult]) -> Task { match self { - GlobalState::MainView { focused_search_result, sub_state, .. } => { + GlobalState::MainView { + focused_search_result, + sub_state, + .. + } => { match sub_state { - MainViewState::None => { - focused_search_result.focus_previous() - .unwrap_or_else(|| Task::none()) - } + MainViewState::None => focused_search_result.focus_previous().unwrap_or_else(|| Task::none()), MainViewState::SearchResultActionPanel { focused_action_item } => { - focused_action_item.focus_previous() - .unwrap_or_else(|| Task::none()) + focused_action_item.focus_previous().unwrap_or_else(|| Task::none()) } MainViewState::InlineViewActionPanel { focused_action_item } => { - focused_action_item.focus_previous() - .unwrap_or_else(|| Task::none()) + focused_action_item.focus_previous().unwrap_or_else(|| Task::none()) } } } GlobalState::ErrorView { .. } => Task::none(), GlobalState::PluginView { sub_state, .. } => { match sub_state { - PluginViewState::None => { - client_context.focus_up() - }, + PluginViewState::None => client_context.focus_up(), PluginViewState::ActionPanel { focused_action_item } => { - focused_action_item.focus_previous() - .unwrap_or_else(|| Task::none()) + focused_action_item.focus_previous().unwrap_or_else(|| Task::none()) } } - }, + } } } fn down(&mut self, client_context: &mut ClientContext, focus_list: &[SearchResult]) -> Task { match self { - GlobalState::MainView { focused_search_result, sub_state, .. } => { + GlobalState::MainView { + focused_search_result, + sub_state, + .. + } => { match sub_state { MainViewState::None => { if focus_list.len() != 0 { - focused_search_result.focus_next(focus_list.len()) + focused_search_result + .focus_next(focus_list.len()) .unwrap_or_else(|| Task::none()) } else { Task::none() @@ -347,7 +369,8 @@ impl Focus for GlobalState { MainViewState::SearchResultActionPanel { focused_action_item } => { if let Some(search_item) = focused_search_result.get(focus_list) { if search_item.entrypoint_actions.len() != 0 { - focused_action_item.focus_next(search_item.entrypoint_actions.len()) + focused_action_item + .focus_next(search_item.entrypoint_actions.len()) .unwrap_or_else(|| Task::none()) } else { Task::none() @@ -360,13 +383,14 @@ impl Focus for GlobalState { match client_context.get_first_inline_view_action_panel() { Some(action_panel) => { if action_panel.action_count() != 0 { - focused_action_item.focus_next(action_panel.action_count()) + focused_action_item + .focus_next(action_panel.action_count()) .unwrap_or_else(|| Task::none()) } else { Task::none() } } - None => Task::none() + None => Task::none(), } } } @@ -374,14 +398,13 @@ impl Focus for GlobalState { GlobalState::ErrorView { .. } => Task::none(), GlobalState::PluginView { sub_state, .. } => { match sub_state { - PluginViewState::None => { - client_context.focus_down() - }, + PluginViewState::None => client_context.focus_down(), PluginViewState::ActionPanel { focused_action_item } => { let action_ids = client_context.get_action_ids(); if action_ids.len() != 0 { - focused_action_item.focus_next(action_ids.len()) + focused_action_item + .focus_next(action_ids.len()) .unwrap_or_else(|| Task::none()) } else { Task::none() @@ -395,12 +418,10 @@ impl Focus for GlobalState { match self { GlobalState::PluginView { sub_state, .. } => { match sub_state { - PluginViewState::None => { - client_context.focus_left() - } - PluginViewState::ActionPanel { .. } => Task::none() + PluginViewState::None => client_context.focus_left(), + PluginViewState::ActionPanel { .. } => Task::none(), } - }, + } GlobalState::MainView { .. } => Task::none(), GlobalState::ErrorView { .. } => Task::none(), } @@ -409,12 +430,10 @@ impl Focus for GlobalState { match self { GlobalState::PluginView { sub_state, .. } => { match sub_state { - PluginViewState::None => { - client_context.focus_right() - } - PluginViewState::ActionPanel { .. } => Task::none() + PluginViewState::None => client_context.focus_right(), + PluginViewState::ActionPanel { .. } => Task::none(), } - }, + } GlobalState::MainView { .. } => Task::none(), GlobalState::ErrorView { .. } => Task::none(), } diff --git a/rust/client/src/ui/state/plugin_view.rs b/rust/client/src/ui/state/plugin_view.rs index c7ac0e2..f3627b2 100644 --- a/rust/client/src/ui/state/plugin_view.rs +++ b/rust/client/src/ui/state/plugin_view.rs @@ -1,4 +1,5 @@ -use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_ACTION_ITEM_HEIGHT}; +use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::scroll_handle::ESTIMATED_ACTION_ITEM_HEIGHT; #[derive(Debug, Clone)] pub enum PluginViewState { @@ -6,7 +7,7 @@ pub enum PluginViewState { ActionPanel { // ephemeral state focused_action_item: ScrollHandle, - } + }, } impl PluginViewState { diff --git a/rust/client/src/ui/sys_tray.rs b/rust/client/src/ui/sys_tray.rs index c4bcd54..9a6cebe 100644 --- a/rust/client/src/ui/sys_tray.rs +++ b/rust/client/src/ui/sys_tray.rs @@ -1,17 +1,17 @@ use image::ImageFormat; pub fn create_tray() -> tray_icon::TrayIcon { + use tray_icon::menu::AboutMetadataBuilder; + use tray_icon::menu::Menu; + use tray_icon::menu::MenuEvent; + use tray_icon::menu::MenuItem; + use tray_icon::menu::PredefinedMenuItem; use tray_icon::TrayIconBuilder; - use tray_icon::menu::{MenuEvent, Menu, MenuItem, PredefinedMenuItem, AboutMetadataBuilder}; MenuEvent::set_event_handler(Some(|event: MenuEvent| { match event.id().as_ref() { - "GAUNTLET_OPEN_MAIN_WINDOW" => { - crate::open_window() - } - "GAUNTLET_OPEN_SETTING_WINDOW" => { - crate::open_settings_window() - } + "GAUNTLET_OPEN_MAIN_WINDOW" => crate::open_window(), + "GAUNTLET_OPEN_SETTING_WINDOW" => crate::open_settings_window(), _ => {} } })); @@ -26,18 +26,19 @@ pub fn create_tray() -> tray_icon::TrayIcon { let (width, height) = image.dimensions(); let rgba = image.into_raw(); - let tray_icon = tray_icon::Icon::from_rgba(rgba.clone(), width, height) - .expect("Failed to open icon"); + let tray_icon = tray_icon::Icon::from_rgba(rgba.clone(), width, height).expect("Failed to open icon"); - let muda_icon = tray_icon::menu::Icon::from_rgba(rgba, width, height) - .expect("Failed to open icon"); + let muda_icon = tray_icon::menu::Icon::from_rgba(rgba, width, height).expect("Failed to open icon"); (tray_icon, muda_icon) }; let about_metadata = AboutMetadataBuilder::new() .name(Some("Gauntlet")) - .version(Some(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../VERSION")))) + .version(Some(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../VERSION" + )))) .authors(Some(vec!["Exidex".to_string()])) .credits(Some("Exidex".to_string())) .license(Some("MPL-2.0")) @@ -45,20 +46,19 @@ pub fn create_tray() -> tray_icon::TrayIcon { .icon(Some(muda_icon)) .build(); - let menu = Menu::with_items( - &[ - &MenuItem::new("Gauntlet", false, None), - &MenuItem::with_id("GAUNTLET_OPEN_MAIN_WINDOW", "Open", true, None), - &MenuItem::with_id("GAUNTLET_OPEN_SETTING_WINDOW", "Open Settings", true, None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::about(Some("About..."), Some(about_metadata)), - &PredefinedMenuItem::quit(Some("Quit Gauntlet")), - ] - ).expect("unable to create tray menu"); + let menu = Menu::with_items(&[ + &MenuItem::new("Gauntlet", false, None), + &MenuItem::with_id("GAUNTLET_OPEN_MAIN_WINDOW", "Open", true, None), + &MenuItem::with_id("GAUNTLET_OPEN_SETTING_WINDOW", "Open Settings", true, None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::about(Some("About..."), Some(about_metadata)), + &PredefinedMenuItem::quit(Some("Quit Gauntlet")), + ]) + .expect("unable to create tray menu"); TrayIconBuilder::new() .with_menu(Box::new(menu)) .with_icon(tray_icon) .build() .expect("unable to create tray") -} \ No newline at end of file +} diff --git a/rust/client/src/ui/theme/button.rs b/rust/client/src/ui/theme/button.rs index 7bd776d..232bb19 100644 --- a/rust/client/src/ui/theme/button.rs +++ b/rust/client/src/ui/theme/button.rs @@ -1,8 +1,18 @@ use button::Style; -use iced::{Border, Color, Padding, Renderer}; -use iced::widget::{button, Button}; +use iced::widget::button; use iced::widget::button::Status; -use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, NOT_INTENDED_TO_BE_USED, padding_all, ThemableWidget}; +use iced::widget::Button; +use iced::Border; +use iced::Color; +use iced::Padding; +use iced::Renderer; + +use crate::ui::theme::get_theme; +use crate::ui::theme::padding_all; +use crate::ui::theme::Element; +use crate::ui::theme::GauntletComplexTheme; +use crate::ui::theme::ThemableWidget; +use crate::ui::theme::NOT_INTENDED_TO_BE_USED; #[derive(Debug, Clone, Copy)] pub enum ButtonStyle { @@ -34,18 +44,18 @@ impl ButtonStyle { let theme = &theme.root_bottom_panel_action_toggle_button; theme.padding.to_iced() - }, + } ButtonStyle::RootBottomPanelPrimaryActionButton => { let theme = &theme.root_bottom_panel_action_toggle_button; theme.padding.to_iced() - }, + } ButtonStyle::RootTopPanelBackButton => { let theme = &theme.root_top_panel_button; theme.padding.to_iced() - }, - ButtonStyle::GridItem | ButtonStyle::GridItemFocused => { + } + ButtonStyle::GridItem | ButtonStyle::GridItemFocused => { let theme = &theme.grid_item; theme.padding.to_iced() @@ -65,78 +75,207 @@ impl ButtonStyle { theme.padding.to_iced() } - ButtonStyle::MetadataLink => { - padding_all(0.0).to_iced() - } + ButtonStyle::MetadataLink => padding_all(0.0).to_iced(), ButtonStyle::MetadataTagItem => { let theme = &theme.metadata_tag_item_button; theme.padding.to_iced() } - ButtonStyle::ShouldNotBeUsed => { - padding_all(5.0).to_iced() - } - ButtonStyle::DatePicker => { - padding_all(5.0).to_iced() - } + ButtonStyle::ShouldNotBeUsed => padding_all(5.0).to_iced(), + ButtonStyle::DatePicker => padding_all(5.0).to_iced(), } } fn appearance(&self, theme: &GauntletComplexTheme, state: Status) -> Style { - let (background_color, background_color_hover, background_color_pressed, text_color, text_color_hover, border_radius, border_width, border_color) = match &self { + let ( + background_color, + background_color_hover, + background_color_pressed, + text_color, + text_color_hover, + border_radius, + border_width, + border_color, + ) = match &self { ButtonStyle::RootBottomPanelPrimaryActionButton | ButtonStyle::RootBottomPanelActionToggleButton => { let theme = &theme.root_bottom_panel_action_toggle_button; - (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) - }, + ( + Some(&theme.background_color), + Some(&theme.background_color_hovered), + Some(&theme.background_color_hovered), + &theme.text_color, + &theme.text_color_hovered, + &theme.border_radius, + &theme.border_width, + &theme.border_color, + ) + } ButtonStyle::RootTopPanelBackButton => { let theme = &theme.root_top_panel_button; - (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color_hovered), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) - }, + ( + Some(&theme.background_color), + Some(&theme.background_color_hovered), + Some(&theme.background_color_hovered), + &theme.text_color, + &theme.text_color_hovered, + &theme.border_radius, + &theme.border_width, + &theme.border_color, + ) + } ButtonStyle::GridItem => { let theme = &theme.grid_item; - (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + ( + Some(&theme.background_color), + Some(&theme.background_color_hovered), + Some(&theme.background_color), + &theme.text_color, + &theme.text_color_hovered, + &theme.border_radius, + &theme.border_width, + &theme.border_color, + ) } ButtonStyle::GridItemFocused => { let theme = &theme.grid_item; - (Some(&theme.background_color_focused), Some(&theme.background_color_focused), Some(&theme.background_color), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + ( + Some(&theme.background_color_focused), + Some(&theme.background_color_focused), + Some(&theme.background_color), + &theme.text_color_hovered, + &theme.text_color_hovered, + &theme.border_radius, + &theme.border_width, + &theme.border_color, + ) } ButtonStyle::Action => { let theme = &theme.action; - (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + ( + Some(&theme.background_color), + Some(&theme.background_color_hovered), + Some(&theme.background_color), + &theme.text_color, + &theme.text_color_hovered, + &theme.border_radius, + &theme.border_width, + &theme.border_color, + ) } ButtonStyle::ActionFocused => { let theme = &theme.action; - (Some(&theme.background_color_focused), Some(&theme.background_color_focused), Some(&theme.background_color), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + ( + Some(&theme.background_color_focused), + Some(&theme.background_color_focused), + Some(&theme.background_color), + &theme.text_color_hovered, + &theme.text_color_hovered, + &theme.border_radius, + &theme.border_width, + &theme.border_color, + ) } ButtonStyle::ListItem => { let theme = &theme.list_item; - (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + ( + Some(&theme.background_color), + Some(&theme.background_color_hovered), + Some(&theme.background_color), + &theme.text_color, + &theme.text_color_hovered, + &theme.border_radius, + &theme.border_width, + &theme.border_color, + ) } ButtonStyle::ListItemFocused => { let theme = &theme.list_item; - (Some(&theme.background_color_focused), Some(&theme.background_color_focused), Some(&theme.background_color), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + ( + Some(&theme.background_color_focused), + Some(&theme.background_color_focused), + Some(&theme.background_color), + &theme.text_color_hovered, + &theme.text_color_hovered, + &theme.border_radius, + &theme.border_width, + &theme.border_color, + ) } ButtonStyle::MainListItem => { let theme = &theme.main_list_item; - (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + ( + Some(&theme.background_color), + Some(&theme.background_color_hovered), + Some(&theme.background_color), + &theme.text_color, + &theme.text_color_hovered, + &theme.border_radius, + &theme.border_width, + &theme.border_color, + ) } ButtonStyle::MainListItemFocused => { let theme = &theme.main_list_item; - (Some(&theme.background_color_focused), Some(&theme.background_color_focused), Some(&theme.background_color), &theme.text_color_hovered, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + ( + Some(&theme.background_color_focused), + Some(&theme.background_color_focused), + Some(&theme.background_color), + &theme.text_color_hovered, + &theme.text_color_hovered, + &theme.border_radius, + &theme.border_width, + &theme.border_color, + ) } ButtonStyle::MetadataLink => { let theme = &theme.metadata_link; - (None, None, None, &theme.text_color, &theme.text_color_hovered, &0.0, &1.0, &Color::TRANSPARENT) + ( + None, + None, + None, + &theme.text_color, + &theme.text_color_hovered, + &0.0, + &1.0, + &Color::TRANSPARENT, + ) } ButtonStyle::MetadataTagItem => { let theme = &theme.metadata_tag_item_button; - (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + ( + Some(&theme.background_color), + Some(&theme.background_color_hovered), + Some(&theme.background_color), + &theme.text_color, + &theme.text_color_hovered, + &theme.border_radius, + &theme.border_width, + &theme.border_color, + ) } ButtonStyle::ShouldNotBeUsed => { - (Some(&NOT_INTENDED_TO_BE_USED), Some(&NOT_INTENDED_TO_BE_USED), Some(&NOT_INTENDED_TO_BE_USED), &NOT_INTENDED_TO_BE_USED, &NOT_INTENDED_TO_BE_USED, &0.0, &1.0, &Color::TRANSPARENT) + ( + Some(&NOT_INTENDED_TO_BE_USED), + Some(&NOT_INTENDED_TO_BE_USED), + Some(&NOT_INTENDED_TO_BE_USED), + &NOT_INTENDED_TO_BE_USED, + &NOT_INTENDED_TO_BE_USED, + &0.0, + &1.0, + &Color::TRANSPARENT, + ) } ButtonStyle::DatePicker => { let theme = &theme.form_input_date_picker_buttons; - (Some(&theme.background_color), Some(&theme.background_color_hovered), Some(&theme.background_color), &theme.text_color, &theme.text_color_hovered, &theme.border_radius, &theme.border_width, &theme.border_color) + ( + Some(&theme.background_color), + Some(&theme.background_color_hovered), + Some(&theme.background_color), + &theme.text_color, + &theme.text_color_hovered, + &theme.border_radius, + &theme.border_width, + &theme.border_color, + ) } }; @@ -197,4 +336,4 @@ impl<'a, Message: 'a + Clone> ThemableWidget<'a, Message> for Button<'a, Message fn themed(self, kind: ButtonStyle) -> Element<'a, Message> { self.class(kind).padding(kind.padding()).into() } -} \ No newline at end of file +} diff --git a/rust/client/src/ui/theme/checkbox.rs b/rust/client/src/ui/theme/checkbox.rs index 10d57a0..861e7a2 100644 --- a/rust/client/src/ui/theme/checkbox.rs +++ b/rust/client/src/ui/theme/checkbox.rs @@ -1,7 +1,15 @@ -use iced::{Border, Renderer}; -use iced::widget::{checkbox, Checkbox}; -use iced::widget::checkbox::{Status, Style}; -use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, NOT_INTENDED_TO_BE_USED, ThemableWidget}; +use iced::widget::checkbox; +use iced::widget::checkbox::Status; +use iced::widget::checkbox::Style; +use iced::widget::Checkbox; +use iced::Border; +use iced::Renderer; + +use crate::ui::theme::get_theme; +use crate::ui::theme::Element; +use crate::ui::theme::GauntletComplexTheme; +use crate::ui::theme::ThemableWidget; +use crate::ui::theme::NOT_INTENDED_TO_BE_USED; pub enum CheckboxStyle { Default, @@ -18,7 +26,7 @@ impl checkbox::Catalog for GauntletComplexTheme { match status { Status::Active { is_checked } => active(self, is_checked), Status::Hovered { is_checked } => hovered(self, is_checked), - Status::Disabled { is_checked } => disabled(is_checked) + Status::Disabled { is_checked } => disabled(is_checked), } } } @@ -87,9 +95,10 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Checkbox<'a, Message, Gaun match style { CheckboxStyle::Default => { self.class(style) - // .spacing() // TODO - // .size() + // .spacing() // TODO + // .size() } - }.into() + } + .into() } } diff --git a/rust/client/src/ui/theme/container.rs b/rust/client/src/ui/theme/container.rs index 0a0eba2..4d37af8 100644 --- a/rust/client/src/ui/theme/container.rs +++ b/rust/client/src/ui/theme/container.rs @@ -1,7 +1,17 @@ -use iced::{Border, Color, Length, Renderer, Shadow, Vector}; -use iced::widget::{Container, container}; +use iced::widget::container; use iced::widget::container::Style; -use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; +use iced::widget::Container; +use iced::Border; +use iced::Color; +use iced::Length; +use iced::Renderer; +use iced::Shadow; +use iced::Vector; + +use crate::ui::theme::get_theme; +use crate::ui::theme::Element; +use crate::ui::theme::GauntletComplexTheme; +use crate::ui::theme::ThemableWidget; pub enum ContainerStyle { ActionPanel, @@ -78,7 +88,6 @@ pub enum ContainerStyleInner { Hud, } - impl container::Catalog for GauntletComplexTheme { type Class<'a> = ContainerStyleInner; @@ -110,7 +119,6 @@ impl container::Catalog for GauntletComplexTheme { } } ContainerStyleInner::ActionShortcutModifier => { - let theme = &self.action_shortcut_modifier; let background_color = &theme.background_color; let border_color = &theme.border_color; @@ -209,7 +217,12 @@ impl container::Catalog for GauntletComplexTheme { Style { background: Some(panel_theme.background_color.into()), border: Border { - radius: gauntlet_common_ui::radius(0.0, 0.0, root_theme.border_radius, root_theme.border_radius), + radius: gauntlet_common_ui::radius( + 0.0, + 0.0, + root_theme.border_radius, + root_theme.border_radius, + ), width: root_theme.border_width, color: root_theme.border_color, }, @@ -255,15 +268,9 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau let theme = get_theme(); match name { - ContainerStyle::RootInner => { - self.padding(0.0) - } - ContainerStyle::ActionPanelTitle => { - self.padding(theme.action_panel_title.padding.to_iced()) - } - ContainerStyle::ActionSectionTitle => { - self.padding(theme.action_section_title.padding.to_iced()) - } + ContainerStyle::RootInner => self.padding(0.0), + ContainerStyle::ActionPanelTitle => self.padding(theme.action_panel_title.padding.to_iced()), + ContainerStyle::ActionSectionTitle => self.padding(theme.action_section_title.padding.to_iced()), ContainerStyle::ActionShortcutModifier => { self.class(ContainerStyleInner::ActionShortcutModifier) .padding(theme.action_shortcut_modifier.padding.to_iced()) @@ -278,18 +285,10 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau .height(Length::Fixed(250.0)) .width(Length::Fixed(350.0)) } - ContainerStyle::MetadataTagItem => { - self.padding(theme.metadata_tag_item.padding.to_iced()) - } - ContainerStyle::MetadataItemLabel => { - self.padding(theme.metadata_item_label.padding.to_iced()) - } - ContainerStyle::MetadataLinkIcon => { - self.padding(theme.metadata_link_icon.padding.to_iced()) - } - ContainerStyle::MetadataItemValue => { - self.padding(theme.metadata_item_value.padding.to_iced()) - } + ContainerStyle::MetadataTagItem => self.padding(theme.metadata_tag_item.padding.to_iced()), + ContainerStyle::MetadataItemLabel => self.padding(theme.metadata_item_label.padding.to_iced()), + ContainerStyle::MetadataLinkIcon => self.padding(theme.metadata_link_icon.padding.to_iced()), + ContainerStyle::MetadataItemValue => self.padding(theme.metadata_item_value.padding.to_iced()), ContainerStyle::MetadataItemValueInList => { self.padding(theme.metadata_item_value_in_list.padding.to_iced()) } @@ -297,27 +296,13 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau self.class(ContainerStyleInner::RootBottomPanel) .padding(theme.root_bottom_panel.padding.to_iced()) } - ContainerStyle::RootTopPanel => { - self.padding(theme.root_top_panel.padding.to_iced()) - } - ContainerStyle::ListItemSubtitle => { - self.padding(theme.list_item_subtitle.padding.to_iced()) - } - ContainerStyle::ListItemTitle => { - self.padding(theme.list_item_title.padding.to_iced()) - } - ContainerStyle::ListItemIcon => { - self.padding(theme.list_item_icon.padding.to_iced()) - } - ContainerStyle::ContentParagraph => { - self.padding(theme.content_paragraph.padding.to_iced()) - } - ContainerStyle::ContentHorizontalBreak => { - self.padding(theme.content_horizontal_break.padding.to_iced()) - } - ContainerStyle::ContentCodeBlock => { - self.padding(theme.content_code_block.padding.to_iced()) - } + ContainerStyle::RootTopPanel => self.padding(theme.root_top_panel.padding.to_iced()), + ContainerStyle::ListItemSubtitle => self.padding(theme.list_item_subtitle.padding.to_iced()), + ContainerStyle::ListItemTitle => self.padding(theme.list_item_title.padding.to_iced()), + ContainerStyle::ListItemIcon => self.padding(theme.list_item_icon.padding.to_iced()), + ContainerStyle::ContentParagraph => self.padding(theme.content_paragraph.padding.to_iced()), + ContainerStyle::ContentHorizontalBreak => self.padding(theme.content_horizontal_break.padding.to_iced()), + ContainerStyle::ContentCodeBlock => self.padding(theme.content_code_block.padding.to_iced()), ContainerStyle::ContentCodeBlockText => { self.class(ContainerStyleInner::ContentCodeBlockText) .padding(theme.content_code_block_text.padding.to_iced()) @@ -326,93 +311,46 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau self.class(ContainerStyleInner::ContentImage) .padding(theme.content_image.padding.to_iced()) } - ContainerStyle::DetailContentInner => { - self.padding(theme.metadata_content_inner.padding.to_iced()) - } - ContainerStyle::MetadataInner => { - self.padding(theme.metadata_inner.padding.to_iced()) - } - ContainerStyle::MetadataSeparator => { - self.padding(theme.metadata_separator.padding.to_iced()) - } - ContainerStyle::DetailMetadata => { - self.padding(theme.detail_metadata.padding.to_iced()) - } - ContainerStyle::DetailContent => { - self.padding(theme.detail_content.padding.to_iced()) - } - ContainerStyle::FormInputLabel => { - self.padding(theme.form_input_label.padding.to_iced()) - } - ContainerStyle::Inline => { - self.padding(theme.inline.padding.to_iced()) - } + ContainerStyle::DetailContentInner => self.padding(theme.metadata_content_inner.padding.to_iced()), + ContainerStyle::MetadataInner => self.padding(theme.metadata_inner.padding.to_iced()), + ContainerStyle::MetadataSeparator => self.padding(theme.metadata_separator.padding.to_iced()), + ContainerStyle::DetailMetadata => self.padding(theme.detail_metadata.padding.to_iced()), + ContainerStyle::DetailContent => self.padding(theme.detail_content.padding.to_iced()), + ContainerStyle::FormInputLabel => self.padding(theme.form_input_label.padding.to_iced()), + ContainerStyle::Inline => self.padding(theme.inline.padding.to_iced()), ContainerStyle::InlineInner => { - self - .height(120) + self.height(120) .max_height(120) .padding(theme.inline_inner.padding.to_iced()) .class(ContainerStyleInner::InlineInner) } - ContainerStyle::InlineName => { - self.padding(theme.inline_name.padding.to_iced()) - } + ContainerStyle::InlineName => self.padding(theme.inline_name.padding.to_iced()), ContainerStyle::EmptyViewImage => { self.padding(theme.empty_view_image.padding.to_iced()) .max_width(theme.empty_view_image.size.width) .max_height(theme.empty_view_image.size.height) } - ContainerStyle::Main => { - self.class(ContainerStyleInner::Main) - } - ContainerStyle::MainListItemText => { - self.padding(theme.main_list_item_text.padding.to_iced()) - } - ContainerStyle::MainListItemSubText => { - self.padding(theme.main_list_item_sub_text.padding.to_iced()) - } - ContainerStyle::MainListItemIcon => { - self.padding(theme.main_list_item_icon.padding.to_iced()) - } - ContainerStyle::MainList => { - self.padding(theme.main_list.padding.to_iced()) - } - ContainerStyle::MainListInner => { - self.padding(theme.main_list_inner.padding.to_iced()) - } - ContainerStyle::MainSearchBar => { - self.padding(theme.main_search_bar.padding.to_iced()) - } - ContainerStyle::Root => { - self.class(ContainerStyleInner::Root) - } - ContainerStyle::PluginErrorViewTitle => { - self.padding(theme.plugin_error_view_title.padding.to_iced()) - } + ContainerStyle::Main => self.class(ContainerStyleInner::Main), + ContainerStyle::MainListItemText => self.padding(theme.main_list_item_text.padding.to_iced()), + ContainerStyle::MainListItemSubText => self.padding(theme.main_list_item_sub_text.padding.to_iced()), + ContainerStyle::MainListItemIcon => self.padding(theme.main_list_item_icon.padding.to_iced()), + ContainerStyle::MainList => self.padding(theme.main_list.padding.to_iced()), + ContainerStyle::MainListInner => self.padding(theme.main_list_inner.padding.to_iced()), + ContainerStyle::MainSearchBar => self.padding(theme.main_search_bar.padding.to_iced()), + ContainerStyle::Root => self.class(ContainerStyleInner::Root), + ContainerStyle::PluginErrorViewTitle => self.padding(theme.plugin_error_view_title.padding.to_iced()), ContainerStyle::PluginErrorViewDescription => { self.padding(theme.plugin_error_view_description.padding.to_iced()) } ContainerStyle::PreferenceRequiredViewDescription => { self.padding(theme.preference_required_view_description.padding.to_iced()) } - ContainerStyle::Form => { - self.padding(theme.form.padding.to_iced()) - } - ContainerStyle::FormInner => { - self.padding(theme.form_inner.padding.to_iced()) - } - ContainerStyle::GridInner => { - self.padding(theme.grid_inner.padding.to_iced()) - } - ContainerStyle::Grid => { - self.padding(theme.grid.padding.to_iced()) - } - ContainerStyle::List => { - self.padding(theme.list.padding.to_iced()) - } - ContainerStyle::ListInner => { - self.padding(theme.list_inner.padding.to_iced()) - } + ContainerStyle::Form => self.padding(theme.form.padding.to_iced()), + ContainerStyle::FormInner => self.padding(theme.form_inner.padding.to_iced()), + ContainerStyle::GridInner => self.padding(theme.grid_inner.padding.to_iced()), + ContainerStyle::Grid => self.padding(theme.grid.padding.to_iced()), + ContainerStyle::List => self.padding(theme.list.padding.to_iced()), + ContainerStyle::ListInner => self.padding(theme.list_inner.padding.to_iced()), ContainerStyle::RootBottomPanelActionToggleText => { self.padding(theme.root_bottom_panel_action_toggle_text.padding.to_iced()) } @@ -420,25 +358,22 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Container<'a, Message, Gau self.padding(theme.root_bottom_panel_primary_action_text.padding.to_iced()) } ContainerStyle::RootBottomPanelPrimaryActionButton => { - self.padding(gauntlet_common_ui::padding(0.0, theme.root_bottom_panel.spacing, 0.0, 0.0)) - } - ContainerStyle::TextAccessory => { - self.padding(theme.text_accessory.padding.to_iced()) + self.padding(gauntlet_common_ui::padding( + 0.0, + theme.root_bottom_panel.spacing, + 0.0, + 0.0, + )) } + ContainerStyle::TextAccessory => self.padding(theme.text_accessory.padding.to_iced()), ContainerStyle::TextAccessoryIcon => { let horizontal_spacing = theme.text_accessory.spacing; self.padding(gauntlet_common_ui::padding(0.0, horizontal_spacing, 0.0, 0.0)) } - ContainerStyle::IconAccessory => { - self.padding(theme.icon_accessory.padding.to_iced()) - } - ContainerStyle::HudInner => { - self.padding(theme.hud_content.padding.to_iced()) - } - ContainerStyle::Hud => { - self.class(ContainerStyleInner::Hud) - } - }.into() + ContainerStyle::IconAccessory => self.padding(theme.icon_accessory.padding.to_iced()), + ContainerStyle::HudInner => self.padding(theme.hud_content.padding.to_iced()), + ContainerStyle::Hud => self.class(ContainerStyleInner::Hud), + } + .into() } } - diff --git a/rust/client/src/ui/theme/date_picker.rs b/rust/client/src/ui/theme/date_picker.rs index 5f21b0a..89ed158 100644 --- a/rust/client/src/ui/theme/date_picker.rs +++ b/rust/client/src/ui/theme/date_picker.rs @@ -1,10 +1,13 @@ -use crate::ui::theme::{Element, GauntletComplexTheme, ThemableWidget}; use iced::Color; use iced_aw::date_picker::Style; use iced_aw::style::date_picker::Catalog; use iced_aw::style::Status; use iced_aw::DatePicker; +use crate::ui::theme::Element; +use crate::ui::theme::GauntletComplexTheme; +use crate::ui::theme::ThemableWidget; + #[derive(Clone, Default)] pub enum DatePickerStyle { #[default] @@ -22,15 +25,14 @@ impl Catalog for GauntletComplexTheme { match status { Status::Active => active(self), Status::Hovered => hovered(self), - Status::Pressed => hovered(self), // TODO proper styling + Status::Pressed => hovered(self), // TODO proper styling Status::Disabled => hovered(self), // TODO proper styling Status::Focused => focused(self), - Status::Selected => selected(self) + Status::Selected => selected(self), } } } - fn active(theme: &GauntletComplexTheme) -> Style { let root_theme = &theme.popup; let theme = &theme.form_input_date_picker; @@ -73,11 +75,10 @@ fn focused(theme: &GauntletComplexTheme) -> Style { } } - impl<'a, Message: 'a + Clone + 'static> ThemableWidget<'a, Message> for DatePicker<'a, Message, GauntletComplexTheme> { type Kind = DatePickerStyle; fn themed(self, kind: DatePickerStyle) -> Element<'a, Message> { self.class(kind).into() } -} \ No newline at end of file +} diff --git a/rust/client/src/ui/theme/grid.rs b/rust/client/src/ui/theme/grid.rs index b4bec5c..75215cd 100644 --- a/rust/client/src/ui/theme/grid.rs +++ b/rust/client/src/ui/theme/grid.rs @@ -1,6 +1,10 @@ use iced::Renderer; use iced_aw::Grid; -use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; + +use crate::ui::theme::get_theme; +use crate::ui::theme::Element; +use crate::ui::theme::GauntletComplexTheme; +use crate::ui::theme::ThemableWidget; pub enum GridStyle { Default, @@ -13,9 +17,8 @@ impl<'a, Message: 'a + 'static> ThemableWidget<'a, Message> for Grid<'a, Message let theme = get_theme(); match kind { - GridStyle::Default => { - self.spacing(theme.grid.spacing) - } - }.into() + GridStyle::Default => self.spacing(theme.grid.spacing), + } + .into() } } diff --git a/rust/client/src/ui/theme/image.rs b/rust/client/src/ui/theme/image.rs index 96416f4..1a3d7da 100644 --- a/rust/client/src/ui/theme/image.rs +++ b/rust/client/src/ui/theme/image.rs @@ -1,6 +1,8 @@ -use crate::ui::theme::{Element, ThemableWidget}; use iced::widget::Image; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; + pub enum ImageStyle { MainListItemIcon, } @@ -10,10 +12,8 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Image Element<'a, Message> { match kind { - ImageStyle::MainListItemIcon => { - self.width(18) - .height(18) - } - }.into() + ImageStyle::MainListItemIcon => self.width(18).height(18), + } + .into() } } diff --git a/rust/client/src/ui/theme/loading_bar.rs b/rust/client/src/ui/theme/loading_bar.rs index 3fb85c1..08bd4d0 100644 --- a/rust/client/src/ui/theme/loading_bar.rs +++ b/rust/client/src/ui/theme/loading_bar.rs @@ -1,6 +1,8 @@ use crate::ui::custom_widgets::loading_bar; -use crate::ui::custom_widgets::loading_bar::{LoadingBar, Style}; -use crate::ui::theme::{Element, ThemableWidget}; +use crate::ui::custom_widgets::loading_bar::LoadingBar; +use crate::ui::custom_widgets::loading_bar::Style; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; use crate::ui::GauntletComplexTheme; #[derive(Default)] diff --git a/rust/client/src/ui/theme/mod.rs b/rust/client/src/ui/theme/mod.rs index 31d2d95..7198f99 100644 --- a/rust/client/src/ui/theme/mod.rs +++ b/rust/client/src/ui/theme/mod.rs @@ -1,24 +1,30 @@ -use arc_swap::{ArcSwap, Guard}; -use gauntlet_common::model::{UiTheme, UiThemeColor, UiThemeMode}; -use iced::application::DefaultStyle; -use iced::{application, Color, Padding}; use std::sync::Arc; +use arc_swap::ArcSwap; +use arc_swap::Guard; +use gauntlet_common::model::UiTheme; +use gauntlet_common::model::UiThemeColor; +use gauntlet_common::model::UiThemeMode; +use iced::application; +use iced::application::DefaultStyle; +use iced::Color; +use iced::Padding; + pub mod button; -pub mod text_input; -pub mod row; -pub mod container; -pub mod text; -pub mod date_picker; -pub mod image; -pub mod pick_list; pub mod checkbox; -pub mod scrollable; -pub mod rule; -pub mod space; +pub mod container; +pub mod date_picker; pub mod grid; -pub mod tooltip; +pub mod image; mod loading_bar; +pub mod pick_list; +pub mod row; +pub mod rule; +pub mod scrollable; +pub mod space; +pub mod text; +pub mod text_input; +pub mod tooltip; pub type Element<'a, Message> = iced::Element<'a, Message, GauntletComplexTheme>; @@ -104,7 +110,7 @@ pub struct GauntletComplexTheme { text_accessory: ThemePaddingTextColorSpacing, icon_accessory: ThemeIconAccessory, hud: ThemeRoot, - hud_content: ThemePaddingOnly + hud_content: ThemePaddingOnly, } impl Default for GauntletComplexTheme { @@ -130,7 +136,7 @@ impl GauntletComplexTheme { background, text, window, - content + content, } = simple_theme; let [background_100, background_200, background_300, background_400] = background; @@ -144,13 +150,13 @@ impl GauntletComplexTheme { to_iced(&background_100), to_iced(&background_200), to_iced(&background_300), - to_iced(&background_400) + to_iced(&background_400), ]; let [text_100, text_200, text_300, _text_400] = [ to_iced(&text_100), to_iced(&text_200), to_iced(&text_300), - to_iced(&text_400) + to_iced(&text_400), ]; GauntletComplexTheme { @@ -200,7 +206,7 @@ impl GauntletComplexTheme { border_color: Color::TRANSPARENT, }, action_shortcut: ThemePaddingOnly { - padding: padding_all(0.0) + padding: padding_all(0.0), }, action_shortcut_modifier: ThemeActionShortcutModifier { padding: padding_axis(0.0, 8.0), @@ -211,7 +217,7 @@ impl GauntletComplexTheme { border_color: Color::TRANSPARENT, }, form_input: ThemePaddingOnly { - padding: padding_all(8.0) + padding: padding_all(8.0), }, metadata_tag_item: ThemePaddingOnly { padding: padding(0.0, 8.0, 4.0, 0.0), @@ -220,15 +226,15 @@ impl GauntletComplexTheme { padding: padding_axis(2.0, 8.0), background_color: match mode { UiThemeMode::Light => background_300, - UiThemeMode::Dark => background_200 + UiThemeMode::Dark => background_200, }, background_color_focused: match mode { UiThemeMode::Light => background_200, - UiThemeMode::Dark => background_100 + UiThemeMode::Dark => background_100, }, background_color_hovered: match mode { UiThemeMode::Light => background_200, - UiThemeMode::Dark => background_100 + UiThemeMode::Dark => background_100, }, text_color: text_100, text_color_hovered: text_100, @@ -258,7 +264,7 @@ impl GauntletComplexTheme { padding: padding_all(4.0), }, content_paragraph: ThemePaddingOnly { - padding: padding_all(8.0) + padding: padding_all(8.0), }, content_code_block: ThemePaddingOnly { padding: padding_all(0.0), @@ -274,9 +280,7 @@ impl GauntletComplexTheme { padding: padding_all(8.0), text_color: text_200, }, - inline_separator: ThemeTextColor { - text_color: text_200, - }, + inline_separator: ThemeTextColor { text_color: text_200 }, inline_inner: ThemeInline { padding: padding_all(8.0), background_color: background_200, @@ -306,9 +310,7 @@ impl GauntletComplexTheme { padding: padding_axis(4.0, 0.0), text_color: text_100, }, - grid_item_subtitle: ThemeTextColor { - text_color: text_200, - }, + grid_item_subtitle: ThemeTextColor { text_color: text_200 }, content_horizontal_break: ThemePaddingOnly { padding: padding_axis(8.0, 0.0), }, @@ -330,15 +332,15 @@ impl GauntletComplexTheme { padding: padding_axis(3.0, 5.0), background_color: match mode { UiThemeMode::Light => background_300, - UiThemeMode::Dark => background_200 + UiThemeMode::Dark => background_200, }, background_color_focused: match mode { UiThemeMode::Light => background_200, - UiThemeMode::Dark => background_100 + UiThemeMode::Dark => background_100, }, background_color_hovered: match mode { UiThemeMode::Light => background_200, - UiThemeMode::Dark => background_100 + UiThemeMode::Dark => background_100, }, text_color: text_100, text_color_hovered: text_100, @@ -349,7 +351,7 @@ impl GauntletComplexTheme { root_bottom_panel: ThemeBottomPanel { padding: padding_axis(6.0, 8.0), background_color: background_300, - spacing: 8.0 + spacing: 8.0, }, root_bottom_panel_action_toggle_button: ThemeButton { padding: padding_axis(3.0, 5.0), @@ -364,11 +366,11 @@ impl GauntletComplexTheme { }, root_bottom_panel_action_toggle_text: ThemePaddingTextColor { padding: padding(0.0, 8.0, 0.0, 4.0), - text_color: text_200 + text_color: text_200, }, root_bottom_panel_primary_action_text: ThemePaddingTextColor { padding: padding(0.0, 8.0, 0.0, 4.0), - text_color: text_100 + text_color: text_100, }, list_item: ThemeButton { padding: padding_all(5.0), @@ -382,7 +384,7 @@ impl GauntletComplexTheme { border_color: Color::TRANSPARENT, }, list_item_icon: ThemePaddingOnly { - padding: padding_axis(0.0, 4.0) + padding: padding_axis(0.0, 4.0), }, detail_metadata: ThemePaddingOnly { padding: padding_axis(0.0, 12.0), @@ -423,27 +425,23 @@ impl GauntletComplexTheme { text_color: text_200, spacing: 8.0, }, - list_section_subtitle: ThemeTextColor { - text_color: text_300 - }, + list_section_subtitle: ThemeTextColor { text_color: text_300 }, grid_section_title: ThemePaddingTextColorSpacing { padding: padding(12.0, 0.0, 4.0, 0.0), text_color: text_200, spacing: 8.0, }, - grid_section_subtitle: ThemeTextColor { - text_color: text_300 - }, + grid_section_subtitle: ThemeTextColor { text_color: text_300 }, main_list_item: ThemeButton { padding: padding_all(5.0), background_color: Color::TRANSPARENT, background_color_focused: match mode { UiThemeMode::Light => background_300, - UiThemeMode::Dark => background_200 + UiThemeMode::Dark => background_200, }, background_color_hovered: match mode { UiThemeMode::Light => background_200, - UiThemeMode::Dark => background_300 + UiThemeMode::Dark => background_300, }, text_color: text_100, text_color_hovered: text_100, @@ -483,9 +481,7 @@ impl GauntletComplexTheme { text_color: text_100, text_color_hovered: text_200, }, - empty_view_subtitle: ThemeTextColor { - text_color: text_300, - }, + empty_view_subtitle: ThemeTextColor { text_color: text_300 }, form_input_date_picker: ThemeDatePicker { background_color: background_400, text_color: text_100, @@ -546,9 +542,7 @@ impl GauntletComplexTheme { border_color: background_200, border_color_hovered: background_200, }, - separator: ThemeSeparator { - color: background_200 - }, + separator: ThemeSeparator { color: background_200 }, scrollbar: ThemeScrollbar { color: background_200, border_radius: content.border.radius, @@ -573,7 +567,7 @@ impl GauntletComplexTheme { icon_color: text_200, }, hud: ThemeRoot { - background_color: Color::from_rgba8(30,30,30, 0.7), + background_color: Color::from_rgba8(30, 30, 30, 0.7), border_radius: 30.0, border_width: 0.0, border_color: Color::TRANSPARENT, @@ -590,14 +584,14 @@ fn init_theme(theme: GauntletComplexTheme) { } fn set_theme(theme: GauntletComplexTheme) { - THEME.get().expect("theme global var was not set").store(Arc::new(theme)) -} - -fn get_theme() -> Guard> { THEME .get() .expect("theme global var was not set") - .load() + .store(Arc::new(theme)) +} + +fn get_theme() -> Guard> { + THEME.get().expect("theme global var was not set").load() } static THEME: once_cell::sync::OnceCell> = once_cell::sync::OnceCell::new(); @@ -614,16 +608,11 @@ const fn padding(top: f32, right: f32, bottom: f32, left: f32) -> ThemePadding { } const fn padding_all(value: f32) -> ThemePadding { - ThemePadding::All { - all: value - } + ThemePadding::All { all: value } } const fn padding_axis(vertical: f32, horizontal: f32) -> ThemePadding { - ThemePadding::Axis { - vertical, - horizontal, - } + ThemePadding::Axis { vertical, horizontal } } #[derive(Debug, Clone)] @@ -651,7 +640,7 @@ pub struct ThemeCheckbox { border_width: f32, border_color: Color, - icon_color: Color + icon_color: Color, } #[derive(Debug, Clone)] @@ -769,7 +758,7 @@ pub struct ThemeDatePicker { day_background_color: Color, day_background_color_selected: Color, - day_background_color_hovered: Color + day_background_color_hovered: Color, } #[derive(Debug, Clone)] @@ -878,7 +867,12 @@ pub enum ThemePadding { impl ThemePadding { fn to_iced(&self) -> Padding { match self { - ThemePadding::Each { top, right, bottom, left } => { + ThemePadding::Each { + top, + right, + bottom, + left, + } => { Padding { top: *top, right: *right, @@ -934,4 +928,3 @@ impl iced_layershell::DefaultStyle for GauntletComplexTheme { } } } - diff --git a/rust/client/src/ui/theme/pick_list.rs b/rust/client/src/ui/theme/pick_list.rs index 09be609..f1dfa51 100644 --- a/rust/client/src/ui/theme/pick_list.rs +++ b/rust/client/src/ui/theme/pick_list.rs @@ -1,9 +1,16 @@ use std::borrow::Borrow; -use iced::{Border, overlay}; -use iced::widget::{pick_list, PickList}; +use iced::overlay; +use iced::widget::pick_list; use iced::widget::pick_list::Status; -use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, NOT_INTENDED_TO_BE_USED, ThemableWidget}; +use iced::widget::PickList; +use iced::Border; + +use crate::ui::theme::get_theme; +use crate::ui::theme::Element; +use crate::ui::theme::GauntletComplexTheme; +use crate::ui::theme::ThemableWidget; +use crate::ui::theme::NOT_INTENDED_TO_BE_USED; #[derive(Clone, Default)] pub enum PickListStyle { @@ -76,7 +83,8 @@ impl overlay::menu::Catalog for GauntletComplexTheme { } } -impl<'a, Message: 'a + Clone + 'static, T, L, V> ThemableWidget<'a, Message> for PickList<'a, T, L, V, Message, GauntletComplexTheme> +impl<'a, Message: 'a + Clone + 'static, T, L, V> ThemableWidget<'a, Message> + for PickList<'a, T, L, V, Message, GauntletComplexTheme> where T: ToString + PartialEq + Clone + 'a, L: Borrow<[T]> + 'a, @@ -89,4 +97,4 @@ where // .padding() // TODO .into() } -} \ No newline at end of file +} diff --git a/rust/client/src/ui/theme/row.rs b/rust/client/src/ui/theme/row.rs index 57c2c0a..3c06aea 100644 --- a/rust/client/src/ui/theme/row.rs +++ b/rust/client/src/ui/theme/row.rs @@ -1,6 +1,11 @@ -use crate::ui::theme::{get_theme, Element, GauntletComplexTheme, ThemableWidget}; use iced::widget::Row; -use iced::{Padding, Renderer}; +use iced::Padding; +use iced::Renderer; + +use crate::ui::theme::get_theme; +use crate::ui::theme::Element; +use crate::ui::theme::GauntletComplexTheme; +use crate::ui::theme::ThemableWidget; pub enum RowStyle { ActionShortcut, @@ -21,12 +26,8 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletC let theme = get_theme(); match name { - RowStyle::ActionShortcut => { - self.padding(theme.action_shortcut.padding.to_iced()) - } - RowStyle::FormInput => { - self.padding(theme.form_input.padding.to_iced()) - } + RowStyle::ActionShortcut => self.padding(theme.action_shortcut.padding.to_iced()), + RowStyle::FormInput => self.padding(theme.form_input.padding.to_iced()), RowStyle::ListSectionTitle => { self.padding(theme.list_section_title.padding.to_iced()) .spacing(theme.list_section_title.spacing) @@ -37,23 +38,28 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Row<'a, Message, GauntletC } RowStyle::ListFirstSectionTitle => { let padding = theme.list_section_title.padding.to_iced(); - self.padding(gauntlet_common_ui::padding(padding.bottom, padding.right, padding.bottom, padding.left)) - .spacing(theme.list_section_title.spacing) + self.padding(gauntlet_common_ui::padding( + padding.bottom, + padding.right, + padding.bottom, + padding.left, + )) + .spacing(theme.list_section_title.spacing) } RowStyle::GridFirstSectionTitle => { let padding = theme.grid_section_title.padding.to_iced(); - self.padding(gauntlet_common_ui::padding(0.0, padding.right, padding.bottom, padding.left)) - .spacing(theme.grid_section_title.spacing) + self.padding(gauntlet_common_ui::padding( + 0.0, + padding.right, + padding.bottom, + padding.left, + )) + .spacing(theme.grid_section_title.spacing) } - RowStyle::GridItemTitle => { - self.padding(theme.grid_item_title.padding.to_iced()) - } - RowStyle::RootBottomPanel => { - self.spacing(theme.root_bottom_panel.spacing) - } - RowStyle::RootTopPanel => { - self.spacing(theme.root_top_panel.spacing) - } - }.into() + RowStyle::GridItemTitle => self.padding(theme.grid_item_title.padding.to_iced()), + RowStyle::RootBottomPanel => self.spacing(theme.root_bottom_panel.spacing), + RowStyle::RootTopPanel => self.spacing(theme.root_top_panel.spacing), + } + .into() } -} \ No newline at end of file +} diff --git a/rust/client/src/ui/theme/rule.rs b/rust/client/src/ui/theme/rule.rs index 117d548..a880837 100644 --- a/rust/client/src/ui/theme/rule.rs +++ b/rust/client/src/ui/theme/rule.rs @@ -1,7 +1,10 @@ +use iced::widget::rule; use iced::widget::rule::Style; -use iced::widget::{rule, Rule}; +use iced::widget::Rule; -use crate::ui::theme::{Element, GauntletComplexTheme, ThemableWidget}; +use crate::ui::theme::Element; +use crate::ui::theme::GauntletComplexTheme; +use crate::ui::theme::ThemableWidget; pub enum RuleStyle { Default, diff --git a/rust/client/src/ui/theme/scrollable.rs b/rust/client/src/ui/theme/scrollable.rs index ec84ee4..4d736c9 100644 --- a/rust/client/src/ui/theme/scrollable.rs +++ b/rust/client/src/ui/theme/scrollable.rs @@ -1,8 +1,12 @@ -use iced::{Border, Color}; -use iced::widget::{container, scrollable}; -use iced::widget::scrollable::{Status, Style}; +use iced::widget::container; +use iced::widget::scrollable; +use iced::widget::scrollable::Status; +use iced::widget::scrollable::Style; +use iced::Border; +use iced::Color; -use crate::ui::theme::{GauntletComplexTheme, get_theme}; +use crate::ui::theme::get_theme; +use crate::ui::theme::GauntletComplexTheme; impl scrollable::Catalog for GauntletComplexTheme { type Class<'a> = (); @@ -29,12 +33,14 @@ impl scrollable::Catalog for GauntletComplexTheme { }; match status { - Status::Active => Style { - container: container::Style::default(), - vertical_rail: scrollbar, - horizontal_rail: scrollbar, - gap: None, - }, + Status::Active => { + Style { + container: container::Style::default(), + vertical_rail: scrollbar, + horizontal_rail: scrollbar, + gap: None, + } + } Status::Hovered { is_horizontal_scrollbar_hovered, is_vertical_scrollbar_hovered, diff --git a/rust/client/src/ui/theme/space.rs b/rust/client/src/ui/theme/space.rs index da22396..8564b44 100644 --- a/rust/client/src/ui/theme/space.rs +++ b/rust/client/src/ui/theme/space.rs @@ -1,6 +1,7 @@ use iced::widget::Space; -use crate::ui::theme::{Element, ThemableWidget}; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; pub enum ThemeKindSpace { MainListItemIcon, @@ -11,11 +12,8 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Space { fn themed(self, kind: ThemeKindSpace) -> Element<'a, Message> { match kind { - ThemeKindSpace::MainListItemIcon => { - self.width(18) - .height(18) - } - }.into() + ThemeKindSpace::MainListItemIcon => self.width(18).height(18), + } + .into() } } - diff --git a/rust/client/src/ui/theme/text.rs b/rust/client/src/ui/theme/text.rs index 1ac925c..e681380 100644 --- a/rust/client/src/ui/theme/text.rs +++ b/rust/client/src/ui/theme/text.rs @@ -1,7 +1,12 @@ -use iced::Renderer; -use iced::widget::{Text, text}; +use iced::widget::text; use iced::widget::text::Style; -use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; +use iced::widget::Text; +use iced::Renderer; + +use crate::ui::theme::get_theme; +use crate::ui::theme::Element; +use crate::ui::theme::GauntletComplexTheme; +use crate::ui::theme::ThemableWidget; #[derive(Clone, Default)] pub enum TextStyle { @@ -35,19 +40,10 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Text<'a, GauntletComplexTh TextStyle::MetadataItemLabel => { let theme = get_theme(); - self.class(kind) - .size(theme.metadata_item_label.text_size) - .into() - } - TextStyle::InlineName => { - self.size(15) - .class(kind) - .into() - } - _ => { - self.class(kind) - .into() + self.class(kind).size(theme.metadata_item_label.text_size).into() } + TextStyle::InlineName => self.size(15).class(kind).into(), + _ => self.class(kind).into(), } } } @@ -62,57 +58,91 @@ impl text::Catalog for GauntletComplexTheme { fn style(&self, class: &Self::Class<'_>) -> Style { match class { TextStyle::Default => Default::default(), - TextStyle::ActionSectionTitle => Style { - color: Some(self.action_section_title.text_color), - }, - TextStyle::EmptyViewSubtitle => Style { - color: Some(self.empty_view_subtitle.text_color), - }, - TextStyle::ListItemSubtitle => Style { - color: Some(self.list_item_subtitle.text_color), - }, - TextStyle::ListSectionTitle => Style { - color: Some(self.list_section_title.text_color), - }, - TextStyle::ListSectionSubtitle => Style { - color: Some(self.list_section_subtitle.text_color), - }, - TextStyle::GridSectionTitle => Style { - color: Some(self.grid_section_title.text_color), - }, - TextStyle::GridSectionSubtitle => Style{ - color: Some(self.grid_section_subtitle.text_color), - }, - TextStyle::MainListItemSubtext => Style { - color: Some(self.main_list_item_sub_text.text_color), - }, - TextStyle::MetadataItemLabel => Style { - color: Some(self.metadata_item_label.text_color), - }, - TextStyle::TextAccessory => Style { - color: Some(self.text_accessory.text_color), - }, - TextStyle::IconAccessory => Style { - color: Some(self.icon_accessory.icon_color), - }, - TextStyle::GridItemTitle => Style { - color: Some(self.grid_item_title.text_color), - }, - TextStyle::GridItemSubTitle => Style { - color: Some(self.grid_item_subtitle.text_color), - }, - TextStyle::InlineName => Style { - color: Some(self.inline_name.text_color), - }, - TextStyle::InlineSeparator => Style { - color: Some(self.inline_separator.text_color), - }, - TextStyle::RootBottomPanelPrimaryActionText => Style { - color: Some(self.root_bottom_panel_primary_action_text.text_color), - }, - TextStyle::RootBottomPanelActionToggleText => Style { - color: Some(self.root_bottom_panel_action_toggle_text.text_color), + TextStyle::ActionSectionTitle => { + Style { + color: Some(self.action_section_title.text_color), + } + } + TextStyle::EmptyViewSubtitle => { + Style { + color: Some(self.empty_view_subtitle.text_color), + } + } + TextStyle::ListItemSubtitle => { + Style { + color: Some(self.list_item_subtitle.text_color), + } + } + TextStyle::ListSectionTitle => { + Style { + color: Some(self.list_section_title.text_color), + } + } + TextStyle::ListSectionSubtitle => { + Style { + color: Some(self.list_section_subtitle.text_color), + } + } + TextStyle::GridSectionTitle => { + Style { + color: Some(self.grid_section_title.text_color), + } + } + TextStyle::GridSectionSubtitle => { + Style { + color: Some(self.grid_section_subtitle.text_color), + } + } + TextStyle::MainListItemSubtext => { + Style { + color: Some(self.main_list_item_sub_text.text_color), + } + } + TextStyle::MetadataItemLabel => { + Style { + color: Some(self.metadata_item_label.text_color), + } + } + TextStyle::TextAccessory => { + Style { + color: Some(self.text_accessory.text_color), + } + } + TextStyle::IconAccessory => { + Style { + color: Some(self.icon_accessory.icon_color), + } + } + TextStyle::GridItemTitle => { + Style { + color: Some(self.grid_item_title.text_color), + } + } + TextStyle::GridItemSubTitle => { + Style { + color: Some(self.grid_item_subtitle.text_color), + } + } + TextStyle::InlineName => { + Style { + color: Some(self.inline_name.text_color), + } + } + TextStyle::InlineSeparator => { + Style { + color: Some(self.inline_separator.text_color), + } + } + TextStyle::RootBottomPanelPrimaryActionText => { + Style { + color: Some(self.root_bottom_panel_primary_action_text.text_color), + } + } + TextStyle::RootBottomPanelActionToggleText => { + Style { + color: Some(self.root_bottom_panel_action_toggle_text.text_color), + } } } } -} \ No newline at end of file +} diff --git a/rust/client/src/ui/theme/text_input.rs b/rust/client/src/ui/theme/text_input.rs index 75ea3b5..250105e 100644 --- a/rust/client/src/ui/theme/text_input.rs +++ b/rust/client/src/ui/theme/text_input.rs @@ -1,8 +1,15 @@ -use iced::widget::text_input::{Status, Style}; -use iced::widget::{text_input, TextInput}; -use iced::{Border, Color, Renderer}; +use iced::widget::text_input; +use iced::widget::text_input::Status; +use iced::widget::text_input::Style; +use iced::widget::TextInput; +use iced::Border; +use iced::Color; +use iced::Renderer; -use crate::ui::theme::{Element, GauntletComplexTheme, ThemableWidget, NOT_INTENDED_TO_BE_USED}; +use crate::ui::theme::Element; +use crate::ui::theme::GauntletComplexTheme; +use crate::ui::theme::ThemableWidget; +use crate::ui::theme::NOT_INTENDED_TO_BE_USED; pub enum TextInputStyle { ShouldNotBeUsed, @@ -43,7 +50,7 @@ fn active(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { value: NOT_INTENDED_TO_BE_USED, selection: NOT_INTENDED_TO_BE_USED, } - }, + } TextInputStyle::FormInput => { let theme = &theme.form_input_text_field; @@ -59,7 +66,7 @@ fn active(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { value: theme.text_color, selection: theme.selection_color, } - }, + } TextInputStyle::MainSearch | TextInputStyle::PluginSearchBar => { Style { background: Color::TRANSPARENT.into(), @@ -69,10 +76,10 @@ fn active(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { }, icon: NOT_INTENDED_TO_BE_USED, placeholder: theme.form_input_text_field.text_color_placeholder, // TODO fix - value: theme.form_input_text_field.text_color, // TODO fix - selection: theme.form_input_text_field.selection_color, // TODO fix + value: theme.form_input_text_field.text_color, // TODO fix + selection: theme.form_input_text_field.selection_color, // TODO fix } - }, + } } } @@ -90,7 +97,7 @@ fn focused(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { value: NOT_INTENDED_TO_BE_USED, selection: NOT_INTENDED_TO_BE_USED, } - }, + } TextInputStyle::FormInput => { let theme = &theme.form_input_text_field; @@ -106,7 +113,7 @@ fn focused(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { value: theme.text_color, selection: theme.selection_color, } - }, + } TextInputStyle::MainSearch | TextInputStyle::PluginSearchBar => { Style { background: Color::TRANSPARENT.into(), @@ -116,10 +123,10 @@ fn focused(theme: &GauntletComplexTheme, style: &TextInputStyle) -> Style { }, icon: NOT_INTENDED_TO_BE_USED, placeholder: theme.form_input_text_field.text_color_placeholder, // TODO fix - value: theme.form_input_text_field.text_color, // TODO fix - selection: theme.form_input_text_field.selection_color, // TODO fix + value: theme.form_input_text_field.text_color, // TODO fix + selection: theme.form_input_text_field.selection_color, // TODO fix } - }, + } } } @@ -143,11 +150,7 @@ impl<'a, Message: 'a + Clone> ThemableWidget<'a, Message> for TextInput<'a, Mess fn themed(self, kind: TextInputStyle) -> Element<'a, Message> { match kind { - TextInputStyle::PluginSearchBar => { - self.class(kind) - .padding(0) - .into() - } + TextInputStyle::PluginSearchBar => self.class(kind).padding(0).into(), _ => { self.class(kind) // .padding() // TODO @@ -155,4 +158,4 @@ impl<'a, Message: 'a + Clone> ThemableWidget<'a, Message> for TextInput<'a, Mess } } } -} \ No newline at end of file +} diff --git a/rust/client/src/ui/theme/tooltip.rs b/rust/client/src/ui/theme/tooltip.rs index 58f7f00..0551a4e 100644 --- a/rust/client/src/ui/theme/tooltip.rs +++ b/rust/client/src/ui/theme/tooltip.rs @@ -1,8 +1,11 @@ -use iced::Renderer; use iced::widget::Tooltip; +use iced::Renderer; -use crate::ui::theme::{Element, GauntletComplexTheme, get_theme, ThemableWidget}; use crate::ui::theme::container::ContainerStyleInner; +use crate::ui::theme::get_theme; +use crate::ui::theme::Element; +use crate::ui::theme::GauntletComplexTheme; +use crate::ui::theme::ThemableWidget; pub enum TooltipStyle { Tooltip, @@ -15,10 +18,8 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Tooltip<'a, Message, Gaunt let theme = get_theme(); match kind { - TooltipStyle::Tooltip => { - self.class(ContainerStyleInner::Tooltip) - .padding(theme.tooltip.padding) - } - }.into() + TooltipStyle::Tooltip => self.class(ContainerStyleInner::Tooltip).padding(theme.tooltip.padding), + } + .into() } } diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 0830856..d579f9a 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -1,7 +1,117 @@ +use std::cell::Cell; +use std::collections::HashMap; +use std::fmt::Debug; +use std::fmt::Display; +use std::sync::Arc; + +use gauntlet_common::model::ActionPanelSectionWidget; +use gauntlet_common::model::ActionPanelSectionWidgetOrderedMembers; +use gauntlet_common::model::ActionPanelWidget; +use gauntlet_common::model::ActionPanelWidgetOrderedMembers; +use gauntlet_common::model::ActionWidget; +use gauntlet_common::model::CheckboxWidget; +use gauntlet_common::model::CodeBlockWidget; +use gauntlet_common::model::ContentWidget; +use gauntlet_common::model::ContentWidgetOrderedMembers; +use gauntlet_common::model::DatePickerWidget; +use gauntlet_common::model::DetailWidget; +use gauntlet_common::model::EmptyViewWidget; +use gauntlet_common::model::FormWidget; +use gauntlet_common::model::FormWidgetOrderedMembers; +use gauntlet_common::model::GridItemWidget; +use gauntlet_common::model::GridSectionWidget; +use gauntlet_common::model::GridSectionWidgetOrderedMembers; +use gauntlet_common::model::GridWidget; +use gauntlet_common::model::GridWidgetOrderedMembers; +use gauntlet_common::model::H1Widget; +use gauntlet_common::model::H2Widget; +use gauntlet_common::model::H3Widget; +use gauntlet_common::model::H4Widget; +use gauntlet_common::model::H5Widget; +use gauntlet_common::model::H6Widget; +use gauntlet_common::model::HorizontalBreakWidget; +use gauntlet_common::model::IconAccessoryWidget; +use gauntlet_common::model::Icons; +use gauntlet_common::model::ImageLike; +use gauntlet_common::model::ImageWidget; +use gauntlet_common::model::InlineSeparatorWidget; +use gauntlet_common::model::InlineWidget; +use gauntlet_common::model::InlineWidgetOrderedMembers; +use gauntlet_common::model::ListItemAccessories; +use gauntlet_common::model::ListItemWidget; +use gauntlet_common::model::ListSectionWidget; +use gauntlet_common::model::ListSectionWidgetOrderedMembers; +use gauntlet_common::model::ListWidget; +use gauntlet_common::model::ListWidgetOrderedMembers; +use gauntlet_common::model::MetadataIconWidget; +use gauntlet_common::model::MetadataLinkWidget; +use gauntlet_common::model::MetadataSeparatorWidget; +use gauntlet_common::model::MetadataTagItemWidget; +use gauntlet_common::model::MetadataTagListWidget; +use gauntlet_common::model::MetadataTagListWidgetOrderedMembers; +use gauntlet_common::model::MetadataValueWidget; +use gauntlet_common::model::MetadataWidget; +use gauntlet_common::model::MetadataWidgetOrderedMembers; +use gauntlet_common::model::ParagraphWidget; +use gauntlet_common::model::PasswordFieldWidget; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::RootWidget; +use gauntlet_common::model::RootWidgetMembers; +use gauntlet_common::model::SearchBarWidget; +use gauntlet_common::model::SelectWidget; +use gauntlet_common::model::SelectWidgetOrderedMembers; +use gauntlet_common::model::SeparatorWidget; +use gauntlet_common::model::TextAccessoryWidget; +use gauntlet_common::model::TextFieldWidget; +use gauntlet_common::model::UiRenderLocation; +use gauntlet_common::model::UiWidgetId; +use gauntlet_common_ui::shortcut_to_text; +use iced::alignment::Horizontal; +use iced::alignment::Vertical; +use iced::font::Weight; +use iced::widget::button; +use iced::widget::checkbox; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_rule; +use iced::widget::horizontal_space; +use iced::widget::image; +use iced::widget::image::Handle; +use iced::widget::mouse_area; +use iced::widget::pick_list; +use iced::widget::row; +use iced::widget::scrollable; +use iced::widget::stack; +use iced::widget::text; +use iced::widget::text::Shaping; +use iced::widget::text_input; +use iced::widget::tooltip; +use iced::widget::tooltip::Position; +use iced::widget::value; +use iced::widget::vertical_rule; +use iced::widget::Space; +use iced::Alignment; +use iced::Font; +use iced::Length; +use iced::Task; +use iced_aw::date_picker::Date; +use iced_aw::helpers::date_picker; +use iced_aw::helpers::grid; +use iced_aw::helpers::grid_row; +use iced_aw::GridRow; +use iced_fonts::Bootstrap; +use iced_fonts::BOOTSTRAP_FONT; +use itertools::Itertools; + use crate::model::UiViewEvent; use crate::ui::custom_widgets::loading_bar::LoadingBar; -use crate::ui::grid_navigation::{grid_down_offset, grid_up_offset, GridSectionData}; -use crate::ui::scroll_handle::{ScrollHandle, ESTIMATED_MAIN_LIST_ITEM_HEIGHT}; +use crate::ui::grid_navigation::grid_down_offset; +use crate::ui::grid_navigation::grid_up_offset; +use crate::ui::grid_navigation::GridSectionData; +use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::scroll_handle::ESTIMATED_MAIN_LIST_ITEM_HEIGHT; use crate::ui::state::PluginViewState; use crate::ui::theme::button::ButtonStyle; use crate::ui::theme::container::ContainerStyle; @@ -13,26 +123,9 @@ use crate::ui::theme::rule::RuleStyle; use crate::ui::theme::text::TextStyle; use crate::ui::theme::text_input::TextInputStyle; use crate::ui::theme::tooltip::TooltipStyle; -use crate::ui::theme::{Element, ThemableWidget}; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; use crate::ui::AppMsg; -use gauntlet_common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, Icons, ImageLike, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PhysicalShortcut, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiRenderLocation, UiWidgetId}; -use gauntlet_common_ui::shortcut_to_text; -use iced::alignment::{Horizontal, Vertical}; -use iced::font::Weight; -use iced::widget::image::Handle; -use iced::widget::text::Shaping; -use iced::widget::tooltip::Position; -use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, mouse_area, pick_list, row, scrollable, stack, text, text_input, tooltip, value, vertical_rule, Space}; -use iced::{Alignment, Font, Length, Task}; -use iced_aw::date_picker::Date; -use iced_aw::helpers::{date_picker, grid, grid_row}; -use iced_aw::GridRow; -use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; -use itertools::Itertools; -use std::cell::Cell; -use std::collections::HashMap; -use std::fmt::{Debug, Display}; -use std::sync::Arc; #[derive(Debug)] pub struct ComponentWidgets<'b> { @@ -47,58 +140,73 @@ impl<'b> ComponentWidgets<'b> { root_widget: &'b Option>, state: &'b HashMap, plugin_id: PluginId, - images: &'b HashMap> + images: &'b HashMap>, ) -> ComponentWidgets<'b> { Self { root_widget, state, plugin_id, - images + images, } } fn text_field_state(&self, widget_id: UiWidgetId) -> &TextFieldState { - let state = self.state.get(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + let state = self.state.get(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); match state { ComponentWidgetState::TextField(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state) + _ => panic!("TextFieldState expected, {:?} found", state), } } fn checkbox_state(&self, widget_id: UiWidgetId) -> &CheckboxState { - let state = self.state.get(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + let state = self.state.get(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); match state { ComponentWidgetState::Checkbox(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state) + _ => panic!("TextFieldState expected, {:?} found", state), } } fn date_picker_state(&self, widget_id: UiWidgetId) -> &DatePickerState { - let state = self.state.get(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + let state = self.state.get(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); match state { ComponentWidgetState::DatePicker(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state) + _ => panic!("TextFieldState expected, {:?} found", state), } } fn select_state(&self, widget_id: UiWidgetId) -> &SelectState { - let state = self.state.get(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + let state = self.state.get(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); match state { ComponentWidgetState::Select(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state) + _ => panic!("TextFieldState expected, {:?} found", state), } } fn root_state(&self, widget_id: UiWidgetId) -> &RootState { - let state = self.state.get(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + let state = self.state.get(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); match state { ComponentWidgetState::Root(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state) + _ => panic!("TextFieldState expected, {:?} found", state), } } } @@ -116,13 +224,13 @@ impl<'b> ComponentWidgetsMut<'b> { root_widget: &'b mut Option>, state: &'b mut HashMap, plugin_id: PluginId, - images: &'b HashMap> + images: &'b HashMap>, ) -> ComponentWidgetsMut<'b> { Self { root_widget, state, plugin_id, - images + images, } } @@ -130,12 +238,18 @@ impl<'b> ComponentWidgetsMut<'b> { Self::text_field_state_mut_on_state(&mut self.state, widget_id) } - fn text_field_state_mut_on_state(state: &mut HashMap, widget_id: UiWidgetId) -> &mut TextFieldState { - let state = state.get_mut(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + fn text_field_state_mut_on_state( + state: &mut HashMap, + widget_id: UiWidgetId, + ) -> &mut TextFieldState { + let state = state.get_mut(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); match state { ComponentWidgetState::TextField(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state) + _ => panic!("TextFieldState expected, {:?} found", state), } } @@ -143,12 +257,18 @@ impl<'b> ComponentWidgetsMut<'b> { Self::root_state_mut_on_field(&mut self.state, widget_id) } - fn root_state_mut_on_field(state: &mut HashMap, widget_id: UiWidgetId) -> &mut RootState { - let state = state.get_mut(&widget_id).expect(&format!("requested state should always be present for id: {}", widget_id)); + fn root_state_mut_on_field( + state: &mut HashMap, + widget_id: UiWidgetId, + ) -> &mut RootState { + let state = state.get_mut(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); match state { ComponentWidgetState::Root(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state) + _ => panic!("TextFieldState expected, {:?} found", state), } } } @@ -188,7 +308,10 @@ pub fn create_state(root_widget: &RootWidget) -> HashMap { - result.insert(widget.__id__, ComponentWidgetState::root(ESTIMATED_MAIN_LIST_ITEM_HEIGHT, 7)); + result.insert( + widget.__id__, + ComponentWidgetState::root(ESTIMATED_MAIN_LIST_ITEM_HEIGHT, 7), + ); if let Some(widget) = &widget.content.search_bar { result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); @@ -196,18 +319,25 @@ pub fn create_state(root_widget: &RootWidget) -> HashMap { // cursed heuristic - let has_title = widget.content + let has_title = widget + .content .ordered_members .iter() - .flat_map(|members| match members { - GridWidgetOrderedMembers::GridItem(widget) => vec![widget], - GridWidgetOrderedMembers::GridSection(widget) => { - widget.content.ordered_members - .iter() - .map(|members| match members { - GridSectionWidgetOrderedMembers::GridItem(widget) => widget - }) - .collect() + .flat_map(|members| { + match members { + GridWidgetOrderedMembers::GridItem(widget) => vec![widget], + GridWidgetOrderedMembers::GridSection(widget) => { + widget + .content + .ordered_members + .iter() + .map(|members| { + match members { + GridSectionWidgetOrderedMembers::GridItem(widget) => widget, + } + }) + .collect() + } } }) .next() @@ -250,12 +380,12 @@ pub enum ComponentWidgetState { #[derive(Debug, Clone)] struct TextFieldState { text_input_id: text_input::Id, - state_value: String + state_value: String, } #[derive(Debug, Clone)] struct CheckboxState { - state_value: bool + state_value: bool, } #[derive(Debug, Clone)] @@ -266,7 +396,7 @@ struct DatePickerState { #[derive(Debug, Clone)] struct SelectState { - state_value: Option + state_value: Option, } #[derive(Debug, Clone)] @@ -286,13 +416,13 @@ impl ComponentWidgetState { fn text_field(value: &Option) -> ComponentWidgetState { ComponentWidgetState::TextField(TextFieldState { text_input_id: text_input::Id::unique(), - state_value: value.to_owned().unwrap_or_default() + state_value: value.to_owned().unwrap_or_default(), }) } fn checkbox(value: &Option) -> ComponentWidgetState { ComponentWidgetState::Checkbox(CheckboxState { - state_value: value.to_owned().unwrap_or(false) + state_value: value.to_owned().unwrap_or(false), }) } @@ -312,7 +442,7 @@ impl ComponentWidgetState { fn select(value: &Option) -> ComponentWidgetState { ComponentWidgetState::Select(SelectState { - state_value: value.to_owned() + state_value: value.to_owned(), }) } } @@ -376,9 +506,7 @@ impl<'b> ComponentWidgets<'b> { Some(widget) => { for members in &widget.content.ordered_members { match members { - ActionPanelWidgetOrderedMembers::Action(widget) => { - result.push(widget.__id__) - } + ActionPanelWidgetOrderedMembers::Action(widget) => result.push(widget.__id__), ActionPanelWidgetOrderedMembers::ActionPanelSection(widget) => { for members in &widget.content.ordered_members { match members { @@ -413,12 +541,12 @@ impl<'b> ComponentWidgets<'b> { let RootState { focused_item, .. } = self.root_state(widget.__id__); ComponentWidgets::list_focused_item_id(focused_item, widget) - }, + } RootWidgetMembers::Grid(widget) => { let RootState { focused_item, .. } = self.root_state(widget.__id__); ComponentWidgets::grid_focused_item_id(focused_item, widget) - }, + } } } @@ -454,7 +582,8 @@ impl<'b> ComponentWidgets<'b> { }); cumulative_item_index = cumulative_item_index + pending_section_size; - cumulative_row_index = cumulative_row_index_at_start + (usize::div_ceil(pending_section_size, width)); + cumulative_row_index = + cumulative_row_index_at_start + (usize::div_ceil(pending_section_size, width)); cumulative_item_index_at_start = cumulative_item_index; cumulative_row_index_at_start = cumulative_row_index; @@ -512,24 +641,23 @@ impl<'b> ComponentWidgetsMut<'b> { let widget_id = match content { RootWidgetMembers::List(widget) => { match &widget.content.search_bar { - None => { - return Task::none() - } - Some(widget) => widget.__id__ + None => return Task::none(), + Some(widget) => widget.__id__, } } RootWidgetMembers::Grid(widget) => { match &widget.content.search_bar { - None => { - return Task::none() - } - Some(widget) => widget.__id__ + None => return Task::none(), + Some(widget) => widget.__id__, } } - _ => return Task::none() + _ => return Task::none(), }; - let TextFieldState { text_input_id, state_value } = ComponentWidgetsMut::text_field_state_mut_on_state(&mut self.state, widget_id); + let TextFieldState { + text_input_id, + state_value, + } = ComponentWidgetsMut::text_field_state_mut_on_state(&mut self.state, widget_id); if let Some(value) = text.chars().next().filter(|c| !c.is_control()) { *state_value = format!("{}{}", state_value, value); @@ -552,24 +680,23 @@ impl<'b> ComponentWidgetsMut<'b> { let widget_id = match content { RootWidgetMembers::List(widget) => { match &widget.content.search_bar { - None => { - return Task::none() - } - Some(widget) => widget.__id__ + None => return Task::none(), + Some(widget) => widget.__id__, } } RootWidgetMembers::Grid(widget) => { match &widget.content.search_bar { - None => { - return Task::none() - } - Some(widget) => widget.__id__ + None => return Task::none(), + Some(widget) => widget.__id__, } } - _ => return Task::none() + _ => return Task::none(), }; - let TextFieldState { text_input_id, state_value } = ComponentWidgetsMut::text_field_state_mut_on_state(&mut self.state, widget_id); + let TextFieldState { + text_input_id, + state_value, + } = ComponentWidgetsMut::text_field_state_mut_on_state(&mut self.state, widget_id); let mut chars = state_value.chars(); chars.next_back(); @@ -592,20 +719,19 @@ impl<'b> ComponentWidgetsMut<'b> { RootWidgetMembers::Form(_) => Task::none(), RootWidgetMembers::Inline(_) => Task::none(), RootWidgetMembers::List(list_widget) => { - let RootState { focused_item, .. } = ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, list_widget.__id__); + let RootState { focused_item, .. } = + ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, list_widget.__id__); - let focus_task = focused_item.focus_previous() - .unwrap_or_else(|| Task::none()); + let focus_task = focused_item.focus_previous().unwrap_or_else(|| Task::none()); - let item_focus_event = ComponentWidgets::list_item_focus_event(self.plugin_id.clone(), focused_item, list_widget); + let item_focus_event = + ComponentWidgets::list_item_focus_event(self.plugin_id.clone(), focused_item, list_widget); - Task::batch([ - item_focus_event, - focus_task - ]) + Task::batch([item_focus_event, focus_task]) } RootWidgetMembers::Grid(grid_widget) => { - let RootState { focused_item, .. } = ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); + let RootState { focused_item, .. } = + ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); let Some(current_index) = &focused_item.index else { return Task::none(); @@ -618,17 +744,15 @@ impl<'b> ComponentWidgetsMut<'b> { Some(data) => { match focused_item.focus_previous_in(data.offset) { None => Task::none(), - Some(_) => focused_item.scroll_to(data.row_index) + Some(_) => focused_item.scroll_to(data.row_index), } } }; - let item_focus_event = ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); + let item_focus_event = + ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); - Task::batch([ - item_focus_event, - focus_task - ]) + Task::batch([item_focus_event, focus_task]) } } } @@ -647,15 +771,20 @@ impl<'b> ComponentWidgetsMut<'b> { RootWidgetMembers::Form(_) => Task::none(), RootWidgetMembers::Inline(_) => Task::none(), RootWidgetMembers::List(widget) => { - let RootState { focused_item, .. } = ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, widget.__id__); + let RootState { focused_item, .. } = + ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, widget.__id__); - let total = widget.content.ordered_members + let total = widget + .content + .ordered_members .iter() .flat_map(|members| { match members { ListWidgetOrderedMembers::ListItem(widget) => vec![widget], ListWidgetOrderedMembers::ListSection(widget) => { - widget.content.ordered_members + widget + .content + .ordered_members .iter() .map(|members| { match members { @@ -668,25 +797,20 @@ impl<'b> ComponentWidgetsMut<'b> { }) .count(); - let focus_task = focused_item.focus_next(total) - .unwrap_or_else(|| Task::none()); + let focus_task = focused_item.focus_next(total).unwrap_or_else(|| Task::none()); - let item_focus_event = ComponentWidgets::list_item_focus_event(self.plugin_id.clone(), focused_item, widget); + let item_focus_event = + ComponentWidgets::list_item_focus_event(self.plugin_id.clone(), focused_item, widget); - Task::batch([ - item_focus_event, - focus_task - ]) + Task::batch([item_focus_event, focus_task]) } RootWidgetMembers::Grid(grid_widget) => { - let RootState { focused_item, .. } = ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); + let RootState { focused_item, .. } = + ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); let amount_per_section_total = ComponentWidgets::grid_section_sizes(grid_widget); - let total = amount_per_section_total - .iter() - .map(|data| data.amount_in_section) - .sum(); + let total = amount_per_section_total.iter().map(|data| data.amount_in_section).sum(); let Some(current_index) = &focused_item.index else { let unfocus = match &grid_widget.content.search_bar { @@ -699,13 +823,10 @@ impl<'b> ComponentWidgetsMut<'b> { let _ = focused_item.focus_next(total); - let item_focus_event = ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); + let item_focus_event = + ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); - return Task::batch([ - unfocus, - focused_item.scroll_to(0), - item_focus_event - ]) + return Task::batch([unfocus, focused_item.scroll_to(0), item_focus_event]); }; let focus_task = match grid_down_offset(*current_index, amount_per_section_total) { @@ -713,17 +834,15 @@ impl<'b> ComponentWidgetsMut<'b> { Some(data) => { match focused_item.focus_next_in(total, data.offset) { None => Task::none(), - Some(_) => focused_item.scroll_to(data.row_index) + Some(_) => focused_item.scroll_to(data.row_index), } } }; - let item_focus_event = ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); + let item_focus_event = + ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); - Task::batch([ - item_focus_event, - focus_task - ]) + Task::batch([item_focus_event, focus_task]) } } } @@ -743,7 +862,8 @@ impl<'b> ComponentWidgetsMut<'b> { RootWidgetMembers::Inline(_) => Task::none(), RootWidgetMembers::List(_) => Task::none(), RootWidgetMembers::Grid(grid_widget) => { - let RootState { focused_item, .. } = ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); + let RootState { focused_item, .. } = + ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); let _ = focused_item.focus_previous(); @@ -769,15 +889,20 @@ impl<'b> ComponentWidgetsMut<'b> { RootWidgetMembers::Inline(_) => Task::none(), RootWidgetMembers::List(_) => Task::none(), RootWidgetMembers::Grid(grid_widget) => { - let RootState { focused_item, .. } = ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); + let RootState { focused_item, .. } = + ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); - let total = grid_widget.content.ordered_members + let total = grid_widget + .content + .ordered_members .iter() .flat_map(|members| { match members { GridWidgetOrderedMembers::GridItem(widget) => vec![widget], GridWidgetOrderedMembers::GridSection(widget) => { - widget.content.ordered_members + widget + .content + .ordered_members .iter() .map(|members| { match members { @@ -801,7 +926,6 @@ impl<'b> ComponentWidgetsMut<'b> { } impl<'b> ComponentWidgets<'b> { - pub fn first_open(&self) -> AppMsg { let Some(root_widget) = &self.root_widget else { return AppMsg::Noop; @@ -814,26 +938,20 @@ impl<'b> ComponentWidgets<'b> { let widget_id = match content { RootWidgetMembers::List(widget) => { match &widget.content.search_bar { - None => { - return AppMsg::Noop - } - Some(widget) => widget.__id__ + None => return AppMsg::Noop, + Some(widget) => widget.__id__, } } RootWidgetMembers::Grid(widget) => { match &widget.content.search_bar { - None => { - return AppMsg::Noop - } - Some(widget) => widget.__id__ + None => return AppMsg::Noop, + Some(widget) => widget.__id__, } } - _ => return AppMsg::Noop + _ => return AppMsg::Noop, }; - AppMsg::FocusPluginViewSearchBar { - widget_id - } + AppMsg::FocusPluginViewSearchBar { widget_id } } fn list_focused_item_id(focused_item: &ScrollHandle, widget: &ListWidget) -> Option { @@ -858,17 +976,23 @@ impl<'b> ComponentWidgets<'b> { match focused_item.get(&items) { None => None, - Some(item_id) => Some(item_id.to_string()) + Some(item_id) => Some(item_id.to_string()), } } fn list_item_focus_event(plugin_id: PluginId, focused_item: &ScrollHandle, widget: &ListWidget) -> Task { let widget_event = match ComponentWidgets::list_focused_item_id(focused_item, widget) { None => { - ComponentWidgetEvent::FocusListItem { list_widget_id: widget.__id__, item_id: None } + ComponentWidgetEvent::FocusListItem { + list_widget_id: widget.__id__, + item_id: None, + } } Some(item_id) => { - ComponentWidgetEvent::FocusListItem { list_widget_id: widget.__id__, item_id: Some(item_id) } + ComponentWidgetEvent::FocusListItem { + list_widget_id: widget.__id__, + item_id: Some(item_id), + } } }; @@ -901,17 +1025,23 @@ impl<'b> ComponentWidgets<'b> { match focused_item.get(&items) { None => None, - Some(item_id) => Some(item_id.to_string()) + Some(item_id) => Some(item_id.to_string()), } } fn grid_item_focus_event(plugin_id: PluginId, focused_item: &ScrollHandle, widget: &GridWidget) -> Task { let widget_event = match ComponentWidgets::grid_focused_item_id(focused_item, widget) { None => { - ComponentWidgetEvent::FocusGridItem { grid_widget_id: widget.__id__, item_id: None } + ComponentWidgetEvent::FocusGridItem { + grid_widget_id: widget.__id__, + item_id: None, + } } Some(item_id) => { - ComponentWidgetEvent::FocusGridItem { grid_widget_id: widget.__id__, item_id: Some(item_id) } + ComponentWidgetEvent::FocusGridItem { + grid_widget_id: widget.__id__, + item_id: Some(item_id), + } } }; @@ -951,16 +1081,13 @@ impl<'b> ComponentWidgets<'b> { TextRenderType::H6 => Some(16), }; - let mut text = text(value.join("")) - .shaping(Shaping::Advanced); + let mut text = text(value.join("")).shaping(Shaping::Advanced); if let Some(size) = header { - text = text - .size(size) - .font(Font { - weight: Weight::Bold, - ..Font::DEFAULT - }) + text = text.size(size).font(Font { + weight: Weight::Bold, + ..Font::DEFAULT + }) } text.into() @@ -973,18 +1100,13 @@ impl<'b> ComponentWidgets<'b> { action_shortcuts: &HashMap, ) -> Element<'a, ComponentWidgetEvent> { match &self.root_widget { - None => { - horizontal_space() - .into() - } + None => horizontal_space().into(), Some(root) => { match &root.content { - None => { - horizontal_space() - .into() - } + None => horizontal_space().into(), Some(content) => { - let entrypoint_name = entrypoint_name.expect("entrypoint name should always exist after render"); + let entrypoint_name = + entrypoint_name.expect("entrypoint name should always exist after render"); match content { RootWidgetMembers::Detail(widget) => { @@ -1004,10 +1126,16 @@ impl<'b> ComponentWidgets<'b> { entrypoint_name, action_shortcuts, ) - }, - RootWidgetMembers::Form(widget) => self.render_form_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts), - RootWidgetMembers::List(widget) => self.render_list_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts), - RootWidgetMembers::Grid(widget) => self.render_grid_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts), + } + RootWidgetMembers::Form(widget) => { + self.render_form_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts) + } + RootWidgetMembers::List(widget) => { + self.render_list_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts) + } + RootWidgetMembers::Grid(widget) => { + self.render_grid_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts) + } _ => { panic!("used inline widget in non-inline place") } @@ -1018,26 +1146,26 @@ impl<'b> ComponentWidgets<'b> { } } - pub fn render_root_inline_widget<'a>(&self, plugin_name: Option<&String>, entrypoint_name: Option<&String>) -> Element<'a, ComponentWidgetEvent> { + pub fn render_root_inline_widget<'a>( + &self, + plugin_name: Option<&String>, + entrypoint_name: Option<&String>, + ) -> Element<'a, ComponentWidgetEvent> { match &self.root_widget { - None => { - horizontal_space() - .into() - } + None => horizontal_space().into(), Some(root) => { match &root.content { - None => { - horizontal_space() - .into() - } + None => horizontal_space().into(), Some(content) => { match content { RootWidgetMembers::Inline(widget) => { - let entrypoint_name = entrypoint_name.expect("entrypoint name should always exist after render"); - let plugin_name = plugin_name.expect("entrypoint name should always exist after render"); + let entrypoint_name = + entrypoint_name.expect("entrypoint name should always exist after render"); + let plugin_name = + plugin_name.expect("entrypoint name should always exist after render"); self.render_inline_widget(widget, plugin_name, entrypoint_name) - }, + } _ => { panic!("used non-inline widget in inline place") } @@ -1052,122 +1180,149 @@ impl<'b> ComponentWidgets<'b> { let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); let tag: Element<_> = button(content) - .on_press(ComponentWidgetEvent::TagClick { widget_id: widget.__id__ }) + .on_press(ComponentWidgetEvent::TagClick { + widget_id: widget.__id__, + }) .themed(ButtonStyle::MetadataTagItem); - container(tag) - .themed(ContainerStyle::MetadataTagItem) + container(tag).themed(ContainerStyle::MetadataTagItem) } - fn render_metadata_tag_list_widget<'a>(&self, widget: &MetadataTagListWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { - let content: Vec> = widget.content.ordered_members + fn render_metadata_tag_list_widget<'a>( + &self, + widget: &MetadataTagListWidget, + is_in_list: bool, + ) -> Element<'a, ComponentWidgetEvent> { + let content: Vec> = widget + .content + .ordered_members .iter() .map(|members| { match members { - MetadataTagListWidgetOrderedMembers::MetadataTagItem(content) => self.render_metadata_tag_item_widget(&content) + MetadataTagListWidgetOrderedMembers::MetadataTagItem(content) => { + self.render_metadata_tag_item_widget(&content) + } } }) .collect(); - let value = row(content) - .wrap() - .into(); + let value = row(content).wrap().into(); - render_metadata_item(&widget.label, value, is_in_list) - .into() + render_metadata_item(&widget.label, value, is_in_list).into() } - fn render_metadata_link_widget<'a>(&self, widget: &MetadataLinkWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { + fn render_metadata_link_widget<'a>( + &self, + widget: &MetadataLinkWidget, + is_in_list: bool, + ) -> Element<'a, ComponentWidgetEvent> { let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); - let icon: Element<_> = value(Bootstrap::BoxArrowUpRight) - .font(BOOTSTRAP_FONT) - .size(16) - .into(); + let icon: Element<_> = value(Bootstrap::BoxArrowUpRight).font(BOOTSTRAP_FONT).size(16).into(); - let icon = container(icon) - .themed(ContainerStyle::MetadataLinkIcon); + let icon = container(icon).themed(ContainerStyle::MetadataLinkIcon); - let content: Element<_> = row([content, icon]) - .align_y(Alignment::Center) - .into(); + let content: Element<_> = row([content, icon]).align_y(Alignment::Center).into(); let link: Element<_> = button(content) - .on_press(ComponentWidgetEvent::LinkClick { widget_id: widget.__id__, href: widget.href.to_owned() }) + .on_press(ComponentWidgetEvent::LinkClick { + widget_id: widget.__id__, + href: widget.href.to_owned(), + }) .themed(ButtonStyle::MetadataLink); let content: Element<_> = if widget.href.is_empty() { link } else { - let href: Element<_> = text(widget.href.to_string()) - .shaping(Shaping::Advanced) - .into(); + let href: Element<_> = text(widget.href.to_string()).shaping(Shaping::Advanced).into(); - tooltip(link, href, Position::Top) - .themed(TooltipStyle::Tooltip) + tooltip(link, href, Position::Top).themed(TooltipStyle::Tooltip) }; - render_metadata_item(&widget.label, content, is_in_list) - .into() + render_metadata_item(&widget.label, content, is_in_list).into() } - fn render_metadata_value_widget<'a>(&self, widget: &MetadataValueWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { + fn render_metadata_value_widget<'a>( + &self, + widget: &MetadataValueWidget, + is_in_list: bool, + ) -> Element<'a, ComponentWidgetEvent> { let value: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); - render_metadata_item(&widget.label, value, is_in_list) - .into() + render_metadata_item(&widget.label, value, is_in_list).into() } - fn render_metadata_icon_widget<'a>(&self, widget: &MetadataIconWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { + fn render_metadata_icon_widget<'a>( + &self, + widget: &MetadataIconWidget, + is_in_list: bool, + ) -> Element<'a, ComponentWidgetEvent> { let value = value(icon_to_bootstrap(&widget.icon)) .font(BOOTSTRAP_FONT) .size(26) .into(); - render_metadata_item(&widget.label, value, is_in_list) - .into() + render_metadata_item(&widget.label, value, is_in_list).into() } - fn render_metadata_separator_widget<'a>(&self, _widget: &MetadataSeparatorWidget) -> Element<'a, ComponentWidgetEvent> { - let separator: Element<_> = horizontal_rule(1) - .into(); + fn render_metadata_separator_widget<'a>( + &self, + _widget: &MetadataSeparatorWidget, + ) -> Element<'a, ComponentWidgetEvent> { + let separator: Element<_> = horizontal_rule(1).into(); container(separator) .width(Length::Fill) .themed(ContainerStyle::MetadataSeparator) } - fn render_metadata_widget<'a>(&self, widget: &MetadataWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { - let content: Vec> = widget.content.ordered_members + fn render_metadata_widget<'a>( + &self, + widget: &MetadataWidget, + is_in_list: bool, + ) -> Element<'a, ComponentWidgetEvent> { + let content: Vec> = widget + .content + .ordered_members .iter() .map(|members| { match members { - MetadataWidgetOrderedMembers::MetadataTagList(content) => self.render_metadata_tag_list_widget(content, is_in_list), - MetadataWidgetOrderedMembers::MetadataLink(content) => self.render_metadata_link_widget(content, is_in_list), - MetadataWidgetOrderedMembers::MetadataValue(content) => self.render_metadata_value_widget(content, is_in_list), - MetadataWidgetOrderedMembers::MetadataIcon(content) => self.render_metadata_icon_widget(content, is_in_list), - MetadataWidgetOrderedMembers::MetadataSeparator(content) => self.render_metadata_separator_widget(content), + MetadataWidgetOrderedMembers::MetadataTagList(content) => { + self.render_metadata_tag_list_widget(content, is_in_list) + } + MetadataWidgetOrderedMembers::MetadataLink(content) => { + self.render_metadata_link_widget(content, is_in_list) + } + MetadataWidgetOrderedMembers::MetadataValue(content) => { + self.render_metadata_value_widget(content, is_in_list) + } + MetadataWidgetOrderedMembers::MetadataIcon(content) => { + self.render_metadata_icon_widget(content, is_in_list) + } + MetadataWidgetOrderedMembers::MetadataSeparator(content) => { + self.render_metadata_separator_widget(content) + } } }) .collect(); - let metadata: Element<_> = column(content) - .into(); + let metadata: Element<_> = column(content).into(); let metadata = container(metadata) .width(Length::Fill) .themed(ContainerStyle::MetadataInner); - scrollable(metadata) - .width(Length::Fill) - .into() + scrollable(metadata).width(Length::Fill).into() } - fn render_paragraph_widget<'a>(&self, widget: &ParagraphWidget, centered: bool) -> Element<'a, ComponentWidgetEvent> { + fn render_paragraph_widget<'a>( + &self, + widget: &ParagraphWidget, + centered: bool, + ) -> Element<'a, ComponentWidgetEvent> { let paragraph: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); - let mut content = container(paragraph) - .width(Length::Fill); + let mut content = container(paragraph).width(Length::Fill); if centered { content = content.align_x(Horizontal::Center) @@ -1180,8 +1335,7 @@ impl<'b> ComponentWidgets<'b> { // TODO image size, height and width let content: Element<_> = render_image(self.images, widget.__id__, &widget.source, None); - let mut content = container(content) - .width(Length::Fill); + let mut content = container(content).width(Length::Fill); if centered { content = content.align_x(Horizontal::Center) @@ -1235,7 +1389,9 @@ impl<'b> ComponentWidgets<'b> { } fn render_content_widget<'a>(&self, widget: &ContentWidget, centered: bool) -> Element<'a, ComponentWidgetEvent> { - let content: Vec<_> = widget.content.ordered_members + let content: Vec<_> = widget + .content + .ordered_members .iter() .map(|members| { match members { @@ -1253,8 +1409,7 @@ impl<'b> ComponentWidgets<'b> { }) .collect(); - let content: Element<_> = column(content) - .into(); + let content: Element<_> = column(content).into(); if centered { container(content) @@ -1269,51 +1424,65 @@ impl<'b> ComponentWidgets<'b> { } fn render_detail_widget<'a>(&self, widget: &DetailWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { - let metadata_element = widget.content.metadata - .as_ref() - .map(|widget| { - let content = self.render_metadata_widget(widget, is_in_list); + let metadata_element = widget.content.metadata.as_ref().map(|widget| { + let content = self.render_metadata_widget(widget, is_in_list); - container(content) - .width(if is_in_list { Length::Fill } else { Length::FillPortion(2) }) - .height(if is_in_list { Length::FillPortion(3) } else { Length::Fill }) - .themed(ContainerStyle::DetailMetadata) - }); + container(content) + .width( + if is_in_list { + Length::Fill + } else { + Length::FillPortion(2) + }, + ) + .height( + if is_in_list { + Length::FillPortion(3) + } else { + Length::Fill + }, + ) + .themed(ContainerStyle::DetailMetadata) + }); - let content_element = widget.content.content - .as_ref() - .map(|widget| { - let content_element: Element<_> = container(self.render_content_widget(widget, false)) - .width(Length::Fill) - .themed(ContainerStyle::DetailContentInner); + let content_element = widget.content.content.as_ref().map(|widget| { + let content_element: Element<_> = container(self.render_content_widget(widget, false)) + .width(Length::Fill) + .themed(ContainerStyle::DetailContentInner); - let content_element: Element<_> = scrollable(content_element) - .width(Length::Fill) - .into(); + let content_element: Element<_> = scrollable(content_element).width(Length::Fill).into(); - let content_element: Element<_> = container(content_element) - .width(if is_in_list { Length::Fill } else { Length::FillPortion(3) }) - .height(if is_in_list { Length::FillPortion(3) } else { Length::Fill }) - .themed(ContainerStyle::DetailContent); + let content_element: Element<_> = container(content_element) + .width( + if is_in_list { + Length::Fill + } else { + Length::FillPortion(3) + }, + ) + .height( + if is_in_list { + Length::FillPortion(3) + } else { + Length::Fill + }, + ) + .themed(ContainerStyle::DetailContent); - content_element - }); + content_element + }); let separator = if is_in_list { - horizontal_rule(1) - .into() + horizontal_rule(1).into() } else { - vertical_rule(1) - .into() + vertical_rule(1).into() }; let list_fn = |vec| { if is_in_list { - column(vec) - .into() + column(vec).into() } else { - row(vec) - .into() + row(vec).into() } }; @@ -1321,15 +1490,9 @@ impl<'b> ComponentWidgets<'b> { (Some(content_element), Some(metadata_element)) => { list_fn(vec![content_element, separator, metadata_element]) } - (Some(content_element), None) => { - list_fn(vec![content_element]) - } - (None, Some(metadata_element)) => { - list_fn(vec![metadata_element]) - } - (None, None) => { - list_fn(vec![]) - } + (Some(content_element), None) => list_fn(vec![content_element]), + (None, Some(metadata_element)) => list_fn(vec![metadata_element]), + (None, None) => list_fn(vec![]), }; content @@ -1365,13 +1528,16 @@ impl<'b> ComponentWidgets<'b> { fn render_date_picker_widget<'a>(&self, widget: &DatePickerWidget) -> Element<'a, ComponentWidgetEvent> { let widget_id = widget.__id__; - let DatePickerState { state_value, show_picker } = self.date_picker_state(widget.__id__); + let DatePickerState { + state_value, + show_picker, + } = self.date_picker_state(widget.__id__); - let button_text = text(state_value.to_string()) - .shaping(Shaping::Advanced); + let button_text = text(state_value.to_string()).shaping(Shaping::Advanced); - let button = button(button_text) - .on_press(ComponentWidgetEvent::ToggleDatePicker { widget_id: widget.__id__ }); + let button = button(button_text).on_press(ComponentWidgetEvent::ToggleDatePicker { + widget_id: widget.__id__, + }); // TODO unable to customize buttons here, split to separate button styles // DatePickerUnderlay, @@ -1388,14 +1554,17 @@ impl<'b> ComponentWidgets<'b> { value: date.to_string(), } }, - ).themed(DatePickerStyle::Default) + ) + .themed(DatePickerStyle::Default) } fn render_select_widget<'a>(&self, widget: &SelectWidget) -> Element<'a, ComponentWidgetEvent> { let widget_id = widget.__id__; let SelectState { state_value } = self.select_state(widget_id); - let items: Vec<_> = widget.content.ordered_members + let items: Vec<_> = widget + .content + .ordered_members .iter() .map(|members| { match members { @@ -1409,21 +1578,23 @@ impl<'b> ComponentWidgets<'b> { }) .collect(); - let state_value = state_value.clone() + let state_value = state_value + .clone() .map(|value| items.iter().find(|item| item.value == value)) .flatten() .map(|value| value.clone()); - pick_list( - items, - state_value, - move |item| ComponentWidgetEvent::SelectPickList { widget_id, value: item.value }, - ).themed(PickListStyle::Default) + pick_list(items, state_value, move |item| { + ComponentWidgetEvent::SelectPickList { + widget_id, + value: item.value, + } + }) + .themed(PickListStyle::Default) } fn render_separator_widget<'a>(&self, _widget: &SeparatorWidget) -> Element<'a, ComponentWidgetEvent> { - horizontal_rule(1) - .into() + horizontal_rule(1).into() } fn render_form_widget<'a>( @@ -1436,15 +1607,17 @@ impl<'b> ComponentWidgets<'b> { let widget_id = widget.__id__; let RootState { show_action_panel, .. } = self.root_state(widget_id); - let items: Vec> = widget.content.ordered_members + let items: Vec> = widget + .content + .ordered_members .iter() .map(|members| { - fn render_field<'c, 'd>(field: Element<'c, ComponentWidgetEvent>, label: &'d Option) -> Element<'c, ComponentWidgetEvent> { + fn render_field<'c, 'd>( + field: Element<'c, ComponentWidgetEvent>, + label: &'d Option, + ) -> Element<'c, ComponentWidgetEvent> { let before_or_label: Element<_> = match label { - None => { - Space::with_width(Length::FillPortion(2)) - .into() - } + None => Space::with_width(Length::FillPortion(2)).into(), Some(label) => { let label: Element<_> = text(label.to_string()) .shaping(Shaping::Advanced) @@ -1458,51 +1631,45 @@ impl<'b> ComponentWidgets<'b> { } }; - let form_input = container(field) - .width(Length::FillPortion(3)) - .into(); + let form_input = container(field).width(Length::FillPortion(3)).into(); - let after = Space::with_width(Length::FillPortion(2)) - .into(); + let after = Space::with_width(Length::FillPortion(2)).into(); - let content = vec![ - before_or_label, - form_input, - after, - ]; + let content = vec![before_or_label, form_input, after]; - let row: Element<_> = row(content) - .align_y(Alignment::Center) - .themed(RowStyle::FormInput); + let row: Element<_> = row(content).align_y(Alignment::Center).themed(RowStyle::FormInput); row } match members { FormWidgetOrderedMembers::Separator(widget) => self.render_separator_widget(widget), - FormWidgetOrderedMembers::TextField(widget) => render_field(self.render_text_field_widget(widget), &widget.label), - FormWidgetOrderedMembers::PasswordField(widget) => render_field(self.render_password_field_widget(widget), &widget.label), - FormWidgetOrderedMembers::Checkbox(widget) => render_field(self.render_checkbox_widget(widget), &widget.label), - FormWidgetOrderedMembers::DatePicker(widget) => render_field(self.render_date_picker_widget(widget), &widget.label), - FormWidgetOrderedMembers::Select(widget) => render_field(self.render_select_widget(widget), &widget.label) + FormWidgetOrderedMembers::TextField(widget) => { + render_field(self.render_text_field_widget(widget), &widget.label) + } + FormWidgetOrderedMembers::PasswordField(widget) => { + render_field(self.render_password_field_widget(widget), &widget.label) + } + FormWidgetOrderedMembers::Checkbox(widget) => { + render_field(self.render_checkbox_widget(widget), &widget.label) + } + FormWidgetOrderedMembers::DatePicker(widget) => { + render_field(self.render_date_picker_widget(widget), &widget.label) + } + FormWidgetOrderedMembers::Select(widget) => { + render_field(self.render_select_widget(widget), &widget.label) + } } }) .collect(); - let content: Element<_> = column(items) - .into(); + let content: Element<_> = column(items).into(); - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::FormInner); + let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::FormInner); - let content: Element<_> = scrollable(content) - .width(Length::Fill) - .into(); + let content: Element<_> = scrollable(content).width(Length::Fill).into(); - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::Form); + let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::Form); self.render_plugin_root( *show_action_panel, @@ -1514,7 +1681,7 @@ impl<'b> ComponentWidgets<'b> { widget.is_loading.unwrap_or(false), plugin_view_state, entrypoint_name, - action_shortcuts + action_shortcuts, ) } @@ -1522,80 +1689,70 @@ impl<'b> ComponentWidgets<'b> { match &widget.icon { None => vertical_rule(1).into(), Some(icon) => { - let top_rule: Element<_> = vertical_rule(1) - .into(); + let top_rule: Element<_> = vertical_rule(1).into(); - let top_rule = container(top_rule) - .align_x(Horizontal::Center) - .into(); + let top_rule = container(top_rule).align_x(Horizontal::Center).into(); let icon = value(icon_to_bootstrap(icon)) .font(BOOTSTRAP_FONT) .size(45) .themed(TextStyle::InlineSeparator); - let bot_rule: Element<_> = vertical_rule(1) - .into(); + let bot_rule: Element<_> = vertical_rule(1).into(); - let bot_rule = container(bot_rule) - .align_x(Horizontal::Center) - .into(); + let bot_rule = container(bot_rule).align_x(Horizontal::Center).into(); - column([top_rule, icon, bot_rule]) - .align_x(Alignment::Center) - .into() + column([top_rule, icon, bot_rule]).align_x(Alignment::Center).into() } } } - fn render_inline_widget<'a>(&self, widget: &InlineWidget, plugin_name: &str, entrypoint_name: &str) -> Element<'a, ComponentWidgetEvent> { + fn render_inline_widget<'a>( + &self, + widget: &InlineWidget, + plugin_name: &str, + entrypoint_name: &str, + ) -> Element<'a, ComponentWidgetEvent> { let name: Element<_> = text(format!("{} - {}", plugin_name, entrypoint_name)) .shaping(Shaping::Advanced) .themed(TextStyle::InlineName); - let name: Element<_> = container(name) - .themed(ContainerStyle::InlineName); + let name: Element<_> = container(name).themed(ContainerStyle::InlineName); - let content: Vec> = widget.content.ordered_members + let content: Vec> = widget + .content + .ordered_members .iter() .map(|members| { match members { InlineWidgetOrderedMembers::Content(widget) => { let element = self.render_content_widget(widget, true); - container(element) - .into() - }, - InlineWidgetOrderedMembers::InlineSeparator(widget) => self.render_inline_separator_widget(widget) + container(element).into() + } + InlineWidgetOrderedMembers::InlineSeparator(widget) => self.render_inline_separator_widget(widget), } }) .collect(); - let content: Element<_> = row(content) - .into(); + let content: Element<_> = row(content).into(); - let content: Element<_> = container(content) - .themed(ContainerStyle::InlineInner); + let content: Element<_> = container(content).themed(ContainerStyle::InlineInner); - let content: Element<_> = column(vec![name, content]) - .width(Length::Fill) - .into(); + let content: Element<_> = column(vec![name, content]).width(Length::Fill).into(); - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::Inline); + let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::Inline); content } fn render_empty_view_widget<'a>(&self, widget: &EmptyViewWidget) -> Element<'a, ComponentWidgetEvent> { - let image: Option> = widget.image + let image: Option> = widget + .image .as_ref() .map(|image| render_image(self.images, widget.__id__, image, Some(TextStyle::EmptyViewSubtitle))); - let title: Element<_> = text(widget.title.to_string()) - .shaping(Shaping::Advanced) - .into(); + let title: Element<_> = text(widget.title.to_string()).shaping(Shaping::Advanced).into(); let subtitle: Element<_> = match &widget.description { None => horizontal_space().into(), @@ -1608,15 +1765,12 @@ impl<'b> ComponentWidgets<'b> { let mut content = vec![title, subtitle]; if let Some(image) = image { - let image: Element<_> = container(image) - .themed(ContainerStyle::EmptyViewImage); + let image: Element<_> = container(image).themed(ContainerStyle::EmptyViewImage); content.insert(0, image) } - let content: Element<_> = column(content) - .align_x(Alignment::Center) - .into(); + let content: Element<_> = column(content).align_x(Alignment::Center).into(); container(content) .width(Length::Fill) @@ -1626,10 +1780,12 @@ impl<'b> ComponentWidgets<'b> { .into() } - fn render_search_bar_widget<'a>(&self, widget: &SearchBarWidget) -> Element<'a, ComponentWidgetEvent> { let widget_id = widget.__id__; - let TextFieldState { state_value, text_input_id } = self.text_field_state(widget_id); + let TextFieldState { + state_value, + text_input_id, + } = self.text_field_state(widget_id); text_input(widget.placeholder.as_deref().unwrap_or_default(), state_value) .id(text_input_id.clone()) @@ -1646,7 +1802,10 @@ impl<'b> ComponentWidgets<'b> { action_shortcuts: &HashMap, ) -> Element<'a, ComponentWidgetEvent> { let widget_id = list_widget.__id__; - let RootState { show_action_panel, focused_item } = self.root_state(widget_id); + let RootState { + show_action_panel, + focused_item, + } = self.root_state(widget_id); let mut pending: Vec<&ListItemWidget> = vec![]; let mut items: Vec> = vec![]; @@ -1658,7 +1817,7 @@ impl<'b> ComponentWidgets<'b> { ListWidgetOrderedMembers::ListItem(widget) => { first_section = false; pending.push(widget) - }, + } ListWidgetOrderedMembers::ListSection(widget) => { if !pending.is_empty() { let content: Vec<_> = pending @@ -1666,18 +1825,22 @@ impl<'b> ComponentWidgets<'b> { .map(|widget| self.render_list_item_widget(widget, focused_item.index, index_counter)) .collect(); - let content: Element<_> = column(content) - .into(); + let content: Element<_> = column(content).into(); items.push(content); pending = vec![]; } - items.push(self.render_list_section_widget(widget, focused_item.index, index_counter, first_section)); + items.push(self.render_list_section_widget( + widget, + focused_item.index, + index_counter, + first_section, + )); first_section = false; - }, + } } } @@ -1687,8 +1850,7 @@ impl<'b> ComponentWidgets<'b> { .map(|widget| self.render_list_item_widget(widget, focused_item.index, index_counter)) .collect(); - let content: Element<_> = column(content) - .into(); + let content: Element<_> = column(content).into(); items.push(content); } @@ -1696,16 +1858,12 @@ impl<'b> ComponentWidgets<'b> { let content = if items.is_empty() { match &list_widget.content.empty_view { Some(widget) => self.render_empty_view_widget(widget), - None => horizontal_space().into() + None => horizontal_space().into(), } } else { - let content: Element<_> = column(items) - .width(Length::Fill) - .into(); + let content: Element<_> = column(items).width(Length::Fill).into(); - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::ListInner); + let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::ListInner); let content: Element<_> = scrollable(content) .id(focused_item.scrollable_id.clone()) @@ -1724,21 +1882,16 @@ impl<'b> ComponentWidgets<'b> { if let Some(detail) = &list_widget.content.detail { let detail = self.render_detail_widget(detail, true); - let detail: Element<_> = container(detail) - .width(Length::FillPortion(5)) - .into(); + let detail: Element<_> = container(detail).width(Length::FillPortion(5)).into(); - let separator: Element<_> = vertical_rule(1) - .into(); + let separator: Element<_> = vertical_rule(1).into(); elements.push(separator); elements.push(detail); } - let content: Element<_> = row(elements) - .height(Length::Fill) - .into(); + let content: Element<_> = row(elements).height(Length::Fill).into(); let focused_item_id = ComponentWidgets::list_focused_item_id(focused_item, list_widget); @@ -1752,7 +1905,7 @@ impl<'b> ComponentWidgets<'b> { list_widget.is_loading.unwrap_or(false), plugin_view_state, entrypoint_name, - action_shortcuts + action_shortcuts, ) } @@ -1763,44 +1916,55 @@ impl<'b> ComponentWidgets<'b> { index_counter: &Cell, first_section: bool, ) -> Element<'a, ComponentWidgetEvent> { - let content: Vec<_> = widget.content.ordered_members + let content: Vec<_> = widget + .content + .ordered_members .iter() .map(|members| { match members { - ListSectionWidgetOrderedMembers::ListItem(widget) => self.render_list_item_widget(widget, item_focus_index, index_counter) + ListSectionWidgetOrderedMembers::ListItem(widget) => { + self.render_list_item_widget(widget, item_focus_index, index_counter) + } } }) .collect(); - let content = column(content) - .into(); + let content = column(content).into(); - let section_title_style = if first_section { RowStyle::ListFirstSectionTitle } else { RowStyle::ListSectionTitle }; + let section_title_style = if first_section { + RowStyle::ListFirstSectionTitle + } else { + RowStyle::ListSectionTitle + }; - render_section(content, Some(&widget.title), &widget.subtitle, section_title_style, TextStyle::ListSectionTitle, TextStyle::ListSectionSubtitle) + render_section( + content, + Some(&widget.title), + &widget.subtitle, + section_title_style, + TextStyle::ListSectionTitle, + TextStyle::ListSectionSubtitle, + ) } fn render_list_item_widget<'a>( &self, widget: &ListItemWidget, item_focus_index: Option, - index_counter: &Cell + index_counter: &Cell, ) -> Element<'a, ComponentWidgetEvent> { - let icon: Option> = widget.icon + let icon: Option> = widget + .icon .as_ref() .map(|icon| render_image(self.images, widget.__id__, icon, None)); - let title: Element<_> = text(widget.title.to_string()) - .shaping(Shaping::Advanced) - .into(); - let title: Element<_> = container(title) - .themed(ContainerStyle::ListItemTitle); + let title: Element<_> = text(widget.title.to_string()).shaping(Shaping::Advanced).into(); + let title: Element<_> = container(title).themed(ContainerStyle::ListItemTitle); let mut content = vec![title]; if let Some(icon) = icon { - let icon: Element<_> = container(icon) - .themed(ContainerStyle::ListItemIcon); + let icon: Element<_> = container(icon).themed(ContainerStyle::ListItemIcon); content.insert(0, icon) } @@ -1809,36 +1973,33 @@ impl<'b> ComponentWidgets<'b> { let subtitle: Element<_> = text(subtitle.to_string()) .shaping(Shaping::Advanced) .themed(TextStyle::ListItemSubtitle); - let subtitle: Element<_> = container(subtitle) - .themed(ContainerStyle::ListItemSubtitle); + let subtitle: Element<_> = container(subtitle).themed(ContainerStyle::ListItemSubtitle); content.push(subtitle) } if widget.content.accessories.len() > 0 { - let accessories: Vec> = widget.content.accessories + let accessories: Vec> = widget + .content + .accessories .iter() .map(|accessory| { match accessory { ListItemAccessories::_0(widget) => render_text_accessory(self.images, widget), - ListItemAccessories::_1(widget) => render_icon_accessory(self.images, widget) + ListItemAccessories::_1(widget) => render_icon_accessory(self.images, widget), } }) .collect(); - let accessories: Element<_> = row(accessories) - .into(); + let accessories: Element<_> = row(accessories).into(); - let space = horizontal_space() - .into(); + let space = horizontal_space().into(); content.push(space); content.push(accessories); } - let content: Element<_> = row(content) - .align_y(Alignment::Center) - .into(); + let content: Element<_> = row(content).align_y(Alignment::Center).into(); let style = match item_focus_index { None => ButtonStyle::ListItem, @@ -1858,13 +2019,15 @@ impl<'b> ComponentWidgets<'b> { let on_press_msg = match primary_action { None => ComponentWidgetEvent::Noop, - Some(widget_id) => ComponentWidgetEvent::RunPrimaryAction { widget_id: *widget_id, id: Some(widget.id.clone()) } + Some(widget_id) => { + ComponentWidgetEvent::RunPrimaryAction { + widget_id: *widget_id, + id: Some(widget.id.clone()), + } + } }; - button(content) - .on_press(on_press_msg) - .width(Length::Fill) - .themed(style) + button(content).on_press(on_press_msg).width(Length::Fill).themed(style) } fn render_grid_widget<'a>( @@ -1874,12 +2037,15 @@ impl<'b> ComponentWidgets<'b> { entrypoint_name: &str, action_shortcuts: &HashMap, ) -> Element<'a, ComponentWidgetEvent> { - let RootState { show_action_panel, focused_item } = self.root_state(grid_widget.__id__); + let RootState { + show_action_panel, + focused_item, + } = self.root_state(grid_widget.__id__); let content = if grid_widget.content.ordered_members.is_empty() { match &grid_widget.content.empty_view { Some(widget) => self.render_empty_view_widget(widget), - None => horizontal_space().into() + None => horizontal_space().into(), } } else { let mut pending: Vec<&GridItemWidget> = vec![]; @@ -1895,14 +2061,20 @@ impl<'b> ComponentWidgets<'b> { } GridWidgetOrderedMembers::GridSection(widget) => { if !pending.is_empty() { - let content = self.render_grid(&pending, &grid_widget.columns, focused_item.index, index_counter); + let content = + self.render_grid(&pending, &grid_widget.columns, focused_item.index, index_counter); items.push(content); pending = vec![]; } - items.push(self.render_grid_section_widget(widget, focused_item.index, index_counter, first_section)); + items.push(self.render_grid_section_widget( + widget, + focused_item.index, + index_counter, + first_section, + )); first_section = false; } @@ -1915,21 +2087,16 @@ impl<'b> ComponentWidgets<'b> { items.push(content); } - let content: Element<_> = column(items) - .into(); + let content: Element<_> = column(items).into(); - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::GridInner); + let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::GridInner); let content: Element<_> = scrollable(content) .id(focused_item.scrollable_id.clone()) .width(Length::Fill) .into(); - let content: Element<_> = container(content) - .width(Length::Fill) - .themed(ContainerStyle::Grid); + let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::Grid); content }; @@ -1946,7 +2113,7 @@ impl<'b> ComponentWidgets<'b> { grid_widget.is_loading.unwrap_or(false), plugin_view_state, entrypoint_name, - action_shortcuts + action_shortcuts, ) } @@ -1955,22 +2122,35 @@ impl<'b> ComponentWidgets<'b> { widget: &GridSectionWidget, item_focus_index: Option, index_counter: &Cell, - first_section: bool + first_section: bool, ) -> Element<'a, ComponentWidgetEvent> { - let items: Vec<_> = widget.content.ordered_members + let items: Vec<_> = widget + .content + .ordered_members .iter() .map(|members| { match members { - GridSectionWidgetOrderedMembers::GridItem(widget) => widget + GridSectionWidgetOrderedMembers::GridItem(widget) => widget, } }) .collect(); let content = self.render_grid(&items, &widget.columns, item_focus_index, index_counter); - let section_title_style = if first_section { RowStyle::GridFirstSectionTitle } else { RowStyle::GridSectionTitle }; + let section_title_style = if first_section { + RowStyle::GridFirstSectionTitle + } else { + RowStyle::GridSectionTitle + }; - render_section(content, Some(&widget.title), &widget.subtitle, section_title_style, TextStyle::GridSectionTitle, TextStyle::GridSectionSubtitle) + render_section( + content, + Some(&widget.title), + &widget.subtitle, + section_title_style, + TextStyle::GridSectionTitle, + TextStyle::GridSectionSubtitle, + ) } fn render_grid_item_widget<'a>( @@ -1978,7 +2158,7 @@ impl<'b> ComponentWidgets<'b> { widget: &GridItemWidget, item_focus_index: Option, index_counter: &Cell, - grid_width: usize + grid_width: usize, ) -> Element<'a, ComponentWidgetEvent> { let height = match grid_width { ..4 => 130, @@ -2012,13 +2192,15 @@ impl<'b> ComponentWidgets<'b> { let on_press_msg = match primary_action { None => ComponentWidgetEvent::Noop, - Some(widget_id) => ComponentWidgetEvent::RunPrimaryAction { widget_id: *widget_id, id: Some(widget.id.clone()) } + Some(widget_id) => { + ComponentWidgetEvent::RunPrimaryAction { + widget_id: *widget_id, + id: Some(widget.id.clone()), + } + } }; - let content: Element<_> = button(content) - .on_press(on_press_msg) - .width(Length::Fill) - .themed(style); + let content: Element<_> = button(content).on_press(on_press_msg).width(Length::Fill).themed(style); let mut sub_content_left = vec![]; @@ -2044,20 +2226,13 @@ impl<'b> ComponentWidgets<'b> { sub_content_right.push(render_icon_accessory(self.images, widget)); } - let sub_content_left: Element<_> = column(sub_content_left) - .width(Length::Fill) - .into(); + let sub_content_left: Element<_> = column(sub_content_left).width(Length::Fill).into(); - let sub_content_right: Element<_> = column(sub_content_right) - .width(Length::Shrink) - .into(); + let sub_content_right: Element<_> = column(sub_content_right).width(Length::Shrink).into(); - let sub_content: Element<_> = row(vec![sub_content_left, sub_content_right]) - .themed(RowStyle::GridItemTitle); + let sub_content: Element<_> = row(vec![sub_content_left, sub_content_right]).themed(RowStyle::GridItemTitle); - let content: Element<_> = column(vec![content, sub_content]) - .width(Length::Fill) - .into(); + let content: Element<_> = column(vec![content, sub_content]).width(Length::Fill).into(); content } @@ -2068,7 +2243,7 @@ impl<'b> ComponentWidgets<'b> { /*aspect_ratio: Option<&str>,*/ columns: &Option, item_focus_index: Option, - index_counter: &Cell + index_counter: &Cell, ) -> Element<'a, ComponentWidgetEvent> { // TODO // let (width, height) = match aspect_ratio { @@ -2107,8 +2282,7 @@ impl<'b> ComponentWidgets<'b> { } fn render_top_panel<'a>(&self, search_bar: &Option) -> Element<'a, ComponentWidgetEvent> { - let icon = value(Bootstrap::ArrowLeft) - .font(BOOTSTRAP_FONT); + let icon = value(Bootstrap::ArrowLeft).font(BOOTSTRAP_FONT); let back_button: Element<_> = button(icon) .on_press(ComponentWidgetEvent::PreviousView) @@ -2142,34 +2316,33 @@ impl<'b> ComponentWidgets<'b> { plugin_view_state: &PluginViewState, entrypoint_name: &str, action_shortcuts: &HashMap, - ) -> Element<'a, ComponentWidgetEvent> { - + ) -> Element<'a, ComponentWidgetEvent> { let top_panel = self.render_top_panel(search_bar); let top_separator = if is_loading { - LoadingBar::new() - .into() + LoadingBar::new().into() } else { - horizontal_rule(1) - .into() + horizontal_rule(1).into() }; let mut action_panel = convert_action_panel(action_panel, &action_shortcuts); - let primary_action = action_panel.as_mut() - .map(|panel| panel.find_first()) - .flatten() - .map(|(label, widget_id)| { - let shortcut = PhysicalShortcut { - physical_key: PhysicalKey::Enter, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: false - }; + let primary_action = + action_panel + .as_mut() + .map(|panel| panel.find_first()) + .flatten() + .map(|(label, widget_id)| { + let shortcut = PhysicalShortcut { + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }; - (label.to_string(), widget_id, shortcut) - }); + (label.to_string(), widget_id, shortcut) + }); match plugin_view_state { PluginViewState::None => { @@ -2183,9 +2356,23 @@ impl<'b> ComponentWidgets<'b> { action_panel, None::<&ScrollHandle>, entrypoint_name, - || ComponentWidgetEvent::ToggleActionPanel { widget_id: root_widget_id }, - |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id, id: focused_item_id.clone() }, - |widget_id| ComponentWidgetEvent::ActionClick { widget_id, id: focused_item_id.clone() }, + || { + ComponentWidgetEvent::ToggleActionPanel { + widget_id: root_widget_id, + } + }, + |widget_id| { + ComponentWidgetEvent::RunPrimaryAction { + widget_id, + id: focused_item_id.clone(), + } + }, + |widget_id| { + ComponentWidgetEvent::ActionClick { + widget_id, + id: focused_item_id.clone(), + } + }, || ComponentWidgetEvent::Noop, ) } @@ -2200,9 +2387,23 @@ impl<'b> ComponentWidgets<'b> { action_panel, Some(&focused_action_item), entrypoint_name, - || ComponentWidgetEvent::ToggleActionPanel { widget_id: root_widget_id }, - |widget_id| ComponentWidgetEvent::RunPrimaryAction { widget_id, id: focused_item_id.clone() }, - |widget_id| ComponentWidgetEvent::ActionClick { widget_id, id: focused_item_id.clone() }, + || { + ComponentWidgetEvent::ToggleActionPanel { + widget_id: root_widget_id, + } + }, + |widget_id| { + ComponentWidgetEvent::RunPrimaryAction { + widget_id, + id: focused_item_id.clone(), + } + }, + |widget_id| { + ComponentWidgetEvent::ActionClick { + widget_id, + id: focused_item_id.clone(), + } + }, || ComponentWidgetEvent::Noop, ) } @@ -2210,11 +2411,10 @@ impl<'b> ComponentWidgets<'b> { } } - #[derive(Clone, Debug, Eq, PartialEq)] struct SelectItem { value: String, - label: String + label: String, } impl Display for SelectItem { @@ -2223,31 +2423,27 @@ impl Display for SelectItem { } } - -fn render_metadata_item<'a>(label: &str, value: Element<'a, ComponentWidgetEvent>, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { +fn render_metadata_item<'a>( + label: &str, + value: Element<'a, ComponentWidgetEvent>, + is_in_list: bool, +) -> Element<'a, ComponentWidgetEvent> { let label: Element<_> = text(label.to_string()) .shaping(Shaping::Advanced) .themed(TextStyle::MetadataItemLabel); - let label = container(label) - .themed(ContainerStyle::MetadataItemLabel); + let label = container(label).themed(ContainerStyle::MetadataItemLabel); if is_in_list { - let space = horizontal_space() - .into(); + let space = horizontal_space().into(); - let value = container(value) - .themed(ContainerStyle::MetadataItemValueInList); + let value = container(value).themed(ContainerStyle::MetadataItemValueInList); - row(vec![label, space, value]) - .width(Length::Fill) - .into() + row(vec![label, space, value]).width(Length::Fill).into() } else { - let value = container(value) - .themed(ContainerStyle::MetadataItemValue); + let value = container(value).themed(ContainerStyle::MetadataItemValue); - column(vec![label, value]) - .into() + column(vec![label, value]).into() } } @@ -2255,8 +2451,14 @@ fn grid_width(columns: &Option) -> usize { columns.map(|value| value.trunc() as usize).unwrap_or(5) } - -fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option<&str>, subtitle: &Option, theme_kind_title: RowStyle, theme_kind_title_text: TextStyle, theme_kind_subtitle_text: TextStyle) -> Element<'a, ComponentWidgetEvent> { +fn render_section<'a>( + content: Element<'a, ComponentWidgetEvent>, + title: Option<&str>, + subtitle: &Option, + theme_kind_title: RowStyle, + theme_kind_title_text: TextStyle, + theme_kind_subtitle_text: TextStyle, +) -> Element<'a, ComponentWidgetEvent> { let mut title_content = vec![]; if let Some(title) = title { @@ -2278,25 +2480,20 @@ fn render_section<'a>(content: Element<'a, ComponentWidgetEvent>, title: Option< } if title_content.is_empty() { - let space: Element<_> = horizontal_space() - .height(40) - .into(); + let space: Element<_> = horizontal_space().height(40).into(); title_content.push(space) } - let title_content = row(title_content) - .themed(theme_kind_title); + let title_content = row(title_content).themed(theme_kind_title); - column([title_content, content]) - .into() + column([title_content, content]).into() } - #[derive(Debug)] pub struct ActionPanel { pub title: Option, - pub items: Vec + pub items: Vec, } impl ActionPanel { @@ -2314,33 +2511,29 @@ pub enum ActionPanelItem { Action { label: String, widget_id: UiWidgetId, - physical_shortcut: Option + physical_shortcut: Option, }, ActionSection { title: Option, - items: Vec - } + items: Vec, + }, } impl ActionPanelItem { fn action_count(&self) -> usize { match self { ActionPanelItem::Action { .. } => 1, - ActionPanelItem::ActionSection { items, .. } => { - items.iter().map(|item| item.action_count()).sum() - } + ActionPanelItem::ActionSection { items, .. } => items.iter().map(|item| item.action_count()).sum(), } } fn find_first(items: &[ActionPanelItem]) -> Option<(String, UiWidgetId)> { for item in items { match item { - ActionPanelItem::Action { label, widget_id, .. } => { - return Some((label.to_string(), *widget_id)) - } + ActionPanelItem::Action { label, widget_id, .. } => return Some((label.to_string(), *widget_id)), ActionPanelItem::ActionSection { items, .. } => { if let Some(item) = Self::find_first(items) { - return Some(item) + return Some(item); } } } @@ -2350,14 +2543,18 @@ impl ActionPanelItem { } } -fn convert_action_panel(action_panel: &Option, action_shortcuts: &HashMap) -> Option { +fn convert_action_panel( + action_panel: &Option, + action_shortcuts: &HashMap, +) -> Option { match action_panel { Some(ActionPanelWidget { content, title, .. }) => { - fn action_widget_to_action(ActionWidget { __id__, id, label }: &ActionWidget, action_shortcuts: &HashMap) -> ActionPanelItem { - let physical_shortcut: Option = id.as_ref() - .map(|id| action_shortcuts.get(id)) - .flatten() - .cloned(); + fn action_widget_to_action( + ActionWidget { __id__, id, label }: &ActionWidget, + action_shortcuts: &HashMap, + ) -> ActionPanelItem { + let physical_shortcut: Option = + id.as_ref().map(|id| action_shortcuts.get(id)).flatten().cloned(); ActionPanelItem::Action { label: label.clone(), @@ -2366,18 +2563,27 @@ fn convert_action_panel(action_panel: &Option, action_shortcu } } - let items = content.ordered_members.iter() + let items = content + .ordered_members + .iter() .map(|members| { match members { ActionPanelWidgetOrderedMembers::Action(widget) => { action_widget_to_action(widget, action_shortcuts) } - ActionPanelWidgetOrderedMembers::ActionPanelSection(ActionPanelSectionWidget { content, title, .. }) => { - let section_items = content.ordered_members + ActionPanelWidgetOrderedMembers::ActionPanelSection(ActionPanelSectionWidget { + content, + title, + .. + }) => { + let section_items = content + .ordered_members .iter() .map(|members| { match members { - ActionPanelSectionWidgetOrderedMembers::Action(widget) => action_widget_to_action(widget, action_shortcuts) + ActionPanelSectionWidgetOrderedMembers::Action(widget) => { + action_widget_to_action(widget, action_shortcuts) + } } }) .collect(); @@ -2396,7 +2602,7 @@ fn convert_action_panel(action_panel: &Option, action_shortcu items, }) } - _ => None + _ => None, } } @@ -2406,7 +2612,7 @@ fn render_action_panel_items<'a, T: 'a + Clone>( items: Vec, action_panel_focus_index: Option, on_action_click: &dyn Fn(UiWidgetId) -> T, - index_counter: &Cell + index_counter: &Cell, ) -> Vec> { let mut columns = vec![]; @@ -2419,14 +2625,18 @@ fn render_action_panel_items<'a, T: 'a + Clone>( }) .themed(TextStyle::ActionSectionTitle); - let text = container(text) - .themed(if root { ContainerStyle::ActionPanelTitle } else { ContainerStyle::ActionSectionTitle }); + let text = container(text).themed( + if root { + ContainerStyle::ActionPanelTitle + } else { + ContainerStyle::ActionSectionTitle + }, + ); columns.push(text) } else { if !root { - let separator: Element<_> = horizontal_rule(1) - .themed(RuleStyle::ActionPanel); + let separator: Element<_> = horizontal_rule(1).themed(RuleStyle::ActionPanel); columns.push(separator); } @@ -2434,44 +2644,46 @@ fn render_action_panel_items<'a, T: 'a + Clone>( for item in items { match item { - ActionPanelItem::Action { label, widget_id, physical_shortcut } => { - + ActionPanelItem::Action { + label, + widget_id, + physical_shortcut, + } => { let physical_shortcut = match index_counter.get() { - 0 => Some(PhysicalShortcut { // primary - physical_key: PhysicalKey::Enter, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: false, - }), - 1 => Some(PhysicalShortcut { // secondary - physical_key: PhysicalKey::Enter, - modifier_shift: true, - modifier_control: false, - modifier_alt: false, - modifier_meta: false, - }), - _ => physical_shortcut + 0 => { + Some(PhysicalShortcut { + // primary + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }) + } + 1 => { + Some(PhysicalShortcut { + // secondary + physical_key: PhysicalKey::Enter, + modifier_shift: true, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }) + } + _ => physical_shortcut, }; - let shortcut_element: Option> = physical_shortcut.as_ref() - .map(|shortcut| render_shortcut(shortcut)); + let shortcut_element: Option> = + physical_shortcut.as_ref().map(|shortcut| render_shortcut(shortcut)); let content: Element<_> = if let Some(shortcut_element) = shortcut_element { - let text: Element<_> = text(label) - .shaping(Shaping::Advanced) - .into(); + let text: Element<_> = text(label).shaping(Shaping::Advanced).into(); - let space: Element<_> = horizontal_space() - .into(); + let space: Element<_> = horizontal_space().into(); - row([text, space, shortcut_element]) - .align_y(Alignment::Center) - .into() + row([text, space, shortcut_element]).align_y(Alignment::Center).into() } else { - text(label) - .shaping(Shaping::Advanced) - .into() + text(label).shaping(Shaping::Advanced).into() }; let style = match action_panel_focus_index { @@ -2495,7 +2707,14 @@ fn render_action_panel_items<'a, T: 'a + Clone>( columns.push(content); } ActionPanelItem::ActionSection { title, items } => { - let content = render_action_panel_items(false, title, items, action_panel_focus_index, on_action_click, index_counter); + let content = render_action_panel_items( + false, + title, + items, + action_panel_focus_index, + on_action_click, + index_counter, + ); for content in content { columns.push(content); @@ -2512,18 +2731,23 @@ fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T>( on_action_click: F, action_panel_scroll_handle: &ScrollHandle, ) -> Element<'a, T> { - let columns = render_action_panel_items(true, action_panel.title, action_panel.items, action_panel_scroll_handle.index, &on_action_click, &Cell::new(0)); + let columns = render_action_panel_items( + true, + action_panel.title, + action_panel.items, + action_panel_scroll_handle.index, + &on_action_click, + &Cell::new(0), + ); - let actions: Element<_> = column(columns) - .into(); + let actions: Element<_> = column(columns).into(); let actions: Element<_> = scrollable(actions) .id(action_panel_scroll_handle.scrollable_id.clone()) .width(Length::Fill) .into(); - container(actions) - .themed(ContainerStyle::ActionPanel) + container(actions).themed(ContainerStyle::ActionPanel) } pub fn render_root<'a, T: 'a + Clone>( @@ -2540,12 +2764,10 @@ pub fn render_root<'a, T: 'a + Clone>( on_panel_primary_click: impl Fn(UiWidgetId) -> T, on_action_click: impl Fn(UiWidgetId) -> T, noop_msg: impl Fn() -> T, -) -> Element<'a, T> { - let entrypoint_name: Element<_> = text(entrypoint_name.to_string()) - .shaping(Shaping::Advanced) - .into(); +) -> Element<'a, T> { + let entrypoint_name: Element<_> = text(entrypoint_name.to_string()).shaping(Shaping::Advanced).into(); - let panel_height = 16 + 8 + 2; // TODO get value from theme + let panel_height = 16 + 8 + 2; // TODO get value from theme let primary_action = match primary_action { Some((label, widget_id, shortcut)) => { @@ -2553,33 +2775,29 @@ pub fn render_root<'a, T: 'a + Clone>( .shaping(Shaping::Advanced) .themed(TextStyle::RootBottomPanelPrimaryActionText); - let label: Element<_> = container(label) - .themed(ContainerStyle::RootBottomPanelPrimaryActionText); + let label: Element<_> = container(label).themed(ContainerStyle::RootBottomPanelPrimaryActionText); let shortcut = render_shortcut(&shortcut); - let content: Element<_> = row(vec![label, shortcut]) - .into(); + let content: Element<_> = row(vec![label, shortcut]).into(); let content: Element<_> = button(content) .on_press(on_panel_primary_click(widget_id)) .themed(ButtonStyle::RootBottomPanelPrimaryActionButton); - let content: Element<_> = container(content) - .themed(ContainerStyle::RootBottomPanelPrimaryActionButton); + let content: Element<_> = container(content).themed(ContainerStyle::RootBottomPanelPrimaryActionButton); Some(content) } - None => None + None => None, }; let (hide_action_panel, action_panel, bottom_panel) = match action_panel { Some(action_panel) => { - let actions_text: Element<_> = text("Actions") - .themed(TextStyle::RootBottomPanelActionToggleText); + let actions_text: Element<_> = text("Actions").themed(TextStyle::RootBottomPanelActionToggleText); - let actions_text: Element<_> = container(actions_text) - .themed(ContainerStyle::RootBottomPanelActionToggleText); + let actions_text: Element<_> = + container(actions_text).themed(ContainerStyle::RootBottomPanelActionToggleText); let shortcut = render_shortcut(&PhysicalShortcut { physical_key: PhysicalKey::KeyK, @@ -2592,23 +2810,19 @@ pub fn render_root<'a, T: 'a + Clone>( let mut bottom_panel_content = vec![entrypoint_name]; if let Some(toast_text) = toast_text { - let toast_text = text(toast_text.to_string()) - .into(); + let toast_text = text(toast_text.to_string()).into(); bottom_panel_content.push(toast_text); } - let space = horizontal_space() - .into(); + let space = horizontal_space().into(); bottom_panel_content.push(space); if let Some(primary_action) = primary_action { bottom_panel_content.push(primary_action); - let rule: Element<_> = vertical_rule(1) - .class(RuleStyle::PrimaryActionSeparator) - .into(); + let rule: Element<_> = vertical_rule(1).class(RuleStyle::PrimaryActionSeparator).into(); let rule: Element<_> = container(rule) .width(Length::Shrink) @@ -2619,8 +2833,7 @@ pub fn render_root<'a, T: 'a + Clone>( bottom_panel_content.push(rule); } - let action_panel_toggle_content: Element<_> = row(vec![actions_text, shortcut]) - .into(); + let action_panel_toggle_content: Element<_> = row(vec![actions_text, shortcut]).into(); let action_panel_toggle: Element<_> = button(action_panel_toggle_content) .on_press(on_panel_toggle_click()) @@ -2635,14 +2848,12 @@ pub fn render_root<'a, T: 'a + Clone>( (!show_action_panel, Some(action_panel), bottom_panel) } None => { - let space: Element<_> = Space::new(Length::Fill, panel_height) - .into(); + let space: Element<_> = Space::new(Length::Fill, panel_height).into(); let mut bottom_panel_content = vec![]; if let Some(toast_text) = toast_text { - let toast_text = text(toast_text.to_string()) - .into(); + let toast_text = text(toast_text.to_string()).into(); bottom_panel_content.push(toast_text); } else { @@ -2672,11 +2883,16 @@ pub fn render_root<'a, T: 'a + Clone>( .height(Length::Fill) .themed(ContainerStyle::RootInner); - let content: Element<_> = column(vec![top_panel, top_separator, content, bottom_panel]) - .into(); + let content: Element<_> = column(vec![top_panel, top_separator, content, bottom_panel]).into(); let content: Element<_> = mouse_area(content) - .on_press(if hide_action_panel { noop_msg() } else { on_panel_toggle_click() }) + .on_press( + if hide_action_panel { + noop_msg() + } else { + on_panel_toggle_click() + }, + ) .into(); let mut content = vec![content]; @@ -2685,7 +2901,7 @@ pub fn render_root<'a, T: 'a + Clone>( if !hide_action_panel { let action_panel = render_action_panel(action_panel, on_action_click, action_panel_scroll_handle); - let action_panel: Element<_>= container(action_panel) + let action_panel: Element<_> = container(action_panel) .padding(gauntlet_common_ui::padding(0.0, 8.0, 48.0, 0.0)) .align_right(Length::Fill) .align_bottom(Length::Fill) @@ -2695,32 +2911,23 @@ pub fn render_root<'a, T: 'a + Clone>( } }; - stack(content) - .into() + stack(content).into() } - fn render_shortcut<'a, T: 'a>(shortcut: &PhysicalShortcut) -> Element<'a, T> { let mut result = vec![]; - let ( - key_name, - alt_modifier_text, - meta_modifier_text, - control_modifier_text, - shift_modifier_text - ) = shortcut_to_text(shortcut); + let (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) = + shortcut_to_text(shortcut); fn apply_modifier<'result, 'element, T: 'element>( result: &'result mut Vec>, - modifier: Option> + modifier: Option>, ) { if let Some(modifier) = modifier { - let modifier: Element<_> = container(modifier) - .themed(ContainerStyle::ActionShortcutModifier); + let modifier: Element<_> = container(modifier).themed(ContainerStyle::ActionShortcutModifier); - let modifier: Element<_> = container(modifier) - .themed(ContainerStyle::ActionShortcutModifiersInit); + let modifier: Element<_> = container(modifier).themed(ContainerStyle::ActionShortcutModifiersInit); result.push(modifier); } @@ -2731,47 +2938,39 @@ fn render_shortcut<'a, T: 'a>(shortcut: &PhysicalShortcut) -> Element<'a, T> { apply_modifier(&mut result, shift_modifier_text); apply_modifier(&mut result, alt_modifier_text); - let key_name: Element<_> = container(key_name) - .themed(ContainerStyle::ActionShortcutModifier); + let key_name: Element<_> = container(key_name).themed(ContainerStyle::ActionShortcutModifier); result.push(key_name); - row(result) - .themed(RowStyle::ActionShortcut) + row(result).themed(RowStyle::ActionShortcut) } -fn render_image<'a, T: 'a + Clone>(images: &HashMap>, widget_id: UiWidgetId, image_data: &ImageLike, icon_style: Option) -> Element<'a, T> { +fn render_image<'a, T: 'a + Clone>( + images: &HashMap>, + widget_id: UiWidgetId, + image_data: &ImageLike, + icon_style: Option, +) -> Element<'a, T> { match image_data { ImageLike::ImageSource(_) => { match images.get(&widget_id) { - Some(bytes) => { - image(Handle::from_bytes(bytes.clone())) - .into() - } - None => { - horizontal_space() - .into() - } + Some(bytes) => image(Handle::from_bytes(bytes.clone())).into(), + None => horizontal_space().into(), } } ImageLike::Icons(icon) => { match icon_style { - None => { - value(icon_to_bootstrap(icon)) - .font(BOOTSTRAP_FONT) - .into() - } - Some(icon_style) => { - value(icon_to_bootstrap(icon)) - .font(BOOTSTRAP_FONT) - .themed(icon_style) - } + None => value(icon_to_bootstrap(icon)).font(BOOTSTRAP_FONT).into(), + Some(icon_style) => value(icon_to_bootstrap(icon)).font(BOOTSTRAP_FONT).themed(icon_style), } } } } -pub fn render_icon_accessory<'a, T: 'a + Clone>(images: &HashMap>, widget: &IconAccessoryWidget) -> Element<'a, T> { +pub fn render_icon_accessory<'a, T: 'a + Clone>( + images: &HashMap>, + widget: &IconAccessoryWidget, +) -> Element<'a, T> { let icon = render_image(images, widget.__id__, &widget.icon, Some(TextStyle::IconAccessory)); let content = container(icon) @@ -2782,18 +2981,19 @@ pub fn render_icon_accessory<'a, T: 'a + Clone>(images: &HashMap content, Some(tooltip_text) => { - let tooltip_text: Element<_> = text(tooltip_text.to_string()) - .shaping(Shaping::Advanced) - .into(); + let tooltip_text: Element<_> = text(tooltip_text.to_string()).shaping(Shaping::Advanced).into(); - tooltip(content, tooltip_text, Position::Top) - .themed(TooltipStyle::Tooltip) + tooltip(content, tooltip_text, Position::Top).themed(TooltipStyle::Tooltip) } } } -pub fn render_text_accessory<'a, T: 'a + Clone>(images: &HashMap>, widget: &TextAccessoryWidget) -> Element<'a, T> { - let icon: Option> = widget.icon +pub fn render_text_accessory<'a, T: 'a + Clone>( + images: &HashMap>, + widget: &TextAccessoryWidget, +) -> Element<'a, T> { + let icon: Option> = widget + .icon .as_ref() .map(|icon| render_image(images, widget.__id__, icon, Some(TextStyle::TextAccessory))); @@ -2804,17 +3004,14 @@ pub fn render_text_accessory<'a, T: 'a + Clone>(images: &HashMap> = vec![]; if let Some(icon) = icon { - let icon: Element<_> = container(icon) - .themed(ContainerStyle::TextAccessoryIcon); + let icon: Element<_> = container(icon).themed(ContainerStyle::TextAccessoryIcon); content.push(icon) } content.push(text_content); - let content: Element<_> = row(content) - .align_y(Alignment::Center) - .into(); + let content: Element<_> = row(content).align_y(Alignment::Center).into(); let content = container(content) .align_x(Horizontal::Center) @@ -2824,63 +3021,59 @@ pub fn render_text_accessory<'a, T: 'a + Clone>(images: &HashMap content, Some(tooltip_text) => { - let tooltip_text: Element<_> = text(tooltip_text.to_string()) - .shaping(Shaping::Advanced) - .into(); + let tooltip_text: Element<_> = text(tooltip_text.to_string()).shaping(Shaping::Advanced).into(); - tooltip(content, tooltip_text, Position::Top) - .themed(TooltipStyle::Tooltip) + tooltip(content, tooltip_text, Position::Top).themed(TooltipStyle::Tooltip) } } } - #[derive(Clone, Debug)] pub enum ComponentWidgetEvent { LinkClick { widget_id: UiWidgetId, - href: String + href: String, }, TagClick { widget_id: UiWidgetId, }, ActionClick { widget_id: UiWidgetId, - id: Option + id: Option, }, RunAction { widget_id: UiWidgetId, - id: Option + id: Option, }, ToggleDatePicker { widget_id: UiWidgetId, }, OnChangeTextField { widget_id: UiWidgetId, - value: String + value: String, }, OnChangePasswordField { widget_id: UiWidgetId, - value: String + value: String, }, OnChangeSearchBar { widget_id: UiWidgetId, - value: String + value: String, }, SubmitDatePicker { widget_id: UiWidgetId, - value: String + value: String, }, CancelDatePicker { widget_id: UiWidgetId, }, ToggleCheckbox { widget_id: UiWidgetId, - value: bool + value: bool, }, SelectPickList { widget_id: UiWidgetId, - value: String + value: String, }, ToggleActionPanel { widget_id: UiWidgetId, @@ -2906,21 +3099,19 @@ include!(concat!(env!("OUT_DIR"), "/components.rs")); impl ComponentWidgetEvent { pub fn handle(self, _plugin_id: PluginId, state: Option<&mut ComponentWidgetState>) -> Option { match self { - ComponentWidgetEvent::LinkClick { widget_id: _, href } => { - Some(UiViewEvent::Open { - href - }) - } - ComponentWidgetEvent::TagClick { widget_id } => { - Some(create_metadata_tag_item_on_click_event(widget_id)) - } + ComponentWidgetEvent::LinkClick { widget_id: _, href } => Some(UiViewEvent::Open { href }), + ComponentWidgetEvent::TagClick { widget_id } => Some(create_metadata_tag_item_on_click_event(widget_id)), ComponentWidgetEvent::RunAction { widget_id, id } | ComponentWidgetEvent::ActionClick { widget_id, id } => { Some(create_action_on_action_event(widget_id, id)) } ComponentWidgetEvent::ToggleDatePicker { widget_id } => { let state = state.expect("state should always exist for "); - let ComponentWidgetState::DatePicker(DatePickerState { state_value: _, show_picker }) = state else { + let ComponentWidgetState::DatePicker(DatePickerState { + state_value: _, + show_picker, + }) = state + else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; @@ -2930,7 +3121,11 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::CancelDatePicker { widget_id } => { let state = state.expect("state should always exist for "); - let ComponentWidgetState::DatePicker(DatePickerState { state_value: _, show_picker }) = state else { + let ComponentWidgetState::DatePicker(DatePickerState { + state_value: _, + show_picker, + }) = state + else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; @@ -2941,7 +3136,11 @@ impl ComponentWidgetEvent { let state = state.expect("state should always exist for "); { - let ComponentWidgetState::DatePicker(DatePickerState { state_value: _, show_picker }) = state else { + let ComponentWidgetState::DatePicker(DatePickerState { + state_value: _, + show_picker, + }) = state + else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; @@ -3017,21 +3216,23 @@ impl ComponentWidgetEvent { } ComponentWidgetEvent::ToggleActionPanel { .. } => { Some(UiViewEvent::AppEvent { - event: AppMsg::ToggleActionPanel { keyboard: false } + event: AppMsg::ToggleActionPanel { keyboard: false }, }) } - ComponentWidgetEvent::FocusListItem { list_widget_id, item_id } => { - Some(create_list_on_item_focus_change_event(list_widget_id, item_id)) - } - ComponentWidgetEvent::FocusGridItem { grid_widget_id, item_id } => { - Some(create_grid_on_item_focus_change_event(grid_widget_id, item_id)) - } + ComponentWidgetEvent::FocusListItem { + list_widget_id, + item_id, + } => Some(create_list_on_item_focus_change_event(list_widget_id, item_id)), + ComponentWidgetEvent::FocusGridItem { + grid_widget_id, + item_id, + } => Some(create_grid_on_item_focus_change_event(grid_widget_id, item_id)), ComponentWidgetEvent::Noop | ComponentWidgetEvent::PreviousView => { panic!("widget_id on these events is not supposed to be called") } ComponentWidgetEvent::RunPrimaryAction { widget_id, id } => { Some(UiViewEvent::AppEvent { - event: AppMsg::OnAnyActionPluginViewAnyPanel { widget_id, id } + event: AppMsg::OnAnyActionPluginViewAnyPanel { widget_id, id }, }) } } @@ -3055,14 +3256,16 @@ impl ComponentWidgetEvent { ComponentWidgetEvent::FocusListItem { list_widget_id, .. } => list_widget_id, ComponentWidgetEvent::FocusGridItem { grid_widget_id, .. } => grid_widget_id, ComponentWidgetEvent::RunPrimaryAction { widget_id, .. } => widget_id, - ComponentWidgetEvent::Noop | ComponentWidgetEvent::PreviousView => panic!("widget_id on these events is not supposed to be called"), - }.to_owned() + ComponentWidgetEvent::Noop | ComponentWidgetEvent::PreviousView => { + panic!("widget_id on these events is not supposed to be called") + } + } + .to_owned() } } pub fn parse_date(value: &str) -> Option<(i32, u32, u32)> { - let ymd: Vec<_> = value.split("-") - .collect(); + let ymd: Vec<_> = value.split("-").collect(); match ymd[..] { [year, month, day] => { @@ -3072,10 +3275,10 @@ pub fn parse_date(value: &str) -> Option<(i32, u32, u32)> { match (year, month, day) { (Ok(year), Ok(month), Ok(day)) => Some((year, month, day)), - _ => None + _ => None, } } - _ => None + _ => None, } } diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index b839cc5..401bbff 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -1,14 +1,26 @@ use std::collections::hash_map::Entry; -use crate::model::UiViewEvent; -use crate::ui::state::PluginViewState; -use crate::ui::theme::Element; -use crate::ui::widget::{create_state, ActionPanel, ComponentWidgetEvent, ComponentWidgetState, ComponentWidgets, ComponentWidgetsMut}; -use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, RootWidget, UiWidgetId}; use std::collections::HashMap; use std::mem; use std::ops::DerefMut; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use std::sync::Mutex; + +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::RootWidget; +use gauntlet_common::model::UiWidgetId; use iced::Task; + +use crate::model::UiViewEvent; +use crate::ui::state::PluginViewState; +use crate::ui::theme::Element; +use crate::ui::widget::create_state; +use crate::ui::widget::ActionPanel; +use crate::ui::widget::ComponentWidgetEvent; +use crate::ui::widget::ComponentWidgetState; +use crate::ui::widget::ComponentWidgets; +use crate::ui::widget::ComponentWidgetsMut; use crate::ui::AppMsg; pub struct PluginWidgetContainer { @@ -18,7 +30,7 @@ pub struct PluginWidgetContainer { plugin_id: Option, plugin_name: Option, entrypoint_id: Option, - entrypoint_name: Option + entrypoint_name: Option, } impl PluginWidgetContainer { @@ -35,11 +47,15 @@ impl PluginWidgetContainer { } pub fn get_plugin_id(&self) -> PluginId { - self.plugin_id.clone().expect("plugin id should always exist after render") + self.plugin_id + .clone() + .expect("plugin id should always exist after render") } pub fn get_entrypoint_id(&self) -> EntrypointId { - self.entrypoint_id.clone().expect("entrypoint id should always exist after render") + self.entrypoint_id + .clone() + .expect("entrypoint id should always exist after render") } pub fn replace_view( @@ -49,7 +65,7 @@ impl PluginWidgetContainer { plugin_id: &PluginId, plugin_name: &str, entrypoint_id: &EntrypointId, - entrypoint_name: &str + entrypoint_name: &str, ) -> AppMsg { tracing::trace!("replace_view is called. container: {:?}", container); @@ -74,14 +90,13 @@ impl PluginWidgetContainer { let first_open = match self.root_widget.as_ref() { None => true, - Some(root_widget) => root_widget.content.is_none() + Some(root_widget) => root_widget.content.is_none(), }; self.root_widget = Some(container); if first_open { - ComponentWidgets::new(&mut self.root_widget, &mut self.state, plugin_id.clone(), &self.images) - .first_open() + ComponentWidgets::new(&mut self.root_widget, &mut self.state, plugin_id.clone(), &self.images).first_open() } else { AppMsg::Noop } @@ -98,8 +113,11 @@ impl PluginWidgetContainer { plugin_view_state: &PluginViewState, action_shortcuts: &HashMap, ) -> Element<'a, ComponentWidgetEvent> { - ComponentWidgets::new(&self.root_widget, &self.state, self.get_plugin_id(), &self.images) - .render_root_widget(plugin_view_state, self.entrypoint_name.as_ref(), action_shortcuts) + ComponentWidgets::new(&self.root_widget, &self.state, self.get_plugin_id(), &self.images).render_root_widget( + plugin_view_state, + self.entrypoint_name.as_ref(), + action_shortcuts, + ) } pub fn render_inline_root_widget<'a>(&self) -> Element<'a, ComponentWidgetEvent> { @@ -109,36 +127,30 @@ impl PluginWidgetContainer { pub fn append_text(&mut self, text: &str) -> Task { let plugin_id = self.get_plugin_id(); - ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) - .append_text(text) + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images).append_text(text) } pub fn backspace_text(&mut self) -> Task { let plugin_id = self.get_plugin_id(); - ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) - .backspace_text() + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images).backspace_text() } pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Task { let plugin_id = self.get_plugin_id(); - ComponentWidgets::new(&self.root_widget, &self.state, plugin_id, &self.images) - .focus_search_bar(widget_id) + ComponentWidgets::new(&self.root_widget, &self.state, plugin_id, &self.images).focus_search_bar(widget_id) } pub fn toggle_action_panel(&mut self) { let plugin_id = self.get_plugin_id(); - ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) - .toggle_action_panel() + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images).toggle_action_panel() } pub fn get_action_ids(&self) -> Vec { - ComponentWidgets::new(&self.root_widget, &self.state, self.get_plugin_id(), &self.images) - .get_action_ids() + ComponentWidgets::new(&self.root_widget, &self.state, self.get_plugin_id(), &self.images).get_action_ids() } pub fn get_focused_item_id(&self) -> Option { - ComponentWidgets::new(&self.root_widget, &self.state, self.get_plugin_id(), &self.images) - .get_focused_item_id() + ComponentWidgets::new(&self.root_widget, &self.state, self.get_plugin_id(), &self.images).get_focused_item_id() } pub fn get_action_panel(&self, action_shortcuts: &HashMap) -> Option { @@ -148,25 +160,21 @@ impl PluginWidgetContainer { pub fn focus_up(&mut self) -> Task { let plugin_id = self.get_plugin_id(); - ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) - .focus_up() + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images).focus_up() } pub fn focus_down(&mut self) -> Task { let plugin_id = self.get_plugin_id(); - ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) - .focus_down() + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images).focus_down() } pub fn focus_left(&mut self) -> Task { let plugin_id = self.get_plugin_id(); - ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) - .focus_left() + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images).focus_left() } pub fn focus_right(&mut self) -> Task { let plugin_id = self.get_plugin_id(); - ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images) - .focus_right() + ComponentWidgetsMut::new(&mut self.root_widget, &mut self.state, plugin_id, &self.images).focus_right() } } diff --git a/rust/common/build.rs b/rust/common/build.rs index eb94ff8..70cc682 100644 --- a/rust/common/build.rs +++ b/rust/common/build.rs @@ -3,19 +3,26 @@ use std::fs::File; use std::io::Write; use std::ops::Deref; use std::path::Path; -use gauntlet_component_model::{create_component_model, Arity, Children, Component, ComponentName, ComponentRef, Property, PropertyKind, PropertyType, SharedType}; -use itertools::Itertools; -use convert_case::{Case, Casing}; +use convert_case::Case; +use convert_case::Casing; +use gauntlet_component_model::create_component_model; +use gauntlet_component_model::Arity; +use gauntlet_component_model::Children; +use gauntlet_component_model::Component; +use gauntlet_component_model::ComponentName; +use gauntlet_component_model::ComponentRef; +use gauntlet_component_model::Property; +use gauntlet_component_model::PropertyKind; +use gauntlet_component_model::PropertyType; +use gauntlet_component_model::SharedType; use indexmap::IndexMap; +use itertools::Itertools; fn main() -> Result<(), Box> { tonic_build::configure() .protoc_arg("--experimental_allow_proto3_optional") - .compile_protos( - &["./../../schema/backend.proto"], - &["./../../schema/"], - )?; + .compile_protos(&["./../../schema/backend.proto"], &["./../../schema/"])?; component_model_generator()?; @@ -32,14 +39,25 @@ fn component_model_generator() -> Result<(), Box> { for component in &components { match component { - Component::Standard { name, props, children, .. } => { - let props_has_content = props.iter().any(|prop| matches!(prop.property_type.kind(), PropertyKind::Component)); + Component::Standard { + name, props, children, .. + } => { + let props_has_content = props + .iter() + .any(|prop| matches!(prop.property_type.kind(), PropertyKind::Component)); let children_has_content = match children { - Children::Members { ordered_members, per_type_members, .. } | Children::StringOrMembers { ordered_members, per_type_members, .. } => { - !ordered_members.is_empty() || !per_type_members.is_empty() + Children::Members { + ordered_members, + per_type_members, + .. } - _ => false + | Children::StringOrMembers { + ordered_members, + per_type_members, + .. + } => !ordered_members.is_empty() || !per_type_members.is_empty(), + _ => false, }; let has_text = matches!(children, Children::StringOrMembers { .. } | Children::String { .. }); @@ -49,10 +67,17 @@ fn component_model_generator() -> Result<(), Box> { let default = IndexMap::new(); let (ordered_members, per_type_members) = match children { - Children::Members { ordered_members, per_type_members, .. } | Children::StringOrMembers { ordered_members, per_type_members, .. } => { - (ordered_members, per_type_members) + Children::Members { + ordered_members, + per_type_members, + .. } - _ => (&default, &default) + | Children::StringOrMembers { + ordered_members, + per_type_members, + .. + } => (ordered_members, per_type_members), + _ => (&default, &default), }; if !ordered_members.is_empty() { @@ -66,7 +91,10 @@ fn component_model_generator() -> Result<(), Box> { .collect::>(); for component_ref in &unique_component_refs { - output.push_str(&format!(" {}({}Widget),\n", component_ref.component_name, component_ref.component_name)); + output.push_str(&format!( + " {}({}Widget),\n", + component_ref.component_name, component_ref.component_name + )); } output.push_str("}\n"); @@ -82,13 +110,24 @@ fn component_model_generator() -> Result<(), Box> { let is_union = match &prop.property_type { PropertyType::Union { .. } => true, PropertyType::Array { item } => matches!(item.as_ref(), PropertyType::Union { .. }), - _ => false + _ => false, }; if is_union { - output.push_str(&format!(" pub {}: {},\n", prop.name.to_case(Case::Snake), generate_required_type(&prop.property_type, Some(format!("{}{}", name, &prop.name.to_case(Case::Pascal)))))); + output.push_str(&format!( + " pub {}: {},\n", + prop.name.to_case(Case::Snake), + generate_required_type( + &prop.property_type, + Some(format!("{}{}", name, &prop.name.to_case(Case::Pascal))) + ) + )); } else { - output.push_str(&format!(" pub {}: {},\n", prop.name.to_case(Case::Snake), generate_type(&prop, name))); + output.push_str(&format!( + " pub {}: {},\n", + prop.name.to_case(Case::Snake), + generate_type(&prop, name) + )); } } } @@ -96,10 +135,18 @@ fn component_model_generator() -> Result<(), Box> { for (member_name, component_ref) in per_type_members { match component_ref.arity { Arity::ZeroOrOne => { - output.push_str(&format!(" pub {}: Option<{}Widget>,\n", member_name.to_case(Case::Snake), component_ref.component_name)); + output.push_str(&format!( + " pub {}: Option<{}Widget>,\n", + member_name.to_case(Case::Snake), + component_ref.component_name + )); } Arity::One => { - output.push_str(&format!(" pub {}: {}Widget,\n", member_name.to_case(Case::Snake), component_ref.component_name)); + output.push_str(&format!( + " pub {}: {}Widget,\n", + member_name.to_case(Case::Snake), + component_ref.component_name + )); } Arity::ZeroOrMore => { todo!() @@ -108,7 +155,10 @@ fn component_model_generator() -> Result<(), Box> { } if !ordered_members.is_empty() { - output.push_str(&format!(" pub ordered_members: Vec<{}WidgetOrderedMembers>,\n", name)); + output.push_str(&format!( + " pub ordered_members: Vec<{}WidgetOrderedMembers>,\n", + name + )); } if has_text { @@ -137,7 +187,7 @@ fn component_model_generator() -> Result<(), Box> { let is_union = match &prop.property_type { PropertyType::Union { .. } => true, PropertyType::Array { item } => matches!(item.as_ref(), PropertyType::Union { .. }), - _ => false + _ => false, }; let prop_name = prop.name.to_case(Case::Snake); @@ -154,7 +204,7 @@ fn component_model_generator() -> Result<(), Box> { PropertyType::Union { items } => { items.iter().flat_map(|prop| all_component_refs(prop)).collect() } - PropertyType::Array { item } => all_component_refs(item) + PropertyType::Array { item } => all_component_refs(item), } } @@ -163,16 +213,15 @@ fn component_model_generator() -> Result<(), Box> { match &prop.property_type { PropertyType::Component { reference } => { prop_other_component_refs.insert(prop_name, reference); - }, + } PropertyType::Array { item, .. } => { match item.as_ref() { PropertyType::Component { reference } => { prop_other_component_refs.insert(prop_name, reference); - - }, + } _ => {} } - }, + } _ => {} } } @@ -185,7 +234,9 @@ fn component_model_generator() -> Result<(), Box> { { output.push_str(&format!("impl<'de> Deserialize<'de> for {}WidgetContent {{\n", name)); - output.push_str(&format!(" fn deserialize(deserializer: D) -> Result\n")); + output.push_str(&format!( + " fn deserialize(deserializer: D) -> Result\n" + )); output.push_str(&format!(" where\n")); output.push_str(&format!(" D: Deserializer<'de>,\n")); output.push_str(&format!(" {{\n")); @@ -197,18 +248,32 @@ fn component_model_generator() -> Result<(), Box> { for (_, prop_union_component_refs) in &prop_union_component_refs { for prop_union_component_ref in prop_union_component_refs { - output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", prop_union_component_ref.component_internal_name)); - output.push_str(&format!(" {}({}Widget),\n", prop_union_component_ref.component_name, prop_union_component_ref.component_name)); + output.push_str(&format!( + " #[serde(rename = \"gauntlet:{}\")]\n", + prop_union_component_ref.component_internal_name + )); + output.push_str(&format!( + " {}({}Widget),\n", + prop_union_component_ref.component_name, + prop_union_component_ref.component_name + )); } } for component_ref in &component_refs { - output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", component_ref.component_internal_name)); - output.push_str(&format!(" {}({}Widget),\n", component_ref.component_name, component_ref.component_name)); + output.push_str(&format!( + " #[serde(rename = \"gauntlet:{}\")]\n", + component_ref.component_internal_name + )); + output.push_str(&format!( + " {}({}Widget),\n", + component_ref.component_name, component_ref.component_name + )); } if has_text { - output.push_str(&format!(" #[serde(rename = \"gauntlet:text_part\")]\n")); + output + .push_str(&format!(" #[serde(rename = \"gauntlet:text_part\")]\n")); output.push_str(&format!(" Text {{\n")); output.push_str(&format!(" value: String\n")); output.push_str(&format!(" }},\n")); @@ -217,7 +282,10 @@ fn component_model_generator() -> Result<(), Box> { output.push_str(" }\n"); } - output.push_str(&format!(" let mut members = Vec::<{}WidgetMembersOwned>::deserialize(deserializer)?;\n", name)); + output.push_str(&format!( + " let mut members = Vec::<{}WidgetMembersOwned>::deserialize(deserializer)?;\n", + name + )); output.push_str("\n"); for (prop_name, _) in &prop_other_component_refs { @@ -229,7 +297,10 @@ fn component_model_generator() -> Result<(), Box> { } for per_type_component_ref in &per_type_component_refs { - output.push_str(&format!(" let mut {}: Option<_> = None;\n", per_type_component_ref.component_internal_name)); + output.push_str(&format!( + " let mut {}: Option<_> = None;\n", + per_type_component_ref.component_internal_name + )); } if !ordered_members.is_empty() { @@ -247,16 +318,31 @@ fn component_model_generator() -> Result<(), Box> { output.push_str(" match member {\n"); for (prop_name, prop_union_component_refs) in &prop_union_component_refs { - for (index, prop_union_component_ref) in prop_union_component_refs.iter().enumerate() { - output.push_str(&format!(" {}WidgetMembersOwned::{}(widget) => {{\n", name, prop_union_component_ref.component_name)); - output.push_str(&format!(" {}.push({}{}::_{}(widget));\n", prop_name, name, prop_name.to_case(Case::Pascal), index)); + for (index, prop_union_component_ref) in + prop_union_component_refs.iter().enumerate() + { + output.push_str(&format!( + " {}WidgetMembersOwned::{}(widget) => {{\n", + name, prop_union_component_ref.component_name + )); + output.push_str(&format!( + " {}.push({}{}::_{}(widget));\n", + prop_name, + name, + prop_name.to_case(Case::Pascal), + index + )); output.push_str(&format!(" }}\n")); } } for (prop_name, prop_other_component_refs) in &prop_other_component_refs { - output.push_str(&format!(" {}WidgetMembersOwned::{}(widget) => {{\n", name, prop_other_component_refs.component_name)); - output.push_str(&format!(" if let Some(_) = {} {{\n", prop_name)); + output.push_str(&format!( + " {}WidgetMembersOwned::{}(widget) => {{\n", + name, prop_other_component_refs.component_name + )); + output + .push_str(&format!(" if let Some(_) = {} {{\n", prop_name)); output.push_str(&format!(" return Err(Error::custom(\"Only one {} is allowed\"))\n", prop_other_component_refs.component_name)); output.push_str(&format!(" }}\n")); output.push_str(&format!(" {} = Some(widget);\n", prop_name)); @@ -264,22 +350,37 @@ fn component_model_generator() -> Result<(), Box> { } for per_type_component_ref in &per_type_component_refs { - output.push_str(&format!(" {}WidgetMembersOwned::{}(widget) => {{\n", name, per_type_component_ref.component_name)); - output.push_str(&format!(" if let Some(_) = {} {{\n", per_type_component_ref.component_internal_name)); + output.push_str(&format!( + " {}WidgetMembersOwned::{}(widget) => {{\n", + name, per_type_component_ref.component_name + )); + output.push_str(&format!( + " if let Some(_) = {} {{\n", + per_type_component_ref.component_internal_name + )); output.push_str(&format!(" return Err(Error::custom(\"Only one {} is allowed\"))\n", per_type_component_ref.component_name)); output.push_str(&format!(" }}\n")); - output.push_str(&format!(" {} = Some(widget);\n", per_type_component_ref.component_internal_name)); + output.push_str(&format!( + " {} = Some(widget);\n", + per_type_component_ref.component_internal_name + )); output.push_str(&format!(" }}\n")); } for ordered_component_ref in &unique_ordered_component_refs { - output.push_str(&format!(" {}WidgetMembersOwned::{}(widget) => {{\n", name, ordered_component_ref.component_name)); + output.push_str(&format!( + " {}WidgetMembersOwned::{}(widget) => {{\n", + name, ordered_component_ref.component_name + )); output.push_str(&format!(" ordered_members.insert(0, {}WidgetOrderedMembers::{}(widget));\n", name, ordered_component_ref.component_name)); output.push_str(&format!(" }}\n")); } if has_text { - output.push_str(&format!(" {}WidgetMembersOwned::Text {{ value }} => {{\n", name)); + output.push_str(&format!( + " {}WidgetMembersOwned::Text {{ value }} => {{\n", + name + )); output.push_str(&format!(" text.insert(0, value);\n")); output.push_str(&format!(" }}\n")); } @@ -298,10 +399,18 @@ fn component_model_generator() -> Result<(), Box> { for per_type_component_ref in &per_type_component_refs { match per_type_component_ref.arity { Arity::ZeroOrOne => { - output.push_str(&format!(" {},\n", per_type_component_ref.component_internal_name)); + output.push_str(&format!( + " {},\n", + per_type_component_ref.component_internal_name + )); } Arity::One => { - output.push_str(&format!(" {}: {}.ok_or(Error::custom(\"{} is required\"))?,\n", per_type_component_ref.component_internal_name, per_type_component_ref.component_internal_name, per_type_component_ref.component_name)); + output.push_str(&format!( + " {}: {}.ok_or(Error::custom(\"{} is required\"))?,\n", + per_type_component_ref.component_internal_name, + per_type_component_ref.component_internal_name, + per_type_component_ref.component_name + )); } Arity::ZeroOrMore => { todo!() @@ -315,7 +424,12 @@ fn component_model_generator() -> Result<(), Box> { output.push_str(&format!(" {},\n", prop_name)); } Arity::One => { - output.push_str(&format!(" {}: {}.ok_or(Error::custom(\"{} is required\"))?,\n", prop_name, prop_other_component_ref.component_internal_name, prop_other_component_ref.component_name)); + output.push_str(&format!( + " {}: {}.ok_or(Error::custom(\"{} is required\"))?,\n", + prop_name, + prop_other_component_ref.component_internal_name, + prop_other_component_ref.component_name + )); } Arity::ZeroOrMore => { todo!() @@ -337,9 +451,10 @@ fn component_model_generator() -> Result<(), Box> { } { - output.push_str(&format!("impl Serialize for {}WidgetContent {{\n", name)); - output.push_str(&format!(" fn serialize(&self, serializer: S) -> Result\n")); + output.push_str(&format!( + " fn serialize(&self, serializer: S) -> Result\n" + )); output.push_str(&format!(" where\n")); output.push_str(&format!(" S: Serializer\n")); output.push_str(&format!(" {{\n")); @@ -351,18 +466,32 @@ fn component_model_generator() -> Result<(), Box> { for (_, prop_union_component_refs) in &prop_union_component_refs { for prop_union_component_ref in prop_union_component_refs { - output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", prop_union_component_ref.component_internal_name)); - output.push_str(&format!(" {}(&'a {}Widget),\n", prop_union_component_ref.component_name, prop_union_component_ref.component_name)); + output.push_str(&format!( + " #[serde(rename = \"gauntlet:{}\")]\n", + prop_union_component_ref.component_internal_name + )); + output.push_str(&format!( + " {}(&'a {}Widget),\n", + prop_union_component_ref.component_name, + prop_union_component_ref.component_name + )); } } for component_ref in &component_refs { - output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", component_ref.component_internal_name)); - output.push_str(&format!(" {}(&'a {}Widget),\n", component_ref.component_name, component_ref.component_name)); + output.push_str(&format!( + " #[serde(rename = \"gauntlet:{}\")]\n", + component_ref.component_internal_name + )); + output.push_str(&format!( + " {}(&'a {}Widget),\n", + component_ref.component_name, component_ref.component_name + )); } if has_text { - output.push_str(&format!(" #[serde(rename = \"gauntlet:text_part\")]\n")); + output + .push_str(&format!(" #[serde(rename = \"gauntlet:text_part\")]\n")); output.push_str(&format!(" Text {{\n")); output.push_str(&format!(" value: &'a String\n")); output.push_str(&format!(" }},\n")); @@ -371,17 +500,27 @@ fn component_model_generator() -> Result<(), Box> { output.push_str(" }\n"); } - output.push_str(&format!(" let mut members = Vec::<{}WidgetMembersRef>::new();\n", name)); + output.push_str(&format!( + " let mut members = Vec::<{}WidgetMembersRef>::new();\n", + name + )); output.push_str("\n"); - for (prop_name, prop_union_component_refs) in &prop_union_component_refs { output.push_str(&format!(" for item in &self.{} {{\n", prop_name)); output.push_str(&format!(" match item {{\n")); for (index, prop_union_component_ref) in prop_union_component_refs.iter().enumerate() { - output.push_str(&format!(" {}{}::_{}(widget) => {{\n", name, prop_name.to_case(Case::Pascal), index)); - output.push_str(&format!(" members.push({}WidgetMembersRef::{}(widget));\n", name, prop_union_component_ref.component_name)); + output.push_str(&format!( + " {}{}::_{}(widget) => {{\n", + name, + prop_name.to_case(Case::Pascal), + index + )); + output.push_str(&format!( + " members.push({}WidgetMembersRef::{}(widget));\n", + name, prop_union_component_ref.component_name + )); output.push_str(&format!(" }}\n")); } @@ -392,12 +531,21 @@ fn component_model_generator() -> Result<(), Box> { for (prop_name, prop_other_component_refs) in &prop_other_component_refs { match prop_other_component_refs.arity { Arity::ZeroOrOne => { - output.push_str(&format!(" if let Some({}) = &self.{} {{\n", prop_name, prop_name)); - output.push_str(&format!(" members.push({}WidgetMembersRef::{}({}))\n", name, prop_other_component_refs.component_name, prop_name)); + output.push_str(&format!( + " if let Some({}) = &self.{} {{\n", + prop_name, prop_name + )); + output.push_str(&format!( + " members.push({}WidgetMembersRef::{}({}))\n", + name, prop_other_component_refs.component_name, prop_name + )); output.push_str(&format!(" }}\n")); } Arity::One => { - output.push_str(&format!(" members.push({}WidgetMembersRef::{}(&self.{}))\n", name, prop_other_component_refs.component_name, prop_name)); + output.push_str(&format!( + " members.push({}WidgetMembersRef::{}(&self.{}))\n", + name, prop_other_component_refs.component_name, prop_name + )); } Arity::ZeroOrMore => { todo!() @@ -408,12 +556,23 @@ fn component_model_generator() -> Result<(), Box> { for per_type_component_ref in &per_type_component_refs { match per_type_component_ref.arity { Arity::ZeroOrOne => { - output.push_str(&format!(" if let Some(item) = &self.{} {{\n", per_type_component_ref.component_internal_name)); - output.push_str(&format!(" members.push({}WidgetMembersRef::{}(item))\n", name, per_type_component_ref.component_name)); + output.push_str(&format!( + " if let Some(item) = &self.{} {{\n", + per_type_component_ref.component_internal_name + )); + output.push_str(&format!( + " members.push({}WidgetMembersRef::{}(item))\n", + name, per_type_component_ref.component_name + )); output.push_str(&format!(" }}\n")); } Arity::One => { - output.push_str(&format!(" members.push({}WidgetMembersRef::{}(&self.{}));\n", name, per_type_component_ref.component_name, per_type_component_ref.component_internal_name)); + output.push_str(&format!( + " members.push({}WidgetMembersRef::{}(&self.{}));\n", + name, + per_type_component_ref.component_name, + per_type_component_ref.component_internal_name + )); } Arity::ZeroOrMore => { todo!() @@ -426,8 +585,14 @@ fn component_model_generator() -> Result<(), Box> { output.push_str(&format!(" match member {{\n")); for ordered_component_ref in &unique_ordered_component_refs { - output.push_str(&format!(" {}WidgetOrderedMembers::{}(widget) => {{\n", name, ordered_component_ref.component_name)); - output.push_str(&format!(" members.push({}WidgetMembersRef::{}(widget))\n", name, ordered_component_ref.component_name)); + output.push_str(&format!( + " {}WidgetOrderedMembers::{}(widget) => {{\n", + name, ordered_component_ref.component_name + )); + output.push_str(&format!( + " members.push({}WidgetMembersRef::{}(widget))\n", + name, ordered_component_ref.component_name + )); output.push_str(&format!(" }}\n")); } @@ -437,12 +602,18 @@ fn component_model_generator() -> Result<(), Box> { if has_text { output.push_str(&format!(" for value in &self.text {{\n")); - output.push_str(&format!(" members.push({}WidgetMembersRef::Text {{ value }});\n", name)); + output.push_str(&format!( + " members.push({}WidgetMembersRef::Text {{ value }});\n", + name + )); output.push_str(&format!(" }}\n")); } output.push_str("\n"); - output.push_str(&format!(" Vec::<{}WidgetMembersRef>::serialize(&members, serializer)\n", name)); + output.push_str(&format!( + " Vec::<{}WidgetMembersRef>::serialize(&members, serializer)\n", + name + )); output.push_str(&format!(" }}\n")); output.push_str(&format!("}}\n")); @@ -458,7 +629,11 @@ fn component_model_generator() -> Result<(), Box> { for prop in props { if matches!(prop.property_type.kind(), PropertyKind::Property) { output.push_str(&format!(" #[serde(rename = \"{}\")]\n", prop.name)); - output.push_str(&format!(" pub {}: {},\n", prop.name.to_case(Case::Snake), generate_type(&prop, name))); + output.push_str(&format!( + " pub {}: {},\n", + prop.name.to_case(Case::Snake), + generate_type(&prop, name) + )); } } @@ -473,7 +648,11 @@ fn component_model_generator() -> Result<(), Box> { output.push_str(&format!("pub enum {}{} {{\n", name, prop_name.to_case(Case::Pascal))); for (index, property_type) in items.iter().enumerate() { - output.push_str(&format!(" _{}({}),\n", index, generate_required_type(&property_type, None))); + output.push_str(&format!( + " _{}({}),\n", + index, + generate_required_type(&property_type, None) + )); } output.push_str("}\n"); @@ -482,14 +661,10 @@ fn component_model_generator() -> Result<(), Box> { for prop in props { match &prop.property_type { - PropertyType::Union { items } => { - generate_union(&mut output, items, &prop.name) - } - PropertyType::Array { item} => { + PropertyType::Union { items } => generate_union(&mut output, items, &prop.name), + PropertyType::Array { item } => { match item.deref() { - PropertyType::Union { items } => { - generate_union(&mut output, items, &prop.name) - } + PropertyType::Union { items } => generate_union(&mut output, items, &prop.name), _ => {} } } @@ -497,7 +672,9 @@ fn component_model_generator() -> Result<(), Box> { } } } - Component::Root { children, shared_types, .. } => { + Component::Root { + children, shared_types, .. + } => { for (type_name, shared_type) in shared_types { match shared_type { SharedType::Enum { items } => { @@ -517,7 +694,14 @@ fn component_model_generator() -> Result<(), Box> { output.push_str(&format!("pub struct {} {{\n", type_name)); for (property_name, property_type) in items { - output.push_str(&format!(" pub {}: {},\n", &property_name, generate_required_type(&property_type, Some(format!("{}{}", type_name, property_name))))); + output.push_str(&format!( + " pub {}: {},\n", + &property_name, + generate_required_type( + &property_type, + Some(format!("{}{}", type_name, property_name)) + ) + )); } output.push_str("}\n"); @@ -550,8 +734,14 @@ fn component_model_generator() -> Result<(), Box> { output.push_str("pub enum RootWidgetMembers {\n"); for component_ref in children { - output.push_str(&format!(" #[serde(rename = \"gauntlet:{}\")]\n", component_ref.component_internal_name)); - output.push_str(&format!(" {}({}Widget),\n", component_ref.component_name, component_ref.component_name)); + output.push_str(&format!( + " #[serde(rename = \"gauntlet:{}\")]\n", + component_ref.component_internal_name + )); + output.push_str(&format!( + " {}({}Widget),\n", + component_ref.component_name, component_ref.component_name + )); } output.push_str("}\n"); @@ -578,8 +768,18 @@ fn generate_file>(path: P, text: &str) -> std::io::Result<()> { fn generate_type(property: &Property, name: &ComponentName) -> String { match property.optional { - true => generate_optional_type(&property.property_type, format!("{}{}", name, &property.name.to_case(Case::Pascal))), - false => generate_required_type(&property.property_type, Some(format!("{}{}", name, &property.name.to_case(Case::Pascal)))) + true => { + generate_optional_type( + &property.property_type, + format!("{}{}", name, &property.name.to_case(Case::Pascal)), + ) + } + false => { + generate_required_type( + &property.property_type, + Some(format!("{}{}", name, &property.name.to_case(Case::Pascal))), + ) + } } } @@ -598,9 +798,9 @@ fn generate_required_type(property_type: &PropertyType, union_name: Option { match union_name { None => panic!("should not be used"), - Some(union_name) => union_name + Some(union_name) => union_name, } - }, - PropertyType::Array { item } => format!("Vec<{}>", generate_required_type(item, union_name)) + } + PropertyType::Array { item } => format!("Vec<{}>", generate_required_type(item, union_name)), } } diff --git a/rust/common/src/dirs.rs b/rust/common/src/dirs.rs index b49a152..c549f76 100644 --- a/rust/common/src/dirs.rs +++ b/rust/common/src/dirs.rs @@ -1,17 +1,19 @@ -use std::path::{Path, PathBuf}; -use anyhow::Context; +use std::path::Path; +use std::path::PathBuf; -use directories::{BaseDirs, ProjectDirs}; +use anyhow::Context; +use directories::BaseDirs; +use directories::ProjectDirs; #[derive(Clone)] pub struct Dirs { - inner: ProjectDirs + inner: ProjectDirs, } impl Dirs { pub fn new() -> Self { Self { - inner: ProjectDirs::from("dev", "project-gauntlet", "Gauntlet").unwrap() + inner: ProjectDirs::from("dev", "project-gauntlet", "Gauntlet").unwrap(), } } @@ -126,7 +128,8 @@ impl Dirs { pub fn plugin_uds_socket(&self, plugin_uuid: &str) -> PathBuf { let state_dir = if cfg!(feature = "release") || cfg!(feature = "scenario_runner") { - self.inner.runtime_dir() + self.inner + .runtime_dir() .unwrap_or_else(|| Path::new("/tmp")) .to_path_buf() } else { @@ -139,4 +142,4 @@ impl Dirs { pub fn window_position(&self) -> PathBuf { self.state_dir().join("window_position") } -} \ No newline at end of file +} diff --git a/rust/common/src/lib.rs b/rust/common/src/lib.rs index a0379af..3c2f781 100644 --- a/rust/common/src/lib.rs +++ b/rust/common/src/lib.rs @@ -1,23 +1,19 @@ -use serde::{Deserialize, Serialize}; +use serde::Deserialize; +use serde::Serialize; +pub mod dirs; pub mod model; pub mod rpc; pub mod scenario_convert; pub mod scenario_model; -pub mod dirs; pub const SETTINGS_ENV: &'static str = "__GAUNTLET_INTERNAL_SETTINGS__"; #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type")] pub enum SettingsEnvData { - OpenPluginPreferences { - plugin_id: String, - }, - OpenEntrypointPreferences { - plugin_id: String, - entrypoint_id: String, - } + OpenPluginPreferences { plugin_id: String }, + OpenEntrypointPreferences { plugin_id: String, entrypoint_id: String }, } pub fn settings_env_data_to_string(data: SettingsEnvData) -> String { diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 8235abc..8d6208b 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -1,14 +1,20 @@ use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -use std::path::{Path, PathBuf}; +use std::fmt::Display; +use std::fmt::Formatter; +use std::path::Path; +use std::path::PathBuf; use std::sync::Arc; use anyhow::anyhow; -use bincode::{Decode, Encode}; +use bincode::Decode; +use bincode::Encode; use gix_url::Scheme; use gix_url::Url; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::Error; +use serde::Deserialize; +use serde::Deserializer; +use serde::Serialize; +use serde::Serializer; #[derive(Debug, Clone, PartialEq, Eq, Hash, Encode, Decode)] pub struct PluginId(Arc); @@ -29,15 +35,9 @@ impl PluginId { let url = self.try_to_url()?; match url.scheme { - Scheme::Git | Scheme::Ssh | Scheme::Http | Scheme::Https => { - Ok(url.to_bstring().to_string()) - } - Scheme::File => { - Err(anyhow!("'file' schema is not supported")) - } - Scheme::Ext(schema) => { - Err(anyhow!("'{}' schema is not supported", schema)) - } + Scheme::Git | Scheme::Ssh | Scheme::Http | Scheme::Https => Ok(url.to_bstring().to_string()), + Scheme::File => Err(anyhow!("'file' schema is not supported")), + Scheme::Ext(schema) => Err(anyhow!("'{}' schema is not supported", schema)), } } @@ -45,7 +45,7 @@ impl PluginId { let url = self.try_to_url()?; if url.scheme != Scheme::File { - return Err(anyhow!("plugin id is expected to point to local file")) + return Err(anyhow!("plugin id is expected to point to local file")); } let plugin_dir: String = url.path.try_into()?; @@ -79,15 +79,13 @@ impl Display for EntrypointId { pub enum DownloadStatus { InProgress, Done, - Failed { - message: String - }, + Failed { message: String }, } #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] pub enum UiRenderLocation { InlineView, - View + View, } #[derive(Debug, Clone)] @@ -123,11 +121,11 @@ pub enum SearchResultAccessory { TextAccessory { text: String, icon: Option, - tooltip: Option + tooltip: Option, }, IconAccessory { icon: Icons, - tooltip: Option + tooltip: Option, }, } @@ -154,13 +152,13 @@ pub enum SearchResultEntrypointType { #[derive(Debug, Clone)] pub enum UiThemeMode { Light, - Dark + Dark, } #[derive(Debug, Clone, Eq, PartialEq)] pub enum WindowPositionMode { Static, - ActiveMonitor + ActiveMonitor, } impl Display for WindowPositionMode { @@ -174,7 +172,6 @@ impl Display for WindowPositionMode { } } - #[derive(Debug, Clone)] pub struct UiThemeColor { pub r: f32, @@ -241,7 +238,7 @@ pub enum UiRequestData { ShowWindow, HideWindow, ClearInlineView { - plugin_id: PluginId + plugin_id: PluginId, }, ReplaceView { plugin_id: PluginId, @@ -266,21 +263,21 @@ pub enum UiRequestData { }, RequestSearchResultUpdate, ShowHud { - display: String + display: String, }, UpdateLoadingBar { plugin_id: PluginId, entrypoint_id: EntrypointId, - show: bool + show: bool, }, SetGlobalShortcut { - shortcut: Option + shortcut: Option, }, SetTheme { - theme: UiTheme + theme: UiTheme, }, SetWindowPositionMode { - mode: WindowPositionMode + mode: WindowPositionMode, }, } @@ -288,16 +285,16 @@ pub enum UiRequestData { pub enum BackendResponseData { Nothing, SetupData { - data: UiSetupData + data: UiSetupData, }, Search { - results: Vec + results: Vec, }, RequestViewRender { - shortcuts: HashMap + shortcuts: HashMap, }, InlineViewShortcuts { - shortcuts: HashMap> + shortcuts: HashMap>, }, } @@ -306,29 +303,29 @@ pub enum BackendRequestData { Setup, Search { text: String, - render_inline_view: bool + render_inline_view: bool, }, RequestViewRender { plugin_id: PluginId, - entrypoint_id: EntrypointId + entrypoint_id: EntrypointId, }, RequestViewClose { plugin_id: PluginId, }, RequestRunCommand { plugin_id: PluginId, - entrypoint_id: EntrypointId + entrypoint_id: EntrypointId, }, RequestRunGeneratedEntrypoint { plugin_id: PluginId, entrypoint_id: EntrypointId, - action_index: usize + action_index: usize, }, SendViewEvent { plugin_id: PluginId, widget_id: UiWidgetId, event_name: String, - event_arguments: Vec + event_arguments: Vec, }, SendKeyboardEvent { plugin_id: PluginId, @@ -338,20 +335,20 @@ pub enum BackendRequestData { modifier_shift: bool, modifier_control: bool, modifier_alt: bool, - modifier_meta: bool + modifier_meta: bool, }, SendOpenEvent { plugin_id: PluginId, - href: String + href: String, }, OpenSettingsWindow, OpenSettingsWindowPreferences { plugin_id: PluginId, - entrypoint_id: Option + entrypoint_id: Option, }, InlineViewShortcuts, SetupResponse { - global_shortcut_error: Option + global_shortcut_error: Option, }, } @@ -368,7 +365,7 @@ where { let value = match value { None => vec![], - Some(value) => vec![value] + Some(value) => vec![value], }; let res = Vec::<&V>::serialize(&value, serializer)?; @@ -376,7 +373,11 @@ where Ok(res) } -fn array_to_option<'de, D, V>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, V: Deserialize<'de> { +fn array_to_option<'de, D, V>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + V: Deserialize<'de>, +{ let res = Option::>::deserialize(deserializer)?; match res { @@ -385,7 +386,7 @@ fn array_to_option<'de, D, V>(deserializer: D) -> Result, D::Error> wh match res.len() { 0 => Ok(None), 1 => Ok(Some(res.remove(0))), - _ => Err(Error::custom("only zero or one allowed")) + _ => Err(Error::custom("only zero or one allowed")), } } } @@ -393,16 +394,14 @@ fn array_to_option<'de, D, V>(deserializer: D) -> Result, D::Error> wh include!(concat!(env!("OUT_DIR"), "/components.rs")); - // TODO generate this #[allow(async_fn_in_trait)] pub trait WidgetVisitor { - async fn action_widget(&mut self, _widget: &ActionWidget) { - } + async fn action_widget(&mut self, _widget: &ActionWidget) {} async fn action_panel_section_widget(&mut self, widget: &ActionPanelSectionWidget) { for members in &widget.content.ordered_members { match members { - ActionPanelSectionWidgetOrderedMembers::Action(widget) => self.action_widget(widget).await + ActionPanelSectionWidgetOrderedMembers::Action(widget) => self.action_widget(widget).await, } } } @@ -410,7 +409,9 @@ pub trait WidgetVisitor { for members in &widget.content.ordered_members { match members { ActionPanelWidgetOrderedMembers::Action(widget) => self.action_widget(widget).await, - ActionPanelWidgetOrderedMembers::ActionPanelSection(widget) => self.action_panel_section_widget(widget).await + ActionPanelWidgetOrderedMembers::ActionPanelSection(widget) => { + self.action_panel_section_widget(widget).await + } } } } @@ -420,7 +421,9 @@ pub trait WidgetVisitor { async fn metadata_tag_list_widget(&mut self, widget: &MetadataTagListWidget) { for members in &widget.content.ordered_members { match members { - MetadataTagListWidgetOrderedMembers::MetadataTagItem(widget) => self.metadata_tag_item_widget(widget).await + MetadataTagListWidgetOrderedMembers::MetadataTagItem(widget) => { + self.metadata_tag_item_widget(widget).await + } } } } @@ -439,9 +442,7 @@ pub trait WidgetVisitor { } } - async fn image(&mut self, _widget_id: UiWidgetId, _widget: &ImageLike) { - - } + async fn image(&mut self, _widget_id: UiWidgetId, _widget: &ImageLike) {} async fn image_widget(&mut self, widget: &ImageWidget) { self.image(widget.__id__, &widget.source).await @@ -492,7 +493,7 @@ pub trait WidgetVisitor { async fn select_widget(&mut self, widget: &SelectWidget) { for members in &widget.content.ordered_members { match members { - SelectWidgetOrderedMembers::SelectItem(widget) => self.select_item_widget(widget).await + SelectWidgetOrderedMembers::SelectItem(widget) => self.select_item_widget(widget).await, } } } @@ -522,7 +523,7 @@ pub trait WidgetVisitor { for members in &widget.content.ordered_members { match members { InlineWidgetOrderedMembers::Content(widget) => self.content_widget(widget).await, - InlineWidgetOrderedMembers::InlineSeparator(widget) => self.inline_separator_widget(widget).await + InlineWidgetOrderedMembers::InlineSeparator(widget) => self.inline_separator_widget(widget).await, } } } @@ -552,14 +553,14 @@ pub trait WidgetVisitor { for accessories in &widget.content.accessories { match accessories { ListItemAccessories::_0(widget) => self.text_accessory_widget(widget).await, - ListItemAccessories::_1(widget) => self.icon_accessory_widget(widget).await + ListItemAccessories::_1(widget) => self.icon_accessory_widget(widget).await, } } } async fn list_section_widget(&mut self, widget: &ListSectionWidget) { for members in &widget.content.ordered_members { match members { - ListSectionWidgetOrderedMembers::ListItem(widget) => self.list_item_widget(widget).await + ListSectionWidgetOrderedMembers::ListItem(widget) => self.list_item_widget(widget).await, } } } @@ -606,7 +607,7 @@ pub trait WidgetVisitor { async fn grid_section_widget(&mut self, widget: &GridSectionWidget) { for members in &widget.content.ordered_members { match members { - GridSectionWidgetOrderedMembers::GridItem(widget) => self.grid_item_widget(widget).await + GridSectionWidgetOrderedMembers::GridItem(widget) => self.grid_item_widget(widget).await, } } } @@ -623,7 +624,7 @@ pub trait WidgetVisitor { for members in &widget.content.ordered_members { match members { GridWidgetOrderedMembers::GridItem(widget) => self.grid_item_widget(widget).await, - GridWidgetOrderedMembers::GridSection(widget) => self.grid_section_widget(widget).await + GridWidgetOrderedMembers::GridSection(widget) => self.grid_section_widget(widget).await, } } } @@ -692,7 +693,7 @@ pub enum SettingsTheme { // Custom, TODO specify file path or drag and drop via settings ui MacOSLight, MacOSDark, - Legacy + Legacy, } impl Display for SettingsTheme { @@ -712,27 +713,13 @@ impl Display for SettingsTheme { #[derive(Debug, Clone)] pub enum PluginPreferenceUserData { - Number { - value: Option, - }, - String { - value: Option, - }, - Enum { - value: Option, - }, - Bool { - value: Option, - }, - ListOfStrings { - value: Option>, - }, - ListOfNumbers { - value: Option>, - }, - ListOfEnums { - value: Option>, - }, + Number { value: Option }, + String { value: Option }, + Enum { value: Option }, + Bool { value: Option }, + ListOfStrings { value: Option> }, + ListOfNumbers { value: Option> }, + ListOfEnums { value: Option> }, // TODO be careful about exposing secrets to logs when adding password type } @@ -783,7 +770,6 @@ pub struct PreferenceEnumValue { pub value: String, } - // copy of iced (currently fork) PhysicalKey but without modifiers #[derive(Debug, Clone)] pub enum PhysicalKey { @@ -1349,7 +1335,7 @@ impl PhysicalKey { PhysicalKey::F33 => "F33", PhysicalKey::F34 => "F34", PhysicalKey::F35 => "F35", - }.to_string() + } + .to_string() } } - diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index 7876aeb..e16cd68 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -1,44 +1,85 @@ use std::collections::HashMap; + +use gauntlet_utils::channel::RequestError; +use gauntlet_utils::channel::RequestSender; use thiserror::Error; -use tonic::{Code, Request}; use tonic::transport::Channel; +use tonic::Code; +use tonic::Request; -use gauntlet_utils::channel::{RequestError, RequestSender}; - -use crate::model::{BackendRequestData, BackendResponseData, DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, SettingsTheme, UiPropertyValue, UiSetupData, UiWidgetId, WindowPositionMode}; -use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadStatus, RpcDownloadStatusRequest, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetThemeRequest, RpcGetWindowPositionModeRequest, RpcPingRequest, RpcPluginsRequest, RpcRemovePluginRequest, RpcSaveLocalPluginRequest, RpcSetEntrypointStateRequest, RpcSetGlobalShortcutRequest, RpcSetPluginStateRequest, RpcSetPreferenceValueRequest, RpcSetThemeRequest, RpcSetWindowPositionModeRequest, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowWindowRequest}; +use crate::model::BackendRequestData; +use crate::model::BackendResponseData; +use crate::model::DownloadStatus; +use crate::model::EntrypointId; +use crate::model::KeyboardEventOrigin; +use crate::model::LocalSaveData; +use crate::model::PhysicalKey; +use crate::model::PhysicalShortcut; +use crate::model::PluginId; +use crate::model::PluginPreferenceUserData; +use crate::model::SearchResult; +use crate::model::SettingsEntrypoint; +use crate::model::SettingsEntrypointType; +use crate::model::SettingsPlugin; +use crate::model::SettingsTheme; +use crate::model::UiPropertyValue; +use crate::model::UiSetupData; +use crate::model::UiWidgetId; +use crate::model::WindowPositionMode; use crate::rpc::grpc::rpc_backend_client::RpcBackendClient; -use crate::rpc::grpc_convert::{plugin_preference_from_rpc, plugin_preference_user_data_from_rpc, plugin_preference_user_data_to_rpc}; +use crate::rpc::grpc::RpcDownloadPluginRequest; +use crate::rpc::grpc::RpcDownloadStatus; +use crate::rpc::grpc::RpcDownloadStatusRequest; +use crate::rpc::grpc::RpcEntrypointTypeSettings; +use crate::rpc::grpc::RpcGetGlobalShortcutRequest; +use crate::rpc::grpc::RpcGetThemeRequest; +use crate::rpc::grpc::RpcGetWindowPositionModeRequest; +use crate::rpc::grpc::RpcPingRequest; +use crate::rpc::grpc::RpcPluginsRequest; +use crate::rpc::grpc::RpcRemovePluginRequest; +use crate::rpc::grpc::RpcSaveLocalPluginRequest; +use crate::rpc::grpc::RpcSetEntrypointStateRequest; +use crate::rpc::grpc::RpcSetGlobalShortcutRequest; +use crate::rpc::grpc::RpcSetPluginStateRequest; +use crate::rpc::grpc::RpcSetPreferenceValueRequest; +use crate::rpc::grpc::RpcSetThemeRequest; +use crate::rpc::grpc::RpcSetWindowPositionModeRequest; +use crate::rpc::grpc::RpcShortcut; +use crate::rpc::grpc::RpcShowSettingsWindowRequest; +use crate::rpc::grpc::RpcShowWindowRequest; +use crate::rpc::grpc_convert::plugin_preference_from_rpc; +use crate::rpc::grpc_convert::plugin_preference_user_data_from_rpc; +use crate::rpc::grpc_convert::plugin_preference_user_data_to_rpc; #[derive(Error, Debug, Clone)] pub enum BackendForFrontendApiError { #[error("Frontend wasn't able to process request in a timely manner")] TimeoutError, #[error("Internal Error: {display:?}")] - Internal { - display: String - }, + Internal { display: String }, } impl From for BackendForFrontendApiError { fn from(error: RequestError) -> BackendForFrontendApiError { match error { RequestError::TimeoutError => BackendForFrontendApiError::TimeoutError, - RequestError::OtherSideWasDropped => BackendForFrontendApiError::Internal { display: "other side was dropped".to_string() } + RequestError::OtherSideWasDropped => { + BackendForFrontendApiError::Internal { + display: "other side was dropped".to_string(), + } + } } } } #[derive(Debug, Clone)] pub struct BackendForFrontendApi { - backend_sender: RequestSender + backend_sender: RequestSender, } impl BackendForFrontendApi { pub fn new(backend_sender: RequestSender) -> Self { - Self { - backend_sender - } + Self { backend_sender } } pub async fn setup_data(&mut self) -> Result { @@ -51,10 +92,11 @@ impl BackendForFrontendApi { Ok(data) } - pub async fn setup_response(&mut self, global_shortcut_error: Option) -> Result<(), BackendForFrontendApiError> { - let request = BackendRequestData::SetupResponse { - global_shortcut_error - }; + pub async fn setup_response( + &mut self, + global_shortcut_error: Option, + ) -> Result<(), BackendForFrontendApiError> { + let request = BackendRequestData::SetupResponse { global_shortcut_error }; let BackendResponseData::Nothing = self.backend_sender.send_receive(request).await? else { unreachable!() @@ -63,7 +105,11 @@ impl BackendForFrontendApi { Ok(()) } - pub async fn search(&mut self, text: String, render_inline_view: bool) -> Result, BackendForFrontendApiError> { + pub async fn search( + &mut self, + text: String, + render_inline_view: bool, + ) -> Result, BackendForFrontendApiError> { let request = BackendRequestData::Search { text, render_inline_view, @@ -76,13 +122,18 @@ impl BackendForFrontendApi { Ok(results) } - pub async fn request_view_render(&mut self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Result, BackendForFrontendApiError> { + pub async fn request_view_render( + &mut self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + ) -> Result, BackendForFrontendApiError> { let request = BackendRequestData::RequestViewRender { plugin_id, entrypoint_id, }; - let BackendResponseData::RequestViewRender { shortcuts } = self.backend_sender.send_receive(request).await? else { + let BackendResponseData::RequestViewRender { shortcuts } = self.backend_sender.send_receive(request).await? + else { unreachable!() }; @@ -90,9 +141,7 @@ impl BackendForFrontendApi { } pub async fn request_view_close(&mut self, plugin_id: PluginId) -> Result<(), BackendForFrontendApiError> { - let request = BackendRequestData::RequestViewClose { - plugin_id, - }; + let request = BackendRequestData::RequestViewClose { plugin_id }; let BackendResponseData::Nothing = self.backend_sender.send_receive(request).await? else { unreachable!() @@ -101,7 +150,11 @@ impl BackendForFrontendApi { Ok(()) } - pub async fn request_run_command(&mut self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> Result<(), BackendForFrontendApiError> { + pub async fn request_run_command( + &mut self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + ) -> Result<(), BackendForFrontendApiError> { let request = BackendRequestData::RequestRunCommand { plugin_id, entrypoint_id, @@ -114,7 +167,12 @@ impl BackendForFrontendApi { Ok(()) } - pub async fn request_run_generated_entrypoint(&mut self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize) -> Result<(), BackendForFrontendApiError> { + pub async fn request_run_generated_entrypoint( + &mut self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + action_index: usize, + ) -> Result<(), BackendForFrontendApiError> { let request = BackendRequestData::RequestRunGeneratedEntrypoint { plugin_id, entrypoint_id, @@ -133,7 +191,7 @@ impl BackendForFrontendApi { plugin_id: PluginId, widget_id: UiWidgetId, event_name: String, - event_arguments: Vec + event_arguments: Vec, ) -> Result<(), BackendForFrontendApiError> { let request = BackendRequestData::SendViewEvent { plugin_id, @@ -158,7 +216,7 @@ impl BackendForFrontendApi { modifier_shift: bool, modifier_control: bool, modifier_alt: bool, - modifier_meta: bool + modifier_meta: bool, ) -> Result<(), BackendForFrontendApiError> { let request = BackendRequestData::SendKeyboardEvent { plugin_id, @@ -178,11 +236,12 @@ impl BackendForFrontendApi { Ok(()) } - pub async fn send_open_event(&mut self, plugin_id: PluginId, href: String) -> Result<(), BackendForFrontendApiError> { - let request = BackendRequestData::SendOpenEvent { - plugin_id, - href, - }; + pub async fn send_open_event( + &mut self, + plugin_id: PluginId, + href: String, + ) -> Result<(), BackendForFrontendApiError> { + let request = BackendRequestData::SendOpenEvent { plugin_id, href }; let BackendResponseData::Nothing = self.backend_sender.send_receive(request).await? else { unreachable!() @@ -191,7 +250,7 @@ impl BackendForFrontendApi { Ok(()) } - pub async fn open_settings_window(&mut self, ) -> Result<(), BackendForFrontendApiError> { + pub async fn open_settings_window(&mut self) -> Result<(), BackendForFrontendApiError> { let request = BackendRequestData::OpenSettingsWindow; let BackendResponseData::Nothing = self.backend_sender.send_receive(request).await? else { @@ -201,7 +260,11 @@ impl BackendForFrontendApi { Ok(()) } - pub async fn open_settings_window_preferences(&mut self, plugin_id: PluginId, entrypoint_id: Option) -> Result<(), BackendForFrontendApiError> { + pub async fn open_settings_window_preferences( + &mut self, + plugin_id: PluginId, + entrypoint_id: Option, + ) -> Result<(), BackendForFrontendApiError> { let request = BackendRequestData::OpenSettingsWindowPreferences { plugin_id, entrypoint_id, @@ -214,10 +277,13 @@ impl BackendForFrontendApi { Ok(()) } - pub async fn inline_view_shortcuts(&self) -> Result>, BackendForFrontendApiError> { + pub async fn inline_view_shortcuts( + &self, + ) -> Result>, BackendForFrontendApiError> { let request = BackendRequestData::InlineViewShortcuts; - let BackendResponseData::InlineViewShortcuts { shortcuts } = self.backend_sender.send_receive(request).await? else { + let BackendResponseData::InlineViewShortcuts { shortcuts } = self.backend_sender.send_receive(request).await? + else { unreachable!() }; @@ -230,9 +296,7 @@ pub enum BackendApiError { #[error("Timeout Error")] Timeout, #[error("Internal Backend Error: {display:?}")] - Internal { - display: String - }, + Internal { display: String }, } impl From for BackendApiError { @@ -240,74 +304,83 @@ impl From for BackendApiError { match error.code() { Code::Ok => unreachable!(), Code::DeadlineExceeded => BackendApiError::Timeout, - _ => BackendApiError::Internal { - display: format!("{}", error) + _ => { + BackendApiError::Internal { + display: format!("{}", error), + } } } - } } impl From for BackendApiError { fn from(error: prost::UnknownEnumValue) -> BackendApiError { BackendApiError::Internal { - display: format!("{}", error) + display: format!("{}", error), } } } #[derive(Debug, Clone)] pub struct BackendApi { - client: RpcBackendClient + client: RpcBackendClient, } impl BackendApi { pub async fn new() -> anyhow::Result { Ok(Self { - client: RpcBackendClient::connect("http://127.0.0.1:42320").await? + client: RpcBackendClient::connect("http://127.0.0.1:42320").await?, }) } pub async fn ping(&mut self) -> Result<(), BackendApiError> { - let _ = self.client.ping(Request::new(RpcPingRequest::default())) - .await?; + let _ = self.client.ping(Request::new(RpcPingRequest::default())).await?; Ok(()) } pub async fn show_window(&mut self) -> Result<(), BackendApiError> { - let _ = self.client.show_window(Request::new(RpcShowWindowRequest::default())) + let _ = self + .client + .show_window(Request::new(RpcShowWindowRequest::default())) .await?; Ok(()) } pub async fn show_settings_window(&mut self) -> Result<(), BackendApiError> { - let _ = self.client.show_settings_window(Request::new(RpcShowSettingsWindowRequest::default())) + let _ = self + .client + .show_settings_window(Request::new(RpcShowSettingsWindowRequest::default())) .await?; Ok(()) } pub async fn plugins(&mut self) -> Result, BackendApiError> { - let plugins = self.client.plugins(Request::new(RpcPluginsRequest::default())) + let plugins = self + .client + .plugins(Request::new(RpcPluginsRequest::default())) .await? .into_inner() .plugins .into_iter() .map(|plugin| { - let entrypoints: HashMap<_, _> = plugin.entrypoints + let entrypoints: HashMap<_, _> = plugin + .entrypoints .into_iter() .map(|entrypoint| { let id = EntrypointId::from_string(entrypoint.entrypoint_id); - let entrypoint_type: RpcEntrypointTypeSettings = entrypoint.entrypoint_type.try_into() - .expect("download status failed"); // TODO proper error handling + let entrypoint_type: RpcEntrypointTypeSettings = + entrypoint.entrypoint_type.try_into().expect("download status failed"); // TODO proper error handling let entrypoint_type = match entrypoint_type { RpcEntrypointTypeSettings::SCommand => SettingsEntrypointType::Command, RpcEntrypointTypeSettings::SView => SettingsEntrypointType::View, RpcEntrypointTypeSettings::SInlineView => SettingsEntrypointType::InlineView, - RpcEntrypointTypeSettings::SEntrypointGenerator => SettingsEntrypointType::EntrypointGenerator + RpcEntrypointTypeSettings::SEntrypointGenerator => { + SettingsEntrypointType::EntrypointGenerator + } }; let entrypoint = SettingsEntrypoint { @@ -316,10 +389,14 @@ impl BackendApi { entrypoint_name: entrypoint.entrypoint_name.clone(), entrypoint_description: entrypoint.entrypoint_description, entrypoint_type, - preferences: entrypoint.preferences.into_iter() + preferences: entrypoint + .preferences + .into_iter() .map(|(key, value)| (key, plugin_preference_from_rpc(value))) .collect(), - preferences_user_data: entrypoint.preferences_user_data.into_iter() + preferences_user_data: entrypoint + .preferences_user_data + .into_iter() .map(|(key, value)| (key, plugin_preference_user_data_from_rpc(value))) .collect(), }; @@ -334,10 +411,14 @@ impl BackendApi { plugin_description: plugin.plugin_description, enabled: plugin.enabled, entrypoints, - preferences: plugin.preferences.into_iter() + preferences: plugin + .preferences + .into_iter() .map(|(key, value)| (key, plugin_preference_from_rpc(value))) .collect(), - preferences_user_data: plugin.preferences_user_data.into_iter() + preferences_user_data: plugin + .preferences_user_data + .into_iter() .map(|(key, value)| (key, plugin_preference_user_data_from_rpc(value))) .collect(), }; @@ -355,26 +436,32 @@ impl BackendApi { enabled, }; - self.client.set_plugin_state(Request::new(request)) - .await?; + self.client.set_plugin_state(Request::new(request)).await?; Ok(()) } - pub async fn set_entrypoint_state(&mut self, plugin_id: PluginId, entrypoint_id: EntrypointId, enabled: bool) -> Result<(), BackendApiError> { + pub async fn set_entrypoint_state( + &mut self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + enabled: bool, + ) -> Result<(), BackendApiError> { let request = RpcSetEntrypointStateRequest { plugin_id: plugin_id.to_string(), entrypoint_id: entrypoint_id.to_string(), enabled, }; - self.client.set_entrypoint_state(Request::new(request)) - .await?; + self.client.set_entrypoint_state(Request::new(request)).await?; Ok(()) } - pub async fn set_global_shortcut(&mut self, shortcut: Option) -> Result, BackendApiError> { + pub async fn set_global_shortcut( + &mut self, + shortcut: Option, + ) -> Result, BackendApiError> { let request = RpcSetGlobalShortcutRequest { shortcut: shortcut.map(|shortcut| { RpcShortcut { @@ -384,10 +471,12 @@ impl BackendApi { modifier_alt: shortcut.modifier_alt, modifier_meta: shortcut.modifier_meta, } - }) + }), }; - let error = self.client.set_global_shortcut(Request::new(request)) + let error = self + .client + .set_global_shortcut(Request::new(request)) .await? .into_inner() .error; @@ -396,23 +485,24 @@ impl BackendApi { } pub async fn get_global_shortcut(&mut self) -> Result<(Option, Option), BackendApiError> { - let response = self.client.get_global_shortcut(Request::new(RpcGetGlobalShortcutRequest::default())) + let response = self + .client + .get_global_shortcut(Request::new(RpcGetGlobalShortcutRequest::default())) .await?; let response = response.into_inner(); Ok(( - response.shortcut - .map(|shortcut| { - PhysicalShortcut { - physical_key: PhysicalKey::from_value(shortcut.physical_key), - modifier_shift: shortcut.modifier_shift, - modifier_control: shortcut.modifier_control, - modifier_alt: shortcut.modifier_alt, - modifier_meta: shortcut.modifier_meta, - } - }), - response.error + response.shortcut.map(|shortcut| { + PhysicalShortcut { + physical_key: PhysicalKey::from_value(shortcut.physical_key), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + } + }), + response.error, )) } @@ -427,17 +517,18 @@ impl BackendApi { }; let request = RpcSetThemeRequest { - theme: theme.to_string() + theme: theme.to_string(), }; - self.client.set_theme(Request::new(request)) - .await?; + self.client.set_theme(Request::new(request)).await?; Ok(()) } pub async fn get_theme(&mut self) -> Result { - let response = self.client.get_theme(Request::new(RpcGetThemeRequest::default())) + let response = self + .client + .get_theme(Request::new(RpcGetThemeRequest::default())) .await?; let theme = response.into_inner().theme; @@ -449,7 +540,7 @@ impl BackendApi { "MacOSLight" => SettingsTheme::MacOSLight, "MacOSDark" => SettingsTheme::MacOSDark, "Legacy" => SettingsTheme::Legacy, - _ => unreachable!() + _ => unreachable!(), }; Ok(theme) @@ -461,18 +552,17 @@ impl BackendApi { WindowPositionMode::ActiveMonitor => "ActiveMonitor", }; - let request = RpcSetWindowPositionModeRequest { - mode: mode.to_string() - }; + let request = RpcSetWindowPositionModeRequest { mode: mode.to_string() }; - self.client.set_window_position_mode(Request::new(request)) - .await?; + self.client.set_window_position_mode(Request::new(request)).await?; Ok(()) } pub async fn get_window_position_mode(&mut self) -> Result { - let response = self.client.get_window_position_mode(Request::new(RpcGetWindowPositionModeRequest::default())) + let response = self + .client + .get_window_position_mode(Request::new(RpcGetWindowPositionModeRequest::default())) .await?; let mode = response.into_inner().mode; @@ -480,13 +570,19 @@ impl BackendApi { let mode = match mode.as_str() { "Static" => WindowPositionMode::Static, "ActiveMonitor" => WindowPositionMode::ActiveMonitor, - _ => unreachable!() + _ => unreachable!(), }; Ok(mode) } - pub async fn set_preference_value(&mut self, plugin_id: PluginId, entrypoint_id: Option, id: String, user_data: PluginPreferenceUserData) -> Result<(), BackendApiError> { + pub async fn set_preference_value( + &mut self, + plugin_id: PluginId, + entrypoint_id: Option, + id: String, + user_data: PluginPreferenceUserData, + ) -> Result<(), BackendApiError> { let request = RpcSetPreferenceValueRequest { plugin_id: plugin_id.to_string(), entrypoint_id: entrypoint_id.map(|id| id.to_string()).unwrap_or_default(), @@ -494,25 +590,25 @@ impl BackendApi { preference_value: Some(plugin_preference_user_data_to_rpc(user_data)), }; - self.client.set_preference_value(Request::new(request)) - .await?; + self.client.set_preference_value(Request::new(request)).await?; Ok(()) } pub async fn download_plugin(&mut self, plugin_id: PluginId) -> Result<(), BackendApiError> { let request = RpcDownloadPluginRequest { - plugin_id: plugin_id.to_string() + plugin_id: plugin_id.to_string(), }; - self.client.download_plugin(Request::new(request)) - .await?; + self.client.download_plugin(Request::new(request)).await?; Ok(()) } pub async fn download_status(&mut self) -> Result, BackendApiError> { - let plugins = self.client.download_status(Request::new(RpcDownloadStatusRequest::default())) + let plugins = self + .client + .download_status(Request::new(RpcDownloadStatusRequest::default())) .await? .into_inner() .status_per_plugin @@ -523,7 +619,11 @@ impl BackendApi { let status = match status.status.try_into()? { RpcDownloadStatus::InProgress => DownloadStatus::InProgress, RpcDownloadStatus::Done => DownloadStatus::Done, - RpcDownloadStatus::Failed => DownloadStatus::Failed { message: status.message }, + RpcDownloadStatus::Failed => { + DownloadStatus::Failed { + message: status.message, + } + } }; Ok::<(PluginId, DownloadStatus), BackendApiError>((plugin_id, status)) @@ -534,10 +634,11 @@ impl BackendApi { } pub async fn remove_plugin(&mut self, plugin_id: PluginId) -> Result<(), BackendApiError> { - let request = RpcRemovePluginRequest { plugin_id: plugin_id.to_string() }; + let request = RpcRemovePluginRequest { + plugin_id: plugin_id.to_string(), + }; - self.client.remove_plugin(Request::new(request)) - .await?; + self.client.remove_plugin(Request::new(request)).await?; Ok(()) } @@ -545,9 +646,7 @@ impl BackendApi { pub async fn save_local_plugin(&mut self, path: String) -> Result { let request = RpcSaveLocalPluginRequest { path }; - let response = self.client.save_local_plugin(Request::new(request)) - .await? - .into_inner(); + let response = self.client.save_local_plugin(Request::new(request)).await?.into_inner(); Ok(LocalSaveData { stdout_file_path: response.stdout_file_path, diff --git a/rust/common/src/rpc/backend_server.rs b/rust/common/src/rpc/backend_server.rs index e4b5b68..a506759 100644 --- a/rust/common/src/rpc/backend_server.rs +++ b/rust/common/src/rpc/backend_server.rs @@ -3,13 +3,67 @@ use std::net::SocketAddr; use std::time::Duration; use tokio::net::TcpStream; -use tonic::{Request, Response, Status}; use tonic::transport::Server; +use tonic::Request; +use tonic::Response; +use tonic::Status; -use crate::model::{DownloadStatus, EntrypointId, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreferenceUserData, SettingsEntrypointType, SettingsPlugin, SettingsTheme, WindowPositionMode}; -use crate::rpc::grpc::{RpcDownloadPluginRequest, RpcDownloadPluginResponse, RpcDownloadStatus, RpcDownloadStatusRequest, RpcDownloadStatusResponse, RpcDownloadStatusValue, RpcEntrypoint, RpcEntrypointTypeSettings, RpcGetGlobalShortcutRequest, RpcGetGlobalShortcutResponse, RpcGetThemeRequest, RpcGetThemeResponse, RpcGetWindowPositionModeRequest, RpcGetWindowPositionModeResponse, RpcPingRequest, RpcPingResponse, RpcPlugin, RpcPluginsRequest, RpcPluginsResponse, RpcRemovePluginRequest, RpcRemovePluginResponse, RpcSaveLocalPluginRequest, RpcSaveLocalPluginResponse, RpcSetEntrypointStateRequest, RpcSetEntrypointStateResponse, RpcSetGlobalShortcutRequest, RpcSetGlobalShortcutResponse, RpcSetPluginStateRequest, RpcSetPluginStateResponse, RpcSetPreferenceValueRequest, RpcSetPreferenceValueResponse, RpcSetThemeRequest, RpcSetThemeResponse, RpcSetWindowPositionModeRequest, RpcSetWindowPositionModeResponse, RpcShortcut, RpcShowSettingsWindowRequest, RpcShowSettingsWindowResponse, RpcShowWindowRequest, RpcShowWindowResponse}; -use crate::rpc::grpc::rpc_backend_server::{RpcBackend, RpcBackendServer}; -use crate::rpc::grpc_convert::{plugin_preference_to_rpc, plugin_preference_user_data_from_rpc, plugin_preference_user_data_to_rpc}; +use crate::model::DownloadStatus; +use crate::model::EntrypointId; +use crate::model::LocalSaveData; +use crate::model::PhysicalKey; +use crate::model::PhysicalShortcut; +use crate::model::PluginId; +use crate::model::PluginPreferenceUserData; +use crate::model::SettingsEntrypointType; +use crate::model::SettingsPlugin; +use crate::model::SettingsTheme; +use crate::model::WindowPositionMode; +use crate::rpc::grpc::rpc_backend_server::RpcBackend; +use crate::rpc::grpc::rpc_backend_server::RpcBackendServer; +use crate::rpc::grpc::RpcDownloadPluginRequest; +use crate::rpc::grpc::RpcDownloadPluginResponse; +use crate::rpc::grpc::RpcDownloadStatus; +use crate::rpc::grpc::RpcDownloadStatusRequest; +use crate::rpc::grpc::RpcDownloadStatusResponse; +use crate::rpc::grpc::RpcDownloadStatusValue; +use crate::rpc::grpc::RpcEntrypoint; +use crate::rpc::grpc::RpcEntrypointTypeSettings; +use crate::rpc::grpc::RpcGetGlobalShortcutRequest; +use crate::rpc::grpc::RpcGetGlobalShortcutResponse; +use crate::rpc::grpc::RpcGetThemeRequest; +use crate::rpc::grpc::RpcGetThemeResponse; +use crate::rpc::grpc::RpcGetWindowPositionModeRequest; +use crate::rpc::grpc::RpcGetWindowPositionModeResponse; +use crate::rpc::grpc::RpcPingRequest; +use crate::rpc::grpc::RpcPingResponse; +use crate::rpc::grpc::RpcPlugin; +use crate::rpc::grpc::RpcPluginsRequest; +use crate::rpc::grpc::RpcPluginsResponse; +use crate::rpc::grpc::RpcRemovePluginRequest; +use crate::rpc::grpc::RpcRemovePluginResponse; +use crate::rpc::grpc::RpcSaveLocalPluginRequest; +use crate::rpc::grpc::RpcSaveLocalPluginResponse; +use crate::rpc::grpc::RpcSetEntrypointStateRequest; +use crate::rpc::grpc::RpcSetEntrypointStateResponse; +use crate::rpc::grpc::RpcSetGlobalShortcutRequest; +use crate::rpc::grpc::RpcSetGlobalShortcutResponse; +use crate::rpc::grpc::RpcSetPluginStateRequest; +use crate::rpc::grpc::RpcSetPluginStateResponse; +use crate::rpc::grpc::RpcSetPreferenceValueRequest; +use crate::rpc::grpc::RpcSetPreferenceValueResponse; +use crate::rpc::grpc::RpcSetThemeRequest; +use crate::rpc::grpc::RpcSetThemeResponse; +use crate::rpc::grpc::RpcSetWindowPositionModeRequest; +use crate::rpc::grpc::RpcSetWindowPositionModeResponse; +use crate::rpc::grpc::RpcShortcut; +use crate::rpc::grpc::RpcShowSettingsWindowRequest; +use crate::rpc::grpc::RpcShowSettingsWindowResponse; +use crate::rpc::grpc::RpcShowWindowRequest; +use crate::rpc::grpc::RpcShowWindowResponse; +use crate::rpc::grpc_convert::plugin_preference_to_rpc; +use crate::rpc::grpc_convert::plugin_preference_user_data_from_rpc; +use crate::rpc::grpc_convert::plugin_preference_user_data_to_rpc; pub async fn wait_for_backend_server() { loop { @@ -34,14 +88,12 @@ pub async fn start_backend_server(server: Box) } struct RpcBackendServerImpl { - server: Box + server: Box, } impl RpcBackendServerImpl { pub fn new(server: Box) -> Self { - Self { - server - } + Self { server } } } @@ -53,52 +105,33 @@ pub trait BackendServer { async fn plugins(&self) -> anyhow::Result>; - async fn set_plugin_state( - &self, - plugin_id: PluginId, - enabled: bool - ) -> anyhow::Result<()>; + async fn set_plugin_state(&self, plugin_id: PluginId, enabled: bool) -> anyhow::Result<()>; async fn set_entrypoint_state( &self, plugin_id: PluginId, entrypoint_id: EntrypointId, - enabled: bool + enabled: bool, ) -> anyhow::Result<()>; - async fn set_global_shortcut( - &self, - shortcut: Option - ) -> anyhow::Result<()>; + async fn set_global_shortcut(&self, shortcut: Option) -> anyhow::Result<()>; - async fn get_global_shortcut( - &self, - ) -> anyhow::Result<(Option, Option)>; + async fn get_global_shortcut(&self) -> anyhow::Result<(Option, Option)>; - async fn set_theme( - &self, - theme: SettingsTheme - ) -> anyhow::Result<()>; + async fn set_theme(&self, theme: SettingsTheme) -> anyhow::Result<()>; - async fn get_theme( - &self, - ) -> anyhow::Result; + async fn get_theme(&self) -> anyhow::Result; - async fn set_window_position_mode( - &self, - mode: WindowPositionMode - ) -> anyhow::Result<()>; + async fn set_window_position_mode(&self, mode: WindowPositionMode) -> anyhow::Result<()>; - async fn get_window_position_mode( - &self, - ) -> anyhow::Result; + async fn get_window_position_mode(&self) -> anyhow::Result; async fn set_preference_value( &self, plugin_id: PluginId, entrypoint_id: Option, preference_id: String, - preference_value: PluginPreferenceUserData + preference_value: PluginPreferenceUserData, ) -> anyhow::Result<()>; async fn download_plugin(&self, plugin_id: PluginId) -> anyhow::Result<()>; @@ -110,23 +143,30 @@ pub trait BackendServer { async fn save_local_plugin(&self, path: String) -> anyhow::Result; } - #[tonic::async_trait] impl RpcBackend for RpcBackendServerImpl { async fn ping(&self, _: Request) -> Result, Status> { Ok(Response::new(RpcPingResponse::default())) } - async fn show_window(&self, _request: Request) -> Result, Status> { - self.server.show_window() + async fn show_window( + &self, + _request: Request, + ) -> Result, Status> { + self.server + .show_window() .await .map_err(|err| Status::internal(format!("{:#}", err)))?; Ok(Response::new(RpcShowWindowResponse::default())) } - async fn show_settings_window(&self, _request: Request) -> Result, Status> { - self.server.show_settings_window() + async fn show_settings_window( + &self, + _request: Request, + ) -> Result, Status> { + self.server + .show_settings_window() .await .map_err(|err| Status::internal(format!("{:#}", err)))?; @@ -134,30 +174,42 @@ impl RpcBackend for RpcBackendServerImpl { } async fn plugins(&self, _: Request) -> Result, Status> { - let plugins = self.server.plugins() + let plugins = self + .server + .plugins() .await .map_err(|err| Status::internal(format!("{:#}", err)))? .into_iter() .map(|plugin| { - let entrypoints = plugin.entrypoints + let entrypoints = plugin + .entrypoints .into_iter() - .map(|(_, entrypoint)| RpcEntrypoint { - enabled: entrypoint.enabled, - entrypoint_id: entrypoint.entrypoint_id.to_string(), - entrypoint_name: entrypoint.entrypoint_name, - entrypoint_description: entrypoint.entrypoint_description, - entrypoint_type: match entrypoint.entrypoint_type { - SettingsEntrypointType::Command => RpcEntrypointTypeSettings::SCommand, - SettingsEntrypointType::View => RpcEntrypointTypeSettings::SView, - SettingsEntrypointType::InlineView => RpcEntrypointTypeSettings::SInlineView, - SettingsEntrypointType::EntrypointGenerator => RpcEntrypointTypeSettings::SEntrypointGenerator, - }.into(), - preferences: entrypoint.preferences.into_iter() - .map(|(key, value)| (key, plugin_preference_to_rpc(value))) - .collect(), - preferences_user_data: entrypoint.preferences_user_data.into_iter() - .map(|(key, value)| (key, plugin_preference_user_data_to_rpc(value))) - .collect(), + .map(|(_, entrypoint)| { + RpcEntrypoint { + enabled: entrypoint.enabled, + entrypoint_id: entrypoint.entrypoint_id.to_string(), + entrypoint_name: entrypoint.entrypoint_name, + entrypoint_description: entrypoint.entrypoint_description, + entrypoint_type: match entrypoint.entrypoint_type { + SettingsEntrypointType::Command => RpcEntrypointTypeSettings::SCommand, + SettingsEntrypointType::View => RpcEntrypointTypeSettings::SView, + SettingsEntrypointType::InlineView => RpcEntrypointTypeSettings::SInlineView, + SettingsEntrypointType::EntrypointGenerator => { + RpcEntrypointTypeSettings::SEntrypointGenerator + } + } + .into(), + preferences: entrypoint + .preferences + .into_iter() + .map(|(key, value)| (key, plugin_preference_to_rpc(value))) + .collect(), + preferences_user_data: entrypoint + .preferences_user_data + .into_iter() + .map(|(key, value)| (key, plugin_preference_user_data_to_rpc(value))) + .collect(), + } }) .collect(); @@ -167,10 +219,14 @@ impl RpcBackend for RpcBackendServerImpl { plugin_description: plugin.plugin_description, enabled: plugin.enabled, entrypoints, - preferences: plugin.preferences.into_iter() + preferences: plugin + .preferences + .into_iter() .map(|(key, value)| (key, plugin_preference_to_rpc(value))) .collect(), - preferences_user_data: plugin.preferences_user_data.into_iter() + preferences_user_data: plugin + .preferences_user_data + .into_iter() .map(|(key, value)| (key, plugin_preference_user_data_to_rpc(value))) .collect(), } @@ -180,21 +236,28 @@ impl RpcBackend for RpcBackendServerImpl { Ok(Response::new(RpcPluginsResponse { plugins })) } - async fn set_plugin_state(&self, request: Request) -> Result, Status> { + async fn set_plugin_state( + &self, + request: Request, + ) -> Result, Status> { let request = request.into_inner(); let plugin_id = request.plugin_id; let enabled = request.enabled; let plugin_id = PluginId::from_string(plugin_id); - self.server.set_plugin_state(plugin_id, enabled) + self.server + .set_plugin_state(plugin_id, enabled) .await .map_err(|err| Status::internal(format!("{:#}", err)))?; Ok(Response::new(RpcSetPluginStateResponse::default())) } - async fn set_entrypoint_state(&self, request: Request) -> Result, Status> { + async fn set_entrypoint_state( + &self, + request: Request, + ) -> Result, Status> { let request = request.into_inner(); let plugin_id = request.plugin_id; let entrypoint_id = request.entrypoint_id; @@ -203,14 +266,18 @@ impl RpcBackend for RpcBackendServerImpl { let plugin_id = PluginId::from_string(plugin_id); let entrypoint_id = EntrypointId::from_string(entrypoint_id); - self.server.set_entrypoint_state(plugin_id, entrypoint_id, enabled) + self.server + .set_entrypoint_state(plugin_id, entrypoint_id, enabled) .await .map_err(|err| Status::internal(format!("{:#}", err)))?; Ok(Response::new(RpcSetEntrypointStateResponse::default())) } - async fn set_preference_value(&self, request: Request) -> Result, Status> { + async fn set_preference_value( + &self, + request: Request, + ) -> Result, Status> { let request = request.into_inner(); let plugin_id = request.plugin_id; let plugin_id = PluginId::from_string(plugin_id); @@ -224,55 +291,70 @@ impl RpcBackend for RpcBackendServerImpl { let preference_id = request.preference_id; let preference_value = request.preference_value.unwrap(); - self.server.set_preference_value(plugin_id, entrypoint_id, preference_id, plugin_preference_user_data_from_rpc(preference_value)) + self.server + .set_preference_value( + plugin_id, + entrypoint_id, + preference_id, + plugin_preference_user_data_from_rpc(preference_value), + ) .await .map_err(|err| Status::internal(format!("{:#}", err)))?; Ok(Response::new(RpcSetPreferenceValueResponse::default())) } - async fn set_global_shortcut(&self, request: Request) -> Result, Status> { + async fn set_global_shortcut( + &self, + request: Request, + ) -> Result, Status> { let request = request.into_inner(); - let shortcut = request.shortcut - .map(|shortcut| { - let physical_key = shortcut.physical_key; - let modifier_shift = shortcut.modifier_shift; - let modifier_control = shortcut.modifier_control; - let modifier_alt = shortcut.modifier_alt; - let modifier_meta = shortcut.modifier_meta; + let shortcut = request.shortcut.map(|shortcut| { + let physical_key = shortcut.physical_key; + let modifier_shift = shortcut.modifier_shift; + let modifier_control = shortcut.modifier_control; + let modifier_alt = shortcut.modifier_alt; + let modifier_meta = shortcut.modifier_meta; - PhysicalShortcut { - physical_key: PhysicalKey::from_value(physical_key), - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - } - }); + PhysicalShortcut { + physical_key: PhysicalKey::from_value(physical_key), + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + } + }); - let error = self.server.set_global_shortcut(shortcut) + let error = self + .server + .set_global_shortcut(shortcut) .await .map_err(|err| format!("{:#}", err)) .err(); - Ok(Response::new(RpcSetGlobalShortcutResponse { - error - })) + Ok(Response::new(RpcSetGlobalShortcutResponse { error })) } - async fn get_global_shortcut(&self, _request: Request) -> Result, Status> { - let (shortcut, error) = self.server.get_global_shortcut() + async fn get_global_shortcut( + &self, + _request: Request, + ) -> Result, Status> { + let (shortcut, error) = self + .server + .get_global_shortcut() .await .map_err(|err| Status::internal(format!("{:#}", err)))?; Ok(Response::new(RpcGetGlobalShortcutResponse { - shortcut: shortcut.map(|shortcut| RpcShortcut { - physical_key: shortcut.physical_key.to_value(), - modifier_shift: shortcut.modifier_shift, - modifier_control: shortcut.modifier_control, - modifier_alt: shortcut.modifier_alt, - modifier_meta: shortcut.modifier_meta, + shortcut: shortcut.map(|shortcut| { + RpcShortcut { + physical_key: shortcut.physical_key.to_value(), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + } }), error, })) @@ -288,10 +370,11 @@ impl RpcBackend for RpcBackendServerImpl { "MacOSLight" => SettingsTheme::MacOSLight, "MacOSDark" => SettingsTheme::MacOSDark, "Legacy" => SettingsTheme::Legacy, - _ => unreachable!() + _ => unreachable!(), }; - self.server.set_theme(theme) + self.server + .set_theme(theme) .await .map_err(|err| Status::internal(format!("{:#}", err)))?; @@ -299,7 +382,9 @@ impl RpcBackend for RpcBackendServerImpl { } async fn get_theme(&self, _request: Request) -> Result, Status> { - let theme = self.server.get_theme() + let theme = self + .server + .get_theme() .await .map_err(|err| Status::internal(format!("{:#}", err)))?; @@ -316,24 +401,33 @@ impl RpcBackend for RpcBackendServerImpl { theme: theme.to_string(), })) } - async fn set_window_position_mode(&self, request: Request) -> Result, Status> { + async fn set_window_position_mode( + &self, + request: Request, + ) -> Result, Status> { let mode = request.into_inner().mode; let mode = match mode.as_str() { "Static" => WindowPositionMode::Static, "ActiveMonitor" => WindowPositionMode::ActiveMonitor, - _ => unreachable!() + _ => unreachable!(), }; - self.server.set_window_position_mode(mode) + self.server + .set_window_position_mode(mode) .await .map_err(|err| Status::internal(format!("{:#}", err)))?; Ok(Response::new(RpcSetWindowPositionModeResponse::default())) } - async fn get_window_position_mode(&self, _request: Request) -> Result, Status> { - let mode = self.server.get_window_position_mode() + async fn get_window_position_mode( + &self, + _request: Request, + ) -> Result, Status> { + let mode = self + .server + .get_window_position_mode() .await .map_err(|err| Status::internal(format!("{:#}", err)))?; @@ -347,21 +441,30 @@ impl RpcBackend for RpcBackendServerImpl { })) } - async fn download_plugin(&self, request: Request) -> Result, Status> { + async fn download_plugin( + &self, + request: Request, + ) -> Result, Status> { let request = request.into_inner(); let plugin_id = request.plugin_id; let plugin_id = PluginId::from_string(plugin_id); - self.server.download_plugin(plugin_id) + self.server + .download_plugin(plugin_id) .await .map_err(|err| Status::internal(format!("{:#}", err)))?; Ok(Response::new(RpcDownloadPluginResponse::default())) } - async fn download_status(&self, _: Request) -> Result, Status> { - let status_per_plugin = self.server.download_status() + async fn download_status( + &self, + _: Request, + ) -> Result, Status> { + let status_per_plugin = self + .server + .download_status() .await .map_err(|err| Status::internal(format!("{:#}", err)))? .into_iter() @@ -372,35 +475,48 @@ impl RpcBackend for RpcBackendServerImpl { DownloadStatus::Failed { message } => (RpcDownloadStatus::Failed, message), }; - (plugin_id.to_string(), RpcDownloadStatusValue { status: status.into(), message }) + ( + plugin_id.to_string(), + RpcDownloadStatusValue { + status: status.into(), + message, + }, + ) }) .collect(); - let response = RpcDownloadStatusResponse { - status_per_plugin, - }; + let response = RpcDownloadStatusResponse { status_per_plugin }; Ok(Response::new(response)) } - async fn remove_plugin(&self, request: Request) -> Result, Status> { + async fn remove_plugin( + &self, + request: Request, + ) -> Result, Status> { let request = request.into_inner(); let plugin_id = request.plugin_id; let plugin_id = PluginId::from_string(plugin_id); - self.server.remove_plugin(plugin_id) + self.server + .remove_plugin(plugin_id) .await .map_err(|err| Status::internal(format!("{:#}", err)))?; Ok(Response::new(RpcRemovePluginResponse::default())) } - async fn save_local_plugin(&self, request: Request) -> Result, Status> { + async fn save_local_plugin( + &self, + request: Request, + ) -> Result, Status> { let request = request.into_inner(); let path = request.path; - let local_save_data = self.server.save_local_plugin(path) + let local_save_data = self + .server + .save_local_plugin(path) .await .map_err(|err| Status::internal(format!("{:#}", err)))?; diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index fbd0c04..7f0cc73 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -1,9 +1,20 @@ use std::collections::HashMap; -use anyhow::anyhow; -use thiserror::Error; -use gauntlet_utils::channel::{RequestError, RequestSender}; -use crate::model::{EntrypointId, UiTheme, PhysicalShortcut, PluginId, RootWidget, UiRenderLocation, UiRequestData, UiResponseData, UiWidgetId, WindowPositionMode}; +use anyhow::anyhow; +use gauntlet_utils::channel::RequestError; +use gauntlet_utils::channel::RequestSender; +use thiserror::Error; + +use crate::model::EntrypointId; +use crate::model::PhysicalShortcut; +use crate::model::PluginId; +use crate::model::RootWidget; +use crate::model::UiRenderLocation; +use crate::model::UiRequestData; +use crate::model::UiResponseData; +use crate::model::UiTheme; +use crate::model::UiWidgetId; +use crate::model::WindowPositionMode; #[derive(Error, Debug)] pub enum FrontendApiError { @@ -17,7 +28,7 @@ impl From for FrontendApiError { fn from(error: RequestError) -> FrontendApiError { match error { RequestError::TimeoutError => FrontendApiError::TimeoutError, - RequestError::OtherSideWasDropped => FrontendApiError::OtherError(anyhow!("other side was dropped")) + RequestError::OtherSideWasDropped => FrontendApiError::OtherError(anyhow!("other side was dropped")), } } } @@ -29,13 +40,14 @@ pub struct FrontendApi { impl FrontendApi { pub fn new(frontend_sender: RequestSender) -> Self { - Self { - frontend_sender - } + Self { frontend_sender } } pub async fn request_search_results_update(&self) -> Result<(), FrontendApiError> { - let _ = self.frontend_sender.send_receive(UiRequestData::RequestSearchResultUpdate).await; + let _ = self + .frontend_sender + .send_receive(UiRequestData::RequestSearchResultUpdate) + .await; Ok(()) } @@ -70,9 +82,7 @@ impl FrontendApi { } pub async fn clear_inline_view(&self, plugin_id: PluginId) -> Result<(), FrontendApiError> { - let request = UiRequestData::ClearInlineView { - plugin_id, - }; + let request = UiRequestData::ClearInlineView { plugin_id }; let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await? else { unreachable!() @@ -137,13 +147,8 @@ impl FrontendApi { Ok(()) } - pub async fn show_hud( - &self, - display: String, - ) -> Result<(), FrontendApiError> { - let request = UiRequestData::ShowHud { - display, - }; + pub async fn show_hud(&self, display: String) -> Result<(), FrontendApiError> { + let request = UiRequestData::ShowHud { display }; let UiResponseData::Nothing = self.frontend_sender.send_receive(request).await? else { unreachable!() @@ -156,7 +161,7 @@ impl FrontendApi { &self, plugin_id: PluginId, entrypoint_id: EntrypointId, - show: bool + show: bool, ) -> Result<(), FrontendApiError> { let request = UiRequestData::UpdateLoadingBar { plugin_id, @@ -171,57 +176,48 @@ impl FrontendApi { Ok(()) } - pub async fn set_global_shortcut( - &self, - shortcut: Option - ) -> anyhow::Result<()> { - let request = UiRequestData::SetGlobalShortcut { - shortcut, - }; + pub async fn set_global_shortcut(&self, shortcut: Option) -> anyhow::Result<()> { + let request = UiRequestData::SetGlobalShortcut { shortcut }; - let data = self.frontend_sender.send_receive(request) + let data = self + .frontend_sender + .send_receive(request) .await .map_err(|err| anyhow!("error: {:?}", err))?; match data { UiResponseData::Nothing => Ok(()), - UiResponseData::Err(err) => Err(err) + UiResponseData::Err(err) => Err(err), } } - pub async fn set_theme( - &self, - theme: UiTheme - ) -> anyhow::Result<()> { - let request = UiRequestData::SetTheme { - theme, - }; + pub async fn set_theme(&self, theme: UiTheme) -> anyhow::Result<()> { + let request = UiRequestData::SetTheme { theme }; - let data = self.frontend_sender.send_receive(request) + let data = self + .frontend_sender + .send_receive(request) .await .map_err(|err| anyhow!("error: {:?}", err))?; match data { UiResponseData::Nothing => Ok(()), - UiResponseData::Err(err) => Err(err) + UiResponseData::Err(err) => Err(err), } } - pub async fn set_window_position_mode( - &self, - mode: WindowPositionMode - ) -> anyhow::Result<()> { - let request = UiRequestData::SetWindowPositionMode { - mode, - }; + pub async fn set_window_position_mode(&self, mode: WindowPositionMode) -> anyhow::Result<()> { + let request = UiRequestData::SetWindowPositionMode { mode }; - let data = self.frontend_sender.send_receive(request) + let data = self + .frontend_sender + .send_receive(request) .await .map_err(|err| anyhow!("error: {:?}", err))?; match data { UiResponseData::Nothing => Ok(()), - UiResponseData::Err(err) => Err(err) + UiResponseData::Err(err) => Err(err), } } -} \ No newline at end of file +} diff --git a/rust/common/src/rpc/grpc.rs b/rust/common/src/rpc/grpc.rs index 9f438f2..0ee3c5b 100644 --- a/rust/common/src/rpc/grpc.rs +++ b/rust/common/src/rpc/grpc.rs @@ -1,2 +1 @@ - tonic::include_proto!("_"); diff --git a/rust/common/src/rpc/grpc_convert.rs b/rust/common/src/rpc/grpc_convert.rs index f80d905..1bcaaaa 100644 --- a/rust/common/src/rpc/grpc_convert.rs +++ b/rust/common/src/rpc/grpc_convert.rs @@ -1,127 +1,124 @@ -use crate::model::{PluginPreference, PluginPreferenceUserData, PreferenceEnumValue}; +use crate::model::PluginPreference; +use crate::model::PluginPreferenceUserData; +use crate::model::PreferenceEnumValue; use crate::rpc::grpc::rpc_ui_property_value::Value; -use crate::rpc::grpc::{RpcEnumValue, RpcPluginPreference, RpcPluginPreferenceUserData, RpcPluginPreferenceValueType, RpcUiPropertyValue}; +use crate::rpc::grpc::RpcEnumValue; +use crate::rpc::grpc::RpcPluginPreference; +use crate::rpc::grpc::RpcPluginPreferenceUserData; +use crate::rpc::grpc::RpcPluginPreferenceValueType; +use crate::rpc::grpc::RpcUiPropertyValue; pub fn plugin_preference_user_data_from_rpc(value: RpcPluginPreferenceUserData) -> PluginPreferenceUserData { let value_type: RpcPluginPreferenceValueType = value.r#type.try_into().unwrap(); match value_type { RpcPluginPreferenceValueType::Number => { - let value = value.value - .map(|value| { - match value.value.unwrap() { - Value::Number(value) => value, - _ => unreachable!() - } - }); + let value = value.value.map(|value| { + match value.value.unwrap() { + Value::Number(value) => value, + _ => unreachable!(), + } + }); - PluginPreferenceUserData::Number { - value - } + PluginPreferenceUserData::Number { value } } RpcPluginPreferenceValueType::String => { - let value = value.value - .map(|value| { - match value.value.unwrap() { - Value::String(value) => value, - _ => unreachable!() - } - }); + let value = value.value.map(|value| { + match value.value.unwrap() { + Value::String(value) => value, + _ => unreachable!(), + } + }); - PluginPreferenceUserData::String { - value - } + PluginPreferenceUserData::String { value } } RpcPluginPreferenceValueType::Enum => { - let value = value.value - .map(|value| { - match value.value.unwrap() { - Value::String(value) => value, - _ => unreachable!() - } - }); + let value = value.value.map(|value| { + match value.value.unwrap() { + Value::String(value) => value, + _ => unreachable!(), + } + }); - PluginPreferenceUserData::Enum { - value - } + PluginPreferenceUserData::Enum { value } } RpcPluginPreferenceValueType::Bool => { - let value = value.value - .map(|value| { - match value.value.unwrap() { - Value::Bool(value) => value, - _ => unreachable!() - } - }); + let value = value.value.map(|value| { + match value.value.unwrap() { + Value::Bool(value) => value, + _ => unreachable!(), + } + }); - PluginPreferenceUserData::Bool { - value - } + PluginPreferenceUserData::Bool { value } } RpcPluginPreferenceValueType::ListOfStrings => { let value = match value.value_list_exists { true => { - let value_list = value.value_list + let value_list = value + .value_list .into_iter() - .flat_map(|value| value.value.map(|value| { - match value { - Value::String(value) => value, - _ => unreachable!() - } - })) + .flat_map(|value| { + value.value.map(|value| { + match value { + Value::String(value) => value, + _ => unreachable!(), + } + }) + }) .collect(); Some(value_list) } - false => None + false => None, }; - PluginPreferenceUserData::ListOfStrings { - value, - } + PluginPreferenceUserData::ListOfStrings { value } } RpcPluginPreferenceValueType::ListOfNumbers => { let value = match value.value_list_exists { true => { - let value_list = value.value_list + let value_list = value + .value_list .into_iter() - .flat_map(|value| value.value.map(|value| { - match value { - Value::Number(value) => value, - _ => unreachable!() - } - })) + .flat_map(|value| { + value.value.map(|value| { + match value { + Value::Number(value) => value, + _ => unreachable!(), + } + }) + }) .collect(); Some(value_list) } - false => None + false => None, }; - PluginPreferenceUserData::ListOfNumbers { - value, - } + PluginPreferenceUserData::ListOfNumbers { value } } RpcPluginPreferenceValueType::ListOfEnums => { let value = match value.value_list_exists { true => { - let value_list = value.value_list + let value_list = value + .value_list .into_iter() - .flat_map(|value| value.value.map(|value| { - match value { - Value::String(value) => value, - _ => unreachable!() - } - })) + .flat_map(|value| { + value.value.map(|value| { + match value { + Value::String(value) => value, + _ => unreachable!(), + } + }) + }) .collect(); Some(value_list) } - false => None + false => None, }; - PluginPreferenceUserData::ListOfEnums { - value, - } + PluginPreferenceUserData::ListOfEnums { value } } } } @@ -131,28 +128,44 @@ pub fn plugin_preference_user_data_to_rpc(value: PluginPreferenceUserData) -> Rp PluginPreferenceUserData::Number { value } => { RpcPluginPreferenceUserData { r#type: RpcPluginPreferenceValueType::Number.into(), - value: value.map(|value| RpcUiPropertyValue { value: Some(Value::Number(value)) }), + value: value.map(|value| { + RpcUiPropertyValue { + value: Some(Value::Number(value)), + } + }), ..RpcPluginPreferenceUserData::default() } } PluginPreferenceUserData::String { value } => { RpcPluginPreferenceUserData { r#type: RpcPluginPreferenceValueType::String.into(), - value: value.map(|value| RpcUiPropertyValue { value: Some(Value::String(value)) }), + value: value.map(|value| { + RpcUiPropertyValue { + value: Some(Value::String(value)), + } + }), ..RpcPluginPreferenceUserData::default() } } PluginPreferenceUserData::Enum { value } => { RpcPluginPreferenceUserData { r#type: RpcPluginPreferenceValueType::Enum.into(), - value: value.map(|value| RpcUiPropertyValue { value: Some(Value::String(value)) }), + value: value.map(|value| { + RpcUiPropertyValue { + value: Some(Value::String(value)), + } + }), ..RpcPluginPreferenceUserData::default() } } PluginPreferenceUserData::Bool { value } => { RpcPluginPreferenceUserData { r#type: RpcPluginPreferenceValueType::Bool.into(), - value: value.map(|value| RpcUiPropertyValue { value: Some(Value::Bool(value)) }), + value: value.map(|value| { + RpcUiPropertyValue { + value: Some(Value::Bool(value)), + } + }), ..RpcPluginPreferenceUserData::default() } } @@ -160,7 +173,18 @@ pub fn plugin_preference_user_data_to_rpc(value: PluginPreferenceUserData) -> Rp let exists = value.is_some(); RpcPluginPreferenceUserData { r#type: RpcPluginPreferenceValueType::ListOfStrings.into(), - value_list: value.map(|value| value.into_iter().map(|value| RpcUiPropertyValue { value: Some(Value::String(value)) }).collect()).unwrap_or(vec![]), + value_list: value + .map(|value| { + value + .into_iter() + .map(|value| { + RpcUiPropertyValue { + value: Some(Value::String(value)), + } + }) + .collect() + }) + .unwrap_or(vec![]), value_list_exists: exists, ..RpcPluginPreferenceUserData::default() } @@ -169,7 +193,18 @@ pub fn plugin_preference_user_data_to_rpc(value: PluginPreferenceUserData) -> Rp let exists = value.is_some(); RpcPluginPreferenceUserData { r#type: RpcPluginPreferenceValueType::ListOfNumbers.into(), - value_list: value.map(|value| value.into_iter().map(|value| RpcUiPropertyValue { value: Some(Value::Number(value)) }).collect()).unwrap_or(vec![]), + value_list: value + .map(|value| { + value + .into_iter() + .map(|value| { + RpcUiPropertyValue { + value: Some(Value::Number(value)), + } + }) + .collect() + }) + .unwrap_or(vec![]), value_list_exists: exists, ..RpcPluginPreferenceUserData::default() } @@ -178,7 +213,18 @@ pub fn plugin_preference_user_data_to_rpc(value: PluginPreferenceUserData) -> Rp let exists = value.is_some(); RpcPluginPreferenceUserData { r#type: RpcPluginPreferenceValueType::ListOfEnums.into(), - value_list: value.map(|value| value.into_iter().map(|value| RpcUiPropertyValue { value: Some(Value::String(value)) }).collect()).unwrap_or(vec![]), + value_list: value + .map(|value| { + value + .into_iter() + .map(|value| { + RpcUiPropertyValue { + value: Some(Value::String(value)), + } + }) + .collect() + }) + .unwrap_or(vec![]), value_list_exists: exists, ..RpcPluginPreferenceUserData::default() } @@ -188,71 +234,162 @@ pub fn plugin_preference_user_data_to_rpc(value: PluginPreferenceUserData) -> Rp pub fn plugin_preference_to_rpc(value: PluginPreference) -> RpcPluginPreference { match value { - PluginPreference::Number { name, default, description } => { + PluginPreference::Number { + name, + default, + description, + } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::Number.into(), - default: default.map(|value| RpcUiPropertyValue { value: Some(Value::Number(value)) }), + default: default.map(|value| { + RpcUiPropertyValue { + value: Some(Value::Number(value)), + } + }), name, description, ..RpcPluginPreference::default() } } - PluginPreference::String { name, default, description } => { + PluginPreference::String { + name, + default, + description, + } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::String.into(), - default: default.map(|value| RpcUiPropertyValue { value: Some(Value::String(value)) }), + default: default.map(|value| { + RpcUiPropertyValue { + value: Some(Value::String(value)), + } + }), name, description, ..RpcPluginPreference::default() } } - PluginPreference::Enum { name, default, description, enum_values } => { + PluginPreference::Enum { + name, + default, + description, + enum_values, + } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::Enum.into(), - default: default.map(|value| RpcUiPropertyValue { value: Some(Value::String(value)) }), + default: default.map(|value| { + RpcUiPropertyValue { + value: Some(Value::String(value)), + } + }), name, description, - enum_values: enum_values.into_iter() - .map(|value| RpcEnumValue { label: value.label, value: value.value }) + enum_values: enum_values + .into_iter() + .map(|value| { + RpcEnumValue { + label: value.label, + value: value.value, + } + }) .collect(), ..RpcPluginPreference::default() } } - PluginPreference::Bool { name, default, description } => { + PluginPreference::Bool { + name, + default, + description, + } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::Bool.into(), - default: default.map(|value| RpcUiPropertyValue { value: Some(Value::Bool(value)) }), + default: default.map(|value| { + RpcUiPropertyValue { + value: Some(Value::Bool(value)), + } + }), name, description, ..RpcPluginPreference::default() } } - PluginPreference::ListOfStrings { name, default, description } => { + PluginPreference::ListOfStrings { + name, + default, + description, + } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::ListOfStrings.into(), - default_list: default.map(|value| value.into_iter().map(|value| RpcUiPropertyValue { value: Some(Value::String(value)) }).collect()).unwrap_or(vec![]), + default_list: default + .map(|value| { + value + .into_iter() + .map(|value| { + RpcUiPropertyValue { + value: Some(Value::String(value)), + } + }) + .collect() + }) + .unwrap_or(vec![]), name, description, ..RpcPluginPreference::default() } } - PluginPreference::ListOfNumbers { name, default, description } => { + PluginPreference::ListOfNumbers { + name, + default, + description, + } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::ListOfNumbers.into(), - default_list: default.map(|value| value.into_iter().map(|value| RpcUiPropertyValue { value: Some(Value::Number(value)) }).collect()).unwrap_or(vec![]), + default_list: default + .map(|value| { + value + .into_iter() + .map(|value| { + RpcUiPropertyValue { + value: Some(Value::Number(value)), + } + }) + .collect() + }) + .unwrap_or(vec![]), name, description, ..RpcPluginPreference::default() } } - PluginPreference::ListOfEnums { name, default, enum_values, description } => { + PluginPreference::ListOfEnums { + name, + default, + enum_values, + description, + } => { RpcPluginPreference { r#type: RpcPluginPreferenceValueType::ListOfEnums.into(), - default_list: default.map(|value| value.into_iter().map(|value| RpcUiPropertyValue { value: Some(Value::String(value)) }).collect()).unwrap_or(vec![]), + default_list: default + .map(|value| { + value + .into_iter() + .map(|value| { + RpcUiPropertyValue { + value: Some(Value::String(value)), + } + }) + .collect() + }) + .unwrap_or(vec![]), name, description, - enum_values: enum_values.into_iter() - .map(|value| RpcEnumValue { label: value.label, value: value.value }) + enum_values: enum_values + .into_iter() + .map(|value| { + RpcEnumValue { + label: value.label, + value: value.value, + } + }) .collect(), ..RpcPluginPreference::default() } @@ -264,13 +401,12 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc let value_type: RpcPluginPreferenceValueType = value.r#type.try_into().unwrap(); match value_type { RpcPluginPreferenceValueType::Number => { - let default = value.default - .map(|value| { - match value.value.unwrap() { - Value::Number(value) => value, - _ => unreachable!() - } - }); + let default = value.default.map(|value| { + match value.value.unwrap() { + Value::Number(value) => value, + _ => unreachable!(), + } + }); PluginPreference::Number { default, @@ -279,13 +415,12 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc } } RpcPluginPreferenceValueType::String => { - let default = value.default - .map(|value| { - match value.value.unwrap() { - Value::String(value) => value, - _ => unreachable!() - } - }); + let default = value.default.map(|value| { + match value.value.unwrap() { + Value::String(value) => value, + _ => unreachable!(), + } + }); PluginPreference::String { default, @@ -294,31 +429,36 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc } } RpcPluginPreferenceValueType::Enum => { - let default = value.default - .map(|value| { - match value.value.unwrap() { - Value::String(value) => value, - _ => unreachable!() - } - }); + let default = value.default.map(|value| { + match value.value.unwrap() { + Value::String(value) => value, + _ => unreachable!(), + } + }); PluginPreference::Enum { default, name: value.name, description: value.description, - enum_values: value.enum_values.into_iter() - .map(|value| PreferenceEnumValue { label: value.label, value: value.value }) - .collect() + enum_values: value + .enum_values + .into_iter() + .map(|value| { + PreferenceEnumValue { + label: value.label, + value: value.value, + } + }) + .collect(), } } RpcPluginPreferenceValueType::Bool => { - let default = value.default - .map(|value| { - match value.value.unwrap() { - Value::Bool(value) => value, - _ => unreachable!() - } - }); + let default = value.default.map(|value| { + match value.value.unwrap() { + Value::Bool(value) => value, + _ => unreachable!(), + } + }); PluginPreference::Bool { default, @@ -329,19 +469,22 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc RpcPluginPreferenceValueType::ListOfStrings => { let default_list = match value.default_list_exists { true => { - let default_list = value.default_list + let default_list = value + .default_list .into_iter() - .flat_map(|value| value.value.map(|value| { - match value { - Value::String(value) => value, - _ => unreachable!() - } - })) + .flat_map(|value| { + value.value.map(|value| { + match value { + Value::String(value) => value, + _ => unreachable!(), + } + }) + }) .collect(); Some(default_list) - }, - false => None + } + false => None, }; PluginPreference::ListOfStrings { @@ -353,19 +496,22 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc RpcPluginPreferenceValueType::ListOfNumbers => { let default_list = match value.default_list_exists { true => { - let default_list = value.default_list + let default_list = value + .default_list .into_iter() - .flat_map(|value| value.value.map(|value| { - match value { - Value::Number(value) => value, - _ => unreachable!() - } - })) + .flat_map(|value| { + value.value.map(|value| { + match value { + Value::Number(value) => value, + _ => unreachable!(), + } + }) + }) .collect(); Some(default_list) - }, - false => None + } + false => None, }; PluginPreference::ListOfNumbers { @@ -377,30 +523,39 @@ pub fn plugin_preference_from_rpc(value: RpcPluginPreference) -> PluginPreferenc RpcPluginPreferenceValueType::ListOfEnums => { let default_list = match value.default_list_exists { true => { - let default_list = value.default_list + let default_list = value + .default_list .into_iter() - .flat_map(|value| value.value.map(|value| { - match value { - Value::String(value) => value, - _ => unreachable!() - } - })) + .flat_map(|value| { + value.value.map(|value| { + match value { + Value::String(value) => value, + _ => unreachable!(), + } + }) + }) .collect(); Some(default_list) - }, - false => None + } + false => None, }; PluginPreference::ListOfEnums { default: default_list, name: value.name, - enum_values: value.enum_values.into_iter() - .map(|value| PreferenceEnumValue { label: value.label, value: value.value }) + enum_values: value + .enum_values + .into_iter() + .map(|value| { + PreferenceEnumValue { + label: value.label, + value: value.value, + } + }) .collect(), description: value.description, } } } } - diff --git a/rust/common/src/rpc/mod.rs b/rust/common/src/rpc/mod.rs index e0a2ea8..b7f6e46 100644 --- a/rust/common/src/rpc/mod.rs +++ b/rust/common/src/rpc/mod.rs @@ -1,5 +1,5 @@ pub mod backend_api; -pub mod frontend_api; pub mod backend_server; +pub mod frontend_api; mod grpc; mod grpc_convert; diff --git a/rust/common/src/scenario_model.rs b/rust/common/src/scenario_model.rs index aa67d3d..534eb74 100644 --- a/rust/common/src/scenario_model.rs +++ b/rust/common/src/scenario_model.rs @@ -1,11 +1,15 @@ use std::collections::HashMap; -use serde::{Deserialize, Serialize}; -use crate::model::{RootWidget, UiWidgetId}; + +use serde::Deserialize; +use serde::Serialize; + +use crate::model::RootWidget; +use crate::model::UiWidgetId; #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] pub enum ScenarioUiRenderLocation { InlineView, - View + View, } #[derive(Debug, Deserialize, Serialize)] @@ -16,7 +20,7 @@ pub enum ScenarioFrontendEvent { render_location: ScenarioUiRenderLocation, top_level_view: bool, container: RootWidget, - #[serde(with="base64")] + #[serde(with = "base64")] images: HashMap>, }, ShowPreferenceRequiredView { @@ -33,14 +37,19 @@ pub enum ScenarioFrontendEvent { mod base64 { use std::collections::HashMap; use std::str::FromStr; - use serde::{Serialize, Deserialize}; - use serde::{Deserializer, Serializer}; - use base64::Engine; + use base64::engine::general_purpose::STANDARD; + use base64::Engine; + use serde::Deserialize; + use serde::Deserializer; + use serde::Serialize; + use serde::Serializer; + use crate::model::UiWidgetId; pub fn serialize(v: &HashMap>, s: S) -> Result { - let map = v.iter() + let map = v + .iter() .map(|(key, value)| (key.to_string(), STANDARD.encode(value))) .collect(); @@ -51,11 +60,11 @@ mod base64 { HashMap::::deserialize(d)? .into_iter() .map(|(key, value)| { - STANDARD.decode(value.as_bytes()) + STANDARD + .decode(value.as_bytes()) .map_err(|e| serde::de::Error::custom(e)) .map(|vec| (UiWidgetId::from_str(&key).expect("should not fail"), vec)) }) .collect() - } -} \ No newline at end of file +} diff --git a/rust/common_ui/src/lib.rs b/rust/common_ui/src/lib.rs index ab3b4a3..de24bfc 100644 --- a/rust/common_ui/src/lib.rs +++ b/rust/common_ui/src/lib.rs @@ -1,46 +1,58 @@ -use iced::{Element, Padding, Pixels}; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::PhysicalShortcut; use iced::border::Radius; use iced::keyboard::Modifiers; -use iced::widget::{text, value}; -use iced_aw::iced_fonts::{bootstrap, Bootstrap, BOOTSTRAP_FONT}; +use iced::widget::text; +use iced::widget::value; +use iced::Element; +use iced::Padding; +use iced::Pixels; +use iced_aw::iced_fonts::bootstrap; +use iced_aw::iced_fonts::Bootstrap; +use iced_aw::iced_fonts::BOOTSTRAP_FONT; -use gauntlet_common::model::{PhysicalKey, PhysicalShortcut}; - -pub fn padding(top: impl Into, right: impl Into, bottom: impl Into, left: impl Into) -> Padding { +pub fn padding( + top: impl Into, + right: impl Into, + bottom: impl Into, + left: impl Into, +) -> Padding { Padding { top: top.into().0, right: right.into().0, bottom: bottom.into().0, - left: left.into().0 + left: left.into().0, } } -pub fn radius(top_left: impl Into, top_right: impl Into, bottom_right: impl Into, bottom_left: impl Into) -> Radius { +pub fn radius( + top_left: impl Into, + top_right: impl Into, + bottom_right: impl Into, + bottom_left: impl Into, +) -> Radius { Radius { top_left: top_left.into().0, top_right: top_right.into().0, bottom_right: bottom_right.into().0, - bottom_left: bottom_left.into().0 + bottom_left: bottom_left.into().0, } } pub fn shortcut_to_text<'a, Message, Theme: text::Catalog + 'a>( - shortcut: &PhysicalShortcut + shortcut: &PhysicalShortcut, ) -> ( Element<'a, Message, Theme>, Option>, Option>, Option>, - Option> + Option>, ) { let (key_name, show_shift) = match shortcut.physical_key { PhysicalKey::Enter => { let key_name = if cfg!(target_os = "macos") { - value(Bootstrap::ArrowReturnLeft) - .font(BOOTSTRAP_FONT) - .into() + value(Bootstrap::ArrowReturnLeft).font(BOOTSTRAP_FONT).into() } else { - text("Enter") - .into() + text("Enter").into() }; (key_name, shortcut.modifier_shift) @@ -48,8 +60,7 @@ pub fn shortcut_to_text<'a, Message, Theme: text::Catalog + 'a>( _ => { let (key_name, show_shift) = physical_key_name(&shortcut.physical_key, shortcut.modifier_shift); - let key_name: Element<_, _> = text(key_name) - .into(); + let key_name: Element<_, _> = text(key_name).into(); (key_name, show_shift) } @@ -57,16 +68,9 @@ pub fn shortcut_to_text<'a, Message, Theme: text::Catalog + 'a>( let alt_modifier_text = if shortcut.modifier_alt { if cfg!(target_os = "macos") { - Some( - value(Bootstrap::Option) - .font(BOOTSTRAP_FONT) - .into() - ) + Some(value(Bootstrap::Option).font(BOOTSTRAP_FONT).into()) } else { - Some( - text("Alt") - .into() - ) + Some(text("Alt").into()) } } else { None @@ -74,21 +78,14 @@ pub fn shortcut_to_text<'a, Message, Theme: text::Catalog + 'a>( let meta_modifier_text = if shortcut.modifier_meta { if cfg!(target_os = "macos") { - Some( - value(Bootstrap::Command) - .font(BOOTSTRAP_FONT) - .into() - ) + Some(value(Bootstrap::Command).font(BOOTSTRAP_FONT).into()) } else if cfg!(target_os = "windows") { Some( text("Win") // is it possible to have shortcuts that use win? - .into() + .into(), ) } else { - Some( - text("Super") - .into() - ) + Some(text("Super").into()) } } else { None @@ -99,13 +96,10 @@ pub fn shortcut_to_text<'a, Message, Theme: text::Catalog + 'a>( Some( text("^") // TODO bootstrap doesn't have proper macos ctrl icon .font(BOOTSTRAP_FONT) - .into() + .into(), ) } else { - Some( - text("Ctrl") - .into() - ) + Some(text("Ctrl").into()) } } else { None @@ -113,22 +107,21 @@ pub fn shortcut_to_text<'a, Message, Theme: text::Catalog + 'a>( let shift_modifier_text = if show_shift && shortcut.modifier_shift { if cfg!(target_os = "macos") { - Some( - value(Bootstrap::Shift) - .font(BOOTSTRAP_FONT) - .into() - ) + Some(value(Bootstrap::Shift).font(BOOTSTRAP_FONT).into()) } else { - Some( - text("Shift") - .into() - ) + Some(text("Shift").into()) } } else { None }; - (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) + ( + key_name, + alt_modifier_text, + meta_modifier_text, + control_modifier_text, + shift_modifier_text, + ) } pub fn physical_key_model(key: iced::keyboard::key::Code, modifiers: Modifiers) -> Option { @@ -327,9 +320,7 @@ pub fn physical_key_model(key: iced::keyboard::key::Code, modifiers: Modifiers) | iced::keyboard::key::Code::SuperRight | iced::keyboard::key::Code::Hyper | iced::keyboard::key::Code::Turbo - | _ => { - return None - } + | _ => return None, }; let modifier_shift = modifiers.shift(); diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index 63dfefa..40aa143 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -2,7 +2,8 @@ use std::fmt::Display; use std::sync::Arc; use indexmap::IndexMap; -use serde::{Serialize, Serializer}; +use serde::Serialize; +use serde::Serializer; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ComponentName(Arc); @@ -21,8 +22,8 @@ impl Display for ComponentName { impl Serialize for ComponentName { fn serialize(&self, serializer: S) -> Result - where - S: Serializer, + where + S: Serializer, { serializer.serialize_str(&self.0) } @@ -46,7 +47,7 @@ pub enum Component { internal_name: String, children: Vec, #[serde(rename = "sharedTypes")] - shared_types: IndexMap + shared_types: IndexMap, }, #[serde(rename = "text_part")] TextPart { @@ -75,32 +76,22 @@ pub enum PropertyType { #[serde(rename = "boolean")] Boolean, #[serde(rename = "component")] - Component { - reference: ComponentRef, - }, + Component { reference: ComponentRef }, #[serde(rename = "function")] - Function { - arguments: Vec - }, + Function { arguments: Vec }, #[serde(rename = "shared_type_ref")] - SharedTypeRef { - name: String - }, + SharedTypeRef { name: String }, #[serde(rename = "union")] - Union { - items: Vec - }, + Union { items: Vec }, #[serde(rename = "array")] - Array { - item: Box - }, + Array { item: Box }, } #[derive(Debug, Clone, Eq, PartialEq)] pub enum PropertyKind { Event, Component, - Property + Property, } impl PropertyType { @@ -135,17 +126,11 @@ impl PropertyType { #[serde(tag = "type")] pub enum SharedType { #[serde(rename = "enum")] - Enum { - items: Vec - }, + Enum { items: Vec }, #[serde(rename = "object")] - Object { - items: IndexMap - }, + Object { items: IndexMap }, #[serde(rename = "union")] - Union { - items: Vec - } + Union { items: Vec }, } #[derive(Debug, Clone, Serialize)] @@ -194,8 +179,9 @@ pub struct ComponentRef { } fn children_string_or_members(ordered_members: I1, per_type_members: I2) -> Children -where I1: IntoIterator, - I2: IntoIterator +where + I1: IntoIterator, + I2: IntoIterator, { Children::StringOrMembers { text_part_internal_name: "text_part".to_owned(), @@ -205,8 +191,9 @@ where I1: IntoIterator, } fn children_members(ordered_members: I1, per_type_members: I2) -> Children -where I1: IntoIterator, - I2: IntoIterator +where + I1: IntoIterator, + I2: IntoIterator, { Children::Members { ordered_members: ordered_members.into_iter().collect(), @@ -227,14 +214,16 @@ fn children_none() -> Children { fn member(member_name: impl ToString, component: &Component, arity: Arity) -> (String, ComponentRef) { match component { - Component::Standard { internal_name, name, .. } => { + Component::Standard { + internal_name, name, .. + } => { ( member_name.to_string(), ComponentRef { component_internal_name: internal_name.to_owned(), component_name: name.to_owned(), - arity - } + arity, + }, ) } Component::Root { .. } => panic!("invalid component member"), @@ -242,9 +231,15 @@ fn member(member_name: impl ToString, component: &Component, arity: Arity) -> (S } } - -fn component(internal_name: impl ToString, description: String, name: impl ToString, properties: I, children: Children) -> Component - where I: IntoIterator +fn component( + internal_name: impl ToString, + description: String, + name: impl ToString, + properties: I, + children: Children, +) -> Component +where + I: IntoIterator, { Component::Standard { internal_name: internal_name.to_string(), @@ -257,13 +252,15 @@ fn component(internal_name: impl ToString, description: String, name: impl To fn component_ref(component: &Component, arity: Arity) -> PropertyType { match component { - Component::Standard { internal_name, name, .. } => { + Component::Standard { + internal_name, name, .. + } => { PropertyType::Component { reference: ComponentRef { component_internal_name: internal_name.to_owned(), component_name: name.to_owned(), - arity - } + arity, + }, } } Component::Root { .. } => panic!("invalid component member"), @@ -292,11 +289,18 @@ macro_rules! mark_doc { fn root(children: &[&Component]) -> Component { Component::Root { internal_name: "root".to_owned(), - children: children.into_iter() + children: children + .into_iter() .map(|child| { match child { - Component::Standard { internal_name, name, .. } => { - ComponentRef { component_name: name.to_owned(), component_internal_name: internal_name.to_owned(), arity: Arity::ZeroOrOne } + Component::Standard { + internal_name, name, .. + } => { + ComponentRef { + component_name: name.to_owned(), + component_internal_name: internal_name.to_owned(), + arity: Arity::ZeroOrOne, + } } Component::Root { .. } => panic!("invalid root child"), Component::TextPart { .. } => panic!("invalid root child"), @@ -304,286 +308,305 @@ fn root(children: &[&Component]) -> Component { }) .collect(), shared_types: IndexMap::from([ - ("Icons".to_owned(), SharedType::Enum { - items: [ - "PersonAdd", - "Airplane", - // "AirplaneLanding", - // "AirplaneTakeoff", - "Alarm", - // "AlarmRinging", - "AlignCentre", - "AlignLeft", - "AlignRight", - // "Anchor", - "ArrowClockwise", - "ArrowCounterClockwise", - "ArrowDown", - "ArrowLeft", - "ArrowRight", - "ArrowUp", - "ArrowLeftRight", - "ArrowsContract", - "ArrowsExpand", - "AtSymbol", - // "BandAid", - "Cash", - // "BarChart", - // "BarCode", - "Battery", - "BatteryCharging", - // "BatteryDisabled", - "Bell", - "BellDisabled", - // "Bike", - // "Binoculars", - // "Bird", - "Document", - "DocumentAdd", - "DocumentDelete", - "Bluetooth", - // "Boat", - "Bold", - // "Bolt", - // "BoltDisabled", - "Book", - "Bookmark", - "Box", - // "Brush", - "Bug", - "Building", - "BulletPoints", - "Calculator", - "Calendar", - "Camera", - "Car", - "Cart", - // "Cd", - // "Center", - "Checkmark", - // "ChessPiece", - "ChevronDown", - "ChevronLeft", - "ChevronRight", - "ChevronUp", - "ChevronExpand", - "Circle", - // "CircleProgress100", - // "CircleProgress25", - // "CircleProgress50", - // "CircleProgress75", - // "ClearFormatting", - "Clipboard", - "Clock", - "Cloud", - "CloudLightning", - "CloudRain", - "CloudSnow", - "CloudSun", - "Code", - "Gear", - "Coin", - "Command", - "Compass", - // "ComputerChip", - // "Contrast", - "CreditCard", - "Crop", - // "Crown", - // "Desktop", - "Dot", - "Download", - // "Duplicate", - "Eject", - "ThreeDots", - "Envelope", - "Eraser", - "ExclamationMark", - "Eye", - "EyeDisabled", - "EyeDropper", - "Female", - "Film", - "Filter", - "Fingerprint", - "Flag", - "Folder", - "FolderAdd", - "FolderDelete", - "Forward", - "GameController", - "Virus", - "Gift", - "Glasses", - "Globe", - "Hammer", - "HardDrive", - "Headphones", - "Heart", - // "HeartDisabled", - "Heartbeat", - "Hourglass", - "House", - "Image", - "Info", - "Italics", - "Key", - "Keyboard", - "Layers", - // "Leaf", - "LightBulb", - "LightBulbDisabled", - "Link", - "List", - "Lock", - // "LockDisabled", - "LockUnlocked", - // "Logout", - // "Lowercase", - // "MagnifyingGlass", - "Male", - "Map", - "Maximize", - "Megaphone", - "MemoryModule", - "MemoryStick", - "Message", - "Microphone", - "MicrophoneDisabled", - "Minimize", - "Minus", - "Mobile", - // "Monitor", - "Moon", - // "Mountain", - "Mouse", - "Multiply", - "Music", - "Network", - "Paperclip", - "Paragraph", - "Pause", - "Pencil", - "Person", - "Phone", - // "PhoneRinging", - "PieChart", - "Capsule", - // "Pin", - // "PinDisabled", - "Play", - "Plug", - "Plus", - // "PlusMinusDivideMultiply", - "Power", - "Printer", - "QuestionMark", - "Quotes", - "Receipt", - "PersonRemove", - "Repeat", - "Reply", - "Rewind", - "Rocket", - // "Ruler", - "Shield", - "Shuffle", - "Snippets", - "Snowflake", - // "VolumeHigh", - // "VolumeLow", - // "VolumeOff", - // "VolumeOn", - "Star", - // "StarDisabled", - "Stop", - "Stopwatch", - "StrikeThrough", - "Sun", - // "Syringe", - "Scissors", - "Tag", - "Thermometer", - "Terminal", - "Text", - "TextCursor", - // "TextSelection", - // "Torch", - // "Train", - "Trash", - "Tree", - "Trophy", - "People", - "Umbrella", - "Underline", - "Upload", - // "Uppercase", - "Wallet", - "Wand", - // "Warning", - // "Weights", - "Wifi", - "WifiDisabled", - "Window", - "Tools", - "Watch", - "XMark", - // - "Indent", - "Unindent", - - ].into_iter().map(|s| s.to_string()).collect() - }), - ("ImageSourceUrl".to_owned(), SharedType::Object { - items: { - let mut map = IndexMap::new(); - map.insert("url".to_string(), PropertyType::String); - map + ( + "Icons".to_owned(), + SharedType::Enum { + items: [ + "PersonAdd", + "Airplane", + // "AirplaneLanding", + // "AirplaneTakeoff", + "Alarm", + // "AlarmRinging", + "AlignCentre", + "AlignLeft", + "AlignRight", + // "Anchor", + "ArrowClockwise", + "ArrowCounterClockwise", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "ArrowUp", + "ArrowLeftRight", + "ArrowsContract", + "ArrowsExpand", + "AtSymbol", + // "BandAid", + "Cash", + // "BarChart", + // "BarCode", + "Battery", + "BatteryCharging", + // "BatteryDisabled", + "Bell", + "BellDisabled", + // "Bike", + // "Binoculars", + // "Bird", + "Document", + "DocumentAdd", + "DocumentDelete", + "Bluetooth", + // "Boat", + "Bold", + // "Bolt", + // "BoltDisabled", + "Book", + "Bookmark", + "Box", + // "Brush", + "Bug", + "Building", + "BulletPoints", + "Calculator", + "Calendar", + "Camera", + "Car", + "Cart", + // "Cd", + // "Center", + "Checkmark", + // "ChessPiece", + "ChevronDown", + "ChevronLeft", + "ChevronRight", + "ChevronUp", + "ChevronExpand", + "Circle", + // "CircleProgress100", + // "CircleProgress25", + // "CircleProgress50", + // "CircleProgress75", + // "ClearFormatting", + "Clipboard", + "Clock", + "Cloud", + "CloudLightning", + "CloudRain", + "CloudSnow", + "CloudSun", + "Code", + "Gear", + "Coin", + "Command", + "Compass", + // "ComputerChip", + // "Contrast", + "CreditCard", + "Crop", + // "Crown", + // "Desktop", + "Dot", + "Download", + // "Duplicate", + "Eject", + "ThreeDots", + "Envelope", + "Eraser", + "ExclamationMark", + "Eye", + "EyeDisabled", + "EyeDropper", + "Female", + "Film", + "Filter", + "Fingerprint", + "Flag", + "Folder", + "FolderAdd", + "FolderDelete", + "Forward", + "GameController", + "Virus", + "Gift", + "Glasses", + "Globe", + "Hammer", + "HardDrive", + "Headphones", + "Heart", + // "HeartDisabled", + "Heartbeat", + "Hourglass", + "House", + "Image", + "Info", + "Italics", + "Key", + "Keyboard", + "Layers", + // "Leaf", + "LightBulb", + "LightBulbDisabled", + "Link", + "List", + "Lock", + // "LockDisabled", + "LockUnlocked", + // "Logout", + // "Lowercase", + // "MagnifyingGlass", + "Male", + "Map", + "Maximize", + "Megaphone", + "MemoryModule", + "MemoryStick", + "Message", + "Microphone", + "MicrophoneDisabled", + "Minimize", + "Minus", + "Mobile", + // "Monitor", + "Moon", + // "Mountain", + "Mouse", + "Multiply", + "Music", + "Network", + "Paperclip", + "Paragraph", + "Pause", + "Pencil", + "Person", + "Phone", + // "PhoneRinging", + "PieChart", + "Capsule", + // "Pin", + // "PinDisabled", + "Play", + "Plug", + "Plus", + // "PlusMinusDivideMultiply", + "Power", + "Printer", + "QuestionMark", + "Quotes", + "Receipt", + "PersonRemove", + "Repeat", + "Reply", + "Rewind", + "Rocket", + // "Ruler", + "Shield", + "Shuffle", + "Snippets", + "Snowflake", + // "VolumeHigh", + // "VolumeLow", + // "VolumeOff", + // "VolumeOn", + "Star", + // "StarDisabled", + "Stop", + "Stopwatch", + "StrikeThrough", + "Sun", + // "Syringe", + "Scissors", + "Tag", + "Thermometer", + "Terminal", + "Text", + "TextCursor", + // "TextSelection", + // "Torch", + // "Train", + "Trash", + "Tree", + "Trophy", + "People", + "Umbrella", + "Underline", + "Upload", + // "Uppercase", + "Wallet", + "Wand", + // "Warning", + // "Weights", + "Wifi", + "WifiDisabled", + "Window", + "Tools", + "Watch", + "XMark", + // + "Indent", + "Unindent", + ] + .into_iter() + .map(|s| s.to_string()) + .collect(), }, - }), - ("ImageSourceAsset".to_owned(), SharedType::Object { - items: { - let mut map = IndexMap::new(); - map.insert("asset".to_string(), PropertyType::String); - map + ), + ( + "ImageSourceUrl".to_owned(), + SharedType::Object { + items: { + let mut map = IndexMap::new(); + map.insert("url".to_string(), PropertyType::String); + map + }, }, - }), - ("ImageSource".to_owned(), SharedType::Union { - items: vec![ - PropertyType::SharedTypeRef { - name: "ImageSourceUrl".to_owned() + ), + ( + "ImageSourceAsset".to_owned(), + SharedType::Object { + items: { + let mut map = IndexMap::new(); + map.insert("asset".to_string(), PropertyType::String); + map }, - PropertyType::SharedTypeRef { - name: "ImageSourceAsset".to_owned() - }, - ] - }), - ("ImageLike".to_owned(), SharedType::Union { - items: vec![ - PropertyType::SharedTypeRef { - name: "ImageSource".to_owned() - }, - PropertyType::SharedTypeRef { - name: "Icons".to_owned() - } - ], - }), + }, + ), + ( + "ImageSource".to_owned(), + SharedType::Union { + items: vec![ + PropertyType::SharedTypeRef { + name: "ImageSourceUrl".to_owned(), + }, + PropertyType::SharedTypeRef { + name: "ImageSourceAsset".to_owned(), + }, + ], + }, + ), + ( + "ImageLike".to_owned(), + SharedType::Union { + items: vec![ + PropertyType::SharedTypeRef { + name: "ImageSource".to_owned(), + }, + PropertyType::SharedTypeRef { + name: "Icons".to_owned(), + }, + ], + }, + ), ]), } } fn event(name: impl Into, description: String, optional: bool, arguments: I) -> Property - where I: IntoIterator +where + I: IntoIterator, { Property { name: name.into(), description, optional, - property_type: PropertyType::Function { arguments: arguments.into_iter().collect() }, + property_type: PropertyType::Function { + arguments: arguments.into_iter().collect(), + }, } } - fn property(name: impl Into, description: String, optional: bool, property_type: PropertyType) -> Property { Property { name: name.into(), @@ -594,62 +617,77 @@ fn property(name: impl Into, description: String, optional: bool, proper } pub fn create_component_model() -> Vec { - let action_component = component( "action", mark_doc!("/action/description.md"), "Action", [ property("id", mark_doc!("/action/props/id.md"), true, PropertyType::String), - property("label", mark_doc!("/action/props/label.md"), false, PropertyType::String), - event("onAction", mark_doc!("/action/props/onAction.md"), false, [ - property("id", "".to_string(), true, PropertyType::String) - ]) + property( + "label", + mark_doc!("/action/props/label.md"), + false, + PropertyType::String, + ), + event( + "onAction", + mark_doc!("/action/props/onAction.md"), + false, + [property("id", "".to_string(), true, PropertyType::String)], + ), ], children_none(), ); - let action_panel_section_component = component( "action_panel_section", mark_doc!("/action_panel_section/description.md"), "ActionPanelSection", - [ - property("title", mark_doc!("/action_panel_section/props/title.md"), true, PropertyType::String), - ], - children_members( - [ - member("Action", &action_component, Arity::ZeroOrMore), - ], - [] - ), + [property( + "title", + mark_doc!("/action_panel_section/props/title.md"), + true, + PropertyType::String, + )], + children_members([member("Action", &action_component, Arity::ZeroOrMore)], []), ); - let action_panel_component = component( "action_panel", mark_doc!("/action_panel/description.md"), "ActionPanel", - [ - property("title", mark_doc!("/action_panel/props/title.md"), true, PropertyType::String), - ], + [property( + "title", + mark_doc!("/action_panel/props/title.md"), + true, + PropertyType::String, + )], children_members( [ member("Action", &action_component, Arity::ZeroOrMore), member("Section", &action_panel_section_component, Arity::ZeroOrMore), ], - [] + [], ), ); - let metadata_link_component = component( "metadata_link", mark_doc!("/metadata_link/description.md"), "MetadataLink", [ - property("label", mark_doc!("/metadata_link/props/label.md"), false, PropertyType::String), - property("href", mark_doc!("/metadata_link/props/href.md"), false, PropertyType::String), + property( + "label", + mark_doc!("/metadata_link/props/label.md"), + false, + PropertyType::String, + ), + property( + "href", + mark_doc!("/metadata_link/props/href.md"), + false, + PropertyType::String, + ), ], children_string(mark_doc!("/metadata_link/props/children.md")), ); @@ -660,7 +698,7 @@ pub fn create_component_model() -> Vec { "MetadataTagItem", [ // property("color", true, PropertyType::String), - event("onClick", mark_doc!("/metadata_tag_item/props/onClick.md"), true, []) + event("onClick", mark_doc!("/metadata_tag_item/props/onClick.md"), true, []), ], children_string(mark_doc!("/metadata_tag_item/props/children.md")), ); @@ -669,15 +707,13 @@ pub fn create_component_model() -> Vec { "metadata_tag_list", mark_doc!("/metadata_tag_list/description.md"), "MetadataTagList", - [ - property("label", mark_doc!("/metadata_tag_list/props/label.md"), false, PropertyType::String) - ], - children_members( - [ - member("Item", &metadata_tag_item_component, Arity::ZeroOrMore), - ], - [], - ), + [property( + "label", + mark_doc!("/metadata_tag_list/props/label.md"), + false, + PropertyType::String, + )], + children_members([member("Item", &metadata_tag_item_component, Arity::ZeroOrMore)], []), ); let metadata_separator_component = component( @@ -693,8 +729,20 @@ pub fn create_component_model() -> Vec { mark_doc!("/metadata_icon/description.md"), "MetadataIcon", [ - property("icon", mark_doc!("/metadata_icon/props/icon.md"), false, PropertyType::SharedTypeRef { name: "Icons".to_owned() }), - property("label", mark_doc!("/metadata_icon/props/label.md"), false, PropertyType::String), + property( + "icon", + mark_doc!("/metadata_icon/props/icon.md"), + false, + PropertyType::SharedTypeRef { + name: "Icons".to_owned(), + }, + ), + property( + "label", + mark_doc!("/metadata_icon/props/label.md"), + false, + PropertyType::String, + ), ], children_none(), ); @@ -703,9 +751,12 @@ pub fn create_component_model() -> Vec { "metadata_value", mark_doc!("/metadata_value/description.md"), "MetadataValue", - [ - property("label", mark_doc!("/metadata_value/props/label.md"), false, PropertyType::String), - ], + [property( + "label", + mark_doc!("/metadata_value/props/label.md"), + false, + PropertyType::String, + )], children_string(mark_doc!("/metadata_value/props/children.md")), ); @@ -739,9 +790,14 @@ pub fn create_component_model() -> Vec { "image", mark_doc!("/image/description.md"), "Image", - [ - property("source", mark_doc!("/image/props/source.md"), false, PropertyType::SharedTypeRef { name: "ImageLike".to_owned() }) - ], + [property( + "source", + mark_doc!("/image/props/source.md"), + false, + PropertyType::SharedTypeRef { + name: "ImageLike".to_owned(), + }, + )], children_none(), ); @@ -849,7 +905,7 @@ pub fn create_component_model() -> Vec { member("CodeBlock", &code_block_component, Arity::ZeroOrMore), // member("Code", &code_component), ], - [] + [], ), ); @@ -858,8 +914,18 @@ pub fn create_component_model() -> Vec { mark_doc!("/detail/description.md"), "Detail", [ - property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), - property("actions", mark_doc!("/detail/props/actions.md"), true, component_ref(&action_panel_component, Arity::ZeroOrOne)) + property( + "isLoading", + mark_doc!("/list/props/isLoading.md"), + true, + PropertyType::Boolean, + ), + property( + "actions", + mark_doc!("/detail/props/actions.md"), + true, + component_ref(&action_panel_component, Arity::ZeroOrOne), + ), ], children_members( [], @@ -870,17 +936,29 @@ pub fn create_component_model() -> Vec { ), ); - let text_field_component = component( "text_field", mark_doc!("/text_field/description.md"), "TextField", [ - property("label", mark_doc!("/text_field/props/label.md"),true, PropertyType::String), - property("value", mark_doc!("/text_field/props/value.md"),true, PropertyType::String), - event("onChange", mark_doc!("/text_field/props/onChange.md"),true, [ - property("value", "".to_string(), true, PropertyType::String) - ]) + property( + "label", + mark_doc!("/text_field/props/label.md"), + true, + PropertyType::String, + ), + property( + "value", + mark_doc!("/text_field/props/value.md"), + true, + PropertyType::String, + ), + event( + "onChange", + mark_doc!("/text_field/props/onChange.md"), + true, + [property("value", "".to_string(), true, PropertyType::String)], + ), ], children_none(), ); @@ -890,11 +968,24 @@ pub fn create_component_model() -> Vec { mark_doc!("/password_field/description.md"), "PasswordField", [ - property("label", mark_doc!("/password_field/props/label.md"), true, PropertyType::String), - property("value", mark_doc!("/password_field/props/value.md"), true, PropertyType::String), - event("onChange", mark_doc!("/password_field/props/onChange.md"), true, [ - property("value", "".to_string(), true, PropertyType::String) - ]) + property( + "label", + mark_doc!("/password_field/props/label.md"), + true, + PropertyType::String, + ), + property( + "value", + mark_doc!("/password_field/props/value.md"), + true, + PropertyType::String, + ), + event( + "onChange", + mark_doc!("/password_field/props/onChange.md"), + true, + [property("value", "".to_string(), true, PropertyType::String)], + ), ], children_none(), ); @@ -911,12 +1002,30 @@ pub fn create_component_model() -> Vec { mark_doc!("/checkbox/description.md"), "Checkbox", [ - property("label", mark_doc!("/checkbox/props/label.md"),true, PropertyType::String), - property("title", mark_doc!("/checkbox/props/title.md"),true, PropertyType::String), - property("value", mark_doc!("/checkbox/props/value.md"),true, PropertyType::Boolean), - event("onChange", mark_doc!("/checkbox/props/onChange.md"),true, [ - property("value", "".to_string(),false, PropertyType::Boolean) - ]) + property( + "label", + mark_doc!("/checkbox/props/label.md"), + true, + PropertyType::String, + ), + property( + "title", + mark_doc!("/checkbox/props/title.md"), + true, + PropertyType::String, + ), + property( + "value", + mark_doc!("/checkbox/props/value.md"), + true, + PropertyType::Boolean, + ), + event( + "onChange", + mark_doc!("/checkbox/props/onChange.md"), + true, + [property("value", "".to_string(), false, PropertyType::Boolean)], + ), ], children_none(), ); @@ -926,11 +1035,24 @@ pub fn create_component_model() -> Vec { mark_doc!("/date_picker/description.md"), "DatePicker", [ - property("label", mark_doc!("/date_picker/props/label.md"),true, PropertyType::String), - property("value", mark_doc!("/date_picker/props/value.md"),true, PropertyType::String), - event("onChange", mark_doc!("/date_picker/props/onChange.md"),true, [ - property("value", "".to_string(), true, PropertyType::String) - ]) + property( + "label", + mark_doc!("/date_picker/props/label.md"), + true, + PropertyType::String, + ), + property( + "value", + mark_doc!("/date_picker/props/value.md"), + true, + PropertyType::String, + ), + event( + "onChange", + mark_doc!("/date_picker/props/onChange.md"), + true, + [property("value", "".to_string(), true, PropertyType::String)], + ), ], children_none(), ); @@ -939,9 +1061,12 @@ pub fn create_component_model() -> Vec { "select_item", mark_doc!("/select_item/description.md"), "SelectItem", - [ - property("value", mark_doc!("/select_item/props/value.md"),false, PropertyType::String), - ], + [property( + "value", + mark_doc!("/select_item/props/value.md"), + false, + PropertyType::String, + )], children_string(mark_doc!("/select_item/props/children.md")), ); @@ -950,18 +1075,16 @@ pub fn create_component_model() -> Vec { mark_doc!("/select/description.md"), "Select", [ - property("label", mark_doc!("/select/props/label.md"),true, PropertyType::String), - property("value", mark_doc!("/select/props/value.md"),true, PropertyType::String), - event("onChange", mark_doc!("/select/props/onChange.md"),true, [ - property("value", "".to_string(), true, PropertyType::String) - ]) + property("label", mark_doc!("/select/props/label.md"), true, PropertyType::String), + property("value", mark_doc!("/select/props/value.md"), true, PropertyType::String), + event( + "onChange", + mark_doc!("/select/props/onChange.md"), + true, + [property("value", "".to_string(), true, PropertyType::String)], + ), ], - children_members( - [ - member("Item", &select_item_component, Arity::ZeroOrMore ) - ], - [] - ), + children_members([member("Item", &select_item_component, Arity::ZeroOrMore)], []), ); // let multi_select_component = component( @@ -984,8 +1107,18 @@ pub fn create_component_model() -> Vec { mark_doc!("/form/description.md"), "Form", [ - property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), - property("actions", mark_doc!("/form/props/actions.md"), true, component_ref(&action_panel_component, Arity::ZeroOrOne)), + property( + "isLoading", + mark_doc!("/list/props/isLoading.md"), + true, + PropertyType::Boolean, + ), + property( + "actions", + mark_doc!("/form/props/actions.md"), + true, + component_ref(&action_panel_component, Arity::ZeroOrOne), + ), ], children_members( [ @@ -998,7 +1131,7 @@ pub fn create_component_model() -> Vec { // member("MultiSelect", &multi_select_component), member("Separator", &separator_component, Arity::ZeroOrMore), ], - [] + [], ), ); @@ -1006,9 +1139,14 @@ pub fn create_component_model() -> Vec { "inline_separator", mark_doc!("/inline_separator/description.md"), "InlineSeparator", - [ - property("icon", mark_doc!("/inline_separator/props/icon.md"), true, PropertyType::SharedTypeRef { name: "Icons".to_owned() }), - ], + [property( + "icon", + mark_doc!("/inline_separator/props/icon.md"), + true, + PropertyType::SharedTypeRef { + name: "Icons".to_owned(), + }, + )], children_none(), ); @@ -1016,9 +1154,12 @@ pub fn create_component_model() -> Vec { "inline", mark_doc!("/inline/description.md"), "Inline", - [ - property("actions", mark_doc!("/inline/props/actions.md"), true, component_ref(&action_panel_component, Arity::ZeroOrOne)), - ], + [property( + "actions", + mark_doc!("/inline/props/actions.md"), + true, + component_ref(&action_panel_component, Arity::ZeroOrOne), + )], children_members( [ member("Left", &content_component, Arity::ZeroOrOne), @@ -1026,7 +1167,7 @@ pub fn create_component_model() -> Vec { member("Right", &content_component, Arity::ZeroOrOne), member("Center", &content_component, Arity::ZeroOrOne), ], - [] + [], ), ); @@ -1035,9 +1176,26 @@ pub fn create_component_model() -> Vec { mark_doc!("/empty_view/description.md"), "EmptyView", [ - property("title", mark_doc!("/empty_view/props/title.md"),false, PropertyType::String), - property("description", mark_doc!("/empty_view/props/description.md"),true, PropertyType::String), - property("image", mark_doc!("/empty_view/props/image.md"),true, PropertyType::SharedTypeRef { name: "ImageLike".to_owned() }), + property( + "title", + mark_doc!("/empty_view/props/title.md"), + false, + PropertyType::String, + ), + property( + "description", + mark_doc!("/empty_view/props/description.md"), + true, + PropertyType::String, + ), + property( + "image", + mark_doc!("/empty_view/props/image.md"), + true, + PropertyType::SharedTypeRef { + name: "ImageLike".to_owned(), + }, + ), ], children_none(), ); @@ -1047,9 +1205,26 @@ pub fn create_component_model() -> Vec { mark_doc!("/accessory_text/description.md"), "TextAccessory", [ - property("text", mark_doc!("/accessory_text/props/text.md"),false, PropertyType::String), - property("icon", mark_doc!("/accessory_text/props/icon.md"),true, PropertyType::SharedTypeRef { name: "ImageLike".to_owned() }), - property("tooltip", mark_doc!("/accessory_text/props/tooltip.md"),true, PropertyType::String), + property( + "text", + mark_doc!("/accessory_text/props/text.md"), + false, + PropertyType::String, + ), + property( + "icon", + mark_doc!("/accessory_text/props/icon.md"), + true, + PropertyType::SharedTypeRef { + name: "ImageLike".to_owned(), + }, + ), + property( + "tooltip", + mark_doc!("/accessory_text/props/tooltip.md"), + true, + PropertyType::String, + ), ], children_none(), ); @@ -1059,8 +1234,20 @@ pub fn create_component_model() -> Vec { mark_doc!("/accessory_icon/description.md"), "IconAccessory", [ - property("icon", mark_doc!("/accessory_icon/props/icon.md"),false, PropertyType::SharedTypeRef { name: "ImageLike".to_owned() }), - property("tooltip", mark_doc!("/accessory_icon/props/tooltip.md"),true, PropertyType::String), + property( + "icon", + mark_doc!("/accessory_icon/props/icon.md"), + false, + PropertyType::SharedTypeRef { + name: "ImageLike".to_owned(), + }, + ), + property( + "tooltip", + mark_doc!("/accessory_icon/props/tooltip.md"), + true, + PropertyType::String, + ), ], children_none(), ); @@ -1070,11 +1257,24 @@ pub fn create_component_model() -> Vec { mark_doc!("/search_bar/description.md"), "SearchBar", [ - property("value", mark_doc!("/search_bar/props/value.md"),true, PropertyType::String), - property("placeholder", mark_doc!("/search_bar/props/placeholder.md"),true, PropertyType::String), - event("onChange", mark_doc!("/search_bar/props/onChange.md"),true, [ - property("value", "".to_string(), true, PropertyType::String) - ]) + property( + "value", + mark_doc!("/search_bar/props/value.md"), + true, + PropertyType::String, + ), + property( + "placeholder", + mark_doc!("/search_bar/props/placeholder.md"), + true, + PropertyType::String, + ), + event( + "onChange", + mark_doc!("/search_bar/props/onChange.md"), + true, + [property("value", "".to_string(), true, PropertyType::String)], + ), ], children_none(), ); @@ -1085,10 +1285,39 @@ pub fn create_component_model() -> Vec { "ListItem", [ property("id", mark_doc!("/list_item/props/id.md"), false, PropertyType::String), - property("title", mark_doc!("/list_item/props/title.md"),false, PropertyType::String), - property("subtitle", mark_doc!("/list_item/props/subtitle.md"),true, PropertyType::String), - property("icon", mark_doc!("/list_item/props/icon.md"),true, PropertyType::SharedTypeRef { name: "ImageLike".to_owned() }), - property("accessories", mark_doc!("/list_item/props/accessories.md"),true, PropertyType::Array { item: Box::new(PropertyType::Union { items: vec![component_ref(&accessory_text_component, Arity::ZeroOrMore), component_ref(&accessory_icon_component, Arity::ZeroOrMore)]}) }), + property( + "title", + mark_doc!("/list_item/props/title.md"), + false, + PropertyType::String, + ), + property( + "subtitle", + mark_doc!("/list_item/props/subtitle.md"), + true, + PropertyType::String, + ), + property( + "icon", + mark_doc!("/list_item/props/icon.md"), + true, + PropertyType::SharedTypeRef { + name: "ImageLike".to_owned(), + }, + ), + property( + "accessories", + mark_doc!("/list_item/props/accessories.md"), + true, + PropertyType::Array { + item: Box::new(PropertyType::Union { + items: vec![ + component_ref(&accessory_text_component, Arity::ZeroOrMore), + component_ref(&accessory_icon_component, Arity::ZeroOrMore), + ], + }), + }, + ), ], children_none(), ); @@ -1098,15 +1327,20 @@ pub fn create_component_model() -> Vec { mark_doc!("/list_section/description.md"), "ListSection", [ - property("title", mark_doc!("/list_section/props/title.md"),false, PropertyType::String), - property("subtitle", mark_doc!("/list_section/props/subtitle.md"),true, PropertyType::String), + property( + "title", + mark_doc!("/list_section/props/title.md"), + false, + PropertyType::String, + ), + property( + "subtitle", + mark_doc!("/list_section/props/subtitle.md"), + true, + PropertyType::String, + ), ], - children_members( - [ - member("Item", &list_item_component, Arity::ZeroOrMore), - ], - [] - ), + children_members([member("Item", &list_item_component, Arity::ZeroOrMore)], []), ); let list_component = component( @@ -1114,11 +1348,24 @@ pub fn create_component_model() -> Vec { mark_doc!("/list/description.md"), "List", [ - property("actions", mark_doc!("/list/props/actions.md"), true, component_ref(&action_panel_component, Arity::ZeroOrOne)), - property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), - event("onItemFocusChange", mark_doc!("/list/props/onItemFocusChange.md"), true, [ - property("itemId", "".to_string(), true, PropertyType::String) - ]) + property( + "actions", + mark_doc!("/list/props/actions.md"), + true, + component_ref(&action_panel_component, Arity::ZeroOrOne), + ), + property( + "isLoading", + mark_doc!("/list/props/isLoading.md"), + true, + PropertyType::Boolean, + ), + event( + "onItemFocusChange", + mark_doc!("/list/props/onItemFocusChange.md"), + true, + [property("itemId", "".to_string(), true, PropertyType::String)], + ), ], children_members( [ @@ -1129,7 +1376,7 @@ pub fn create_component_model() -> Vec { member("SearchBar", &search_bar_component, Arity::ZeroOrOne), member("EmptyView", &empty_view_component, Arity::ZeroOrOne), member("Detail", &detail_component, Arity::ZeroOrOne), - ] + ], ), ); @@ -1139,16 +1386,26 @@ pub fn create_component_model() -> Vec { "GridItem", [ property("id", mark_doc!("/list_item/props/id.md"), false, PropertyType::String), - property("title", mark_doc!("/grid_item/props/title.md"), true, PropertyType::String), - property("subtitle", mark_doc!("/grid_item/props/subtitle.md"), true, PropertyType::String), - property("accessory", mark_doc!("/grid_item/props/accessory.md"),true, component_ref(&accessory_icon_component, Arity::ZeroOrOne)), + property( + "title", + mark_doc!("/grid_item/props/title.md"), + true, + PropertyType::String, + ), + property( + "subtitle", + mark_doc!("/grid_item/props/subtitle.md"), + true, + PropertyType::String, + ), + property( + "accessory", + mark_doc!("/grid_item/props/accessory.md"), + true, + component_ref(&accessory_icon_component, Arity::ZeroOrOne), + ), ], - children_members( - [], - [ - member("Content", &content_component, Arity::One), - ] - ), + children_members([], [member("Content", &content_component, Arity::One)]), ); let grid_section_component = component( @@ -1156,19 +1413,28 @@ pub fn create_component_model() -> Vec { mark_doc!("/grid_section/description.md"), "GridSection", [ - property("title", mark_doc!("/grid_section/props/title.md"), false, PropertyType::String), - property("subtitle", mark_doc!("/grid_section/props/subtitle.md"), true, PropertyType::String), + property( + "title", + mark_doc!("/grid_section/props/title.md"), + false, + PropertyType::String, + ), + property( + "subtitle", + mark_doc!("/grid_section/props/subtitle.md"), + true, + PropertyType::String, + ), // property("aspectRatio", true, PropertyType::String), - property("columns", mark_doc!("/grid_section/props/columns.md"), true, PropertyType::Number) - // fit - // inset + property( + "columns", + mark_doc!("/grid_section/props/columns.md"), + true, + PropertyType::Number, + ), // fit + // inset ], - children_members( - [ - member("Item", &grid_item_component, Arity::ZeroOrMore), - ], - [] - ), + children_members([member("Item", &grid_item_component, Arity::ZeroOrMore)], []), ); let grid_component = component( @@ -1176,15 +1442,33 @@ pub fn create_component_model() -> Vec { mark_doc!("/grid/description.md"), "Grid", [ - property("isLoading", mark_doc!("/list/props/isLoading.md"), true, PropertyType::Boolean), - property("actions", mark_doc!("/grid/props/actions.md"),true, component_ref(&action_panel_component, Arity::ZeroOrOne)), + property( + "isLoading", + mark_doc!("/list/props/isLoading.md"), + true, + PropertyType::Boolean, + ), + property( + "actions", + mark_doc!("/grid/props/actions.md"), + true, + component_ref(&action_panel_component, Arity::ZeroOrOne), + ), // property("aspectRatio", true, PropertyType::String), - property("columns", mark_doc!("/grid/props/columns.md"),true, PropertyType::Number), // TODO default + property( + "columns", + mark_doc!("/grid/props/columns.md"), + true, + PropertyType::Number, + ), // TODO default // fit // inset - event("onItemFocusChange", mark_doc!("/grid/props/onItemFocusChange.md"), true, [ - property("itemId", "".to_string(), true, PropertyType::String) - ]) + event( + "onItemFocusChange", + mark_doc!("/grid/props/onItemFocusChange.md"), + true, + [property("itemId", "".to_string(), true, PropertyType::String)], + ), ], children_members( [ @@ -1194,7 +1478,7 @@ pub fn create_component_model() -> Vec { [ member("SearchBar", &search_bar_component, Arity::ZeroOrOne), member("EmptyView", &empty_view_component, Arity::ZeroOrOne), - ] + ], ), ); @@ -1269,11 +1553,9 @@ pub fn create_component_model() -> Vec { vec![ text_part, - action_component, action_panel_section_component, action_panel_component, - metadata_link_component, metadata_tag_item_component, metadata_tag_list_component, @@ -1281,7 +1563,6 @@ pub fn create_component_model() -> Vec { metadata_value_component, metadata_icon_component, metadata_component, - // link_component, image_component, h1_component, @@ -1295,9 +1576,7 @@ pub fn create_component_model() -> Vec { // code_component, paragraph_component, content_component, - detail_component, - text_field_component, password_field_component, // text_area_component, @@ -1308,24 +1587,18 @@ pub fn create_component_model() -> Vec { // multi_select_component, separator_component, form_component, - inline_separator_component, inline_component, - empty_view_component, - accessory_icon_component, accessory_text_component, - search_bar_component, - list_item_component, list_section_component, list_component, grid_item_component, grid_section_component, grid_component, - root, ] -} \ No newline at end of file +} diff --git a/rust/component_model/src/main.rs b/rust/component_model/src/main.rs index 4ff086f..922371a 100644 --- a/rust/component_model/src/main.rs +++ b/rust/component_model/src/main.rs @@ -1,4 +1,6 @@ -use std::{env, fs}; +use std::env; +use std::fs; + use anyhow::anyhow; use gauntlet_component_model::create_component_model; @@ -17,8 +19,6 @@ fn main() -> anyhow::Result<()> { Ok(()) } - args @ _ => { - Err(anyhow!("Unsupported args number: {args}")) - } + args @ _ => Err(anyhow!("Unsupported args number: {args}")), } } diff --git a/rust/management_client/src/components/mod.rs b/rust/management_client/src/components/mod.rs index 3565a3b..b77b782 100644 --- a/rust/management_client/src/components/mod.rs +++ b/rust/management_client/src/components/mod.rs @@ -1 +1 @@ -pub mod shortcut_selector; \ No newline at end of file +pub mod shortcut_selector; diff --git a/rust/management_client/src/components/shortcut_selector.rs b/rust/management_client/src/components/shortcut_selector.rs index 3ef1576..df54fe5 100644 --- a/rust/management_client/src/components/shortcut_selector.rs +++ b/rust/management_client/src/components/shortcut_selector.rs @@ -1,18 +1,36 @@ -use iced::{alignment, Element, Event, Length, Padding, Rectangle, Renderer, Size}; -use iced::advanced::{Clipboard, layout, Layout, mouse, renderer, Shell, Widget}; -use iced::advanced::graphics::core::{event, keyboard}; -use iced::advanced::widget::{Tree, tree}; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common_ui::physical_key_model; +use gauntlet_common_ui::shortcut_to_text; +use iced::advanced::graphics::core::event; +use iced::advanced::graphics::core::keyboard; +use iced::advanced::layout; +use iced::advanced::mouse; +use iced::advanced::renderer; +use iced::advanced::widget::tree; +use iced::advanced::widget::Tree; +use iced::advanced::Clipboard; +use iced::advanced::Layout; +use iced::advanced::Shell; +use iced::advanced::Widget; +use iced::alignment; use iced::keyboard::key::Physical; use iced::mouse::Button; -use iced::widget::{container, row, text}; -use iced::widget::container::{draw_background, layout}; - -use gauntlet_common::model::PhysicalShortcut; -use gauntlet_common_ui::{physical_key_model, shortcut_to_text}; +use iced::widget::container; +use iced::widget::container::draw_background; +use iced::widget::container::layout; +use iced::widget::row; +use iced::widget::text; +use iced::Element; +use iced::Event; +use iced::Length; +use iced::Padding; +use iced::Rectangle; +use iced::Renderer; +use iced::Size; pub struct ShortcutSelector<'a, Message, Theme> where - Theme: Catalog + text::Catalog + container::Catalog + Theme: Catalog + text::Catalog + container::Catalog, { padding: Padding, width: Length, @@ -30,27 +48,22 @@ where impl<'a, Message: 'a, Theme> ShortcutSelector<'a, Message, Theme> where - Theme: Catalog + text::Catalog + container::Catalog + 'a + Theme: Catalog + text::Catalog + container::Catalog + 'a, { pub fn new( current_shortcut: &Option, on_shortcut_captured: F, on_capturing_change: F2, ) -> Self - where - F: 'a + Fn(Option) -> Message, - F2: 'a + Fn(bool) -> Message, + where + F: 'a + Fn(Option) -> Message, + F2: 'a + Fn(bool) -> Message, { let mut content: Vec> = vec![]; if let Some(current_shortcut) = current_shortcut { - let ( - key_name, - alt_modifier_text, - meta_modifier_text, - control_modifier_text, - shift_modifier_text - ) = shortcut_to_text(current_shortcut); + let (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) = + shortcut_to_text(current_shortcut); if let Some(meta_modifier_text) = meta_modifier_text { content.push(meta_modifier_text); @@ -71,12 +84,9 @@ where content.push(key_name); } - let content: Element<_, _> = row(content) - .spacing(8.0) - .into(); + let content: Element<_, _> = row(content).spacing(8.0).into(); - let content = container(content) - .into(); + let content = container(content).into(); Self { padding: Padding::ZERO, @@ -100,10 +110,9 @@ struct State { is_capturing: bool, } - impl<'a, Message, Theme> Widget for ShortcutSelector<'a, Message, Theme> where - Theme: Catalog + text::Catalog + container::Catalog + Theme: Catalog + text::Catalog + container::Catalog, { fn size(&self) -> Size { Size { @@ -112,12 +121,7 @@ where } } - fn layout( - &self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { + fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { layout( limits, self.width, @@ -199,7 +203,11 @@ where Event::Keyboard(event) => { if state.is_capturing { match event { - keyboard::Event::KeyReleased { physical_key, modifiers, .. } => { + keyboard::Event::KeyReleased { + physical_key, + modifiers, + .. + } => { match physical_key { Physical::Code(code) => { match code { @@ -237,14 +245,13 @@ where event::Status::Captured } } - } } } - Physical::Unidentified(_) => event::Status::Ignored + Physical::Unidentified(_) => event::Status::Ignored, } } - _ => event::Status::Ignored + _ => event::Status::Ignored, } } else { event::Status::Ignored @@ -269,12 +276,11 @@ where event::Status::Ignored } } - _ => event::Status::Ignored + _ => event::Status::Ignored, } } - _ => event::Status::Ignored + _ => event::Status::Ignored, } - } fn mouse_interaction( @@ -296,7 +302,7 @@ where impl<'a, Message, Theme> From> for Element<'a, Message, Theme> where Message: 'a, - Theme: Catalog + text::Catalog + container::Catalog + 'a + Theme: Catalog + text::Catalog + container::Catalog + 'a, { fn from(shortcut_selector: ShortcutSelector<'a, Message, Theme>) -> Self { Self::new(shortcut_selector) @@ -306,7 +312,7 @@ where #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { Active, - Capturing + Capturing, } pub trait Catalog { diff --git a/rust/management_client/src/lib.rs b/rust/management_client/src/lib.rs index 44353a9..7746327 100644 --- a/rust/management_client/src/lib.rs +++ b/rust/management_client/src/lib.rs @@ -1,7 +1,7 @@ -mod ui; -mod theme; -mod views; mod components; +mod theme; +mod ui; +mod views; pub fn start_management_client() { ui::run(); diff --git a/rust/management_client/src/theme.rs b/rust/management_client/src/theme.rs index e1e81c3..bcd118d 100644 --- a/rust/management_client/src/theme.rs +++ b/rust/management_client/src/theme.rs @@ -1,16 +1,17 @@ -use iced::application::{Appearance, DefaultStyle}; +use iced::application::Appearance; +use iced::application::DefaultStyle; -pub mod container; -pub mod text; -pub mod table; pub mod button; -pub mod text_input; -pub mod number_input; -pub mod rule; pub mod checkbox; +pub mod container; +pub mod number_input; pub mod pick_list; +pub mod rule; pub mod scrollable; pub mod shortcut_selector; +pub mod table; +pub mod text; +pub mod text_input; pub type Element<'a, Message> = iced::Element<'a, Message, GauntletSettingsTheme>; @@ -48,7 +49,6 @@ pub const SUCCESS: ThemeColor = ThemeColor::new(0x659B5E, 1.0); pub const DANGER: ThemeColor = ThemeColor::new(0x6C1B1B, 1.0); pub const DANGER_BRIGHT: ThemeColor = ThemeColor::new(0xC20000, 1.0); - #[derive(Clone, Debug)] pub struct ThemeColor { hex: u32, diff --git a/rust/management_client/src/theme/button.rs b/rust/management_client/src/theme/button.rs index be1e62c..91f1ee9 100644 --- a/rust/management_client/src/theme/button.rs +++ b/rust/management_client/src/theme/button.rs @@ -1,8 +1,18 @@ use iced::widget::button; -use iced::widget::button::{Status, Style}; +use iced::widget::button::Status; +use iced::widget::button::Style; use iced::Border; -use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, BACKGROUND_LIGHTER, BUTTON_BORDER_RADIUS, DANGER, PRIMARY, PRIMARY_HOVERED, SUCCESS, TEXT_DARKEST, TEXT_LIGHTEST}; +use crate::theme::GauntletSettingsTheme; +use crate::theme::BACKGROUND_DARKER; +use crate::theme::BACKGROUND_LIGHTER; +use crate::theme::BUTTON_BORDER_RADIUS; +use crate::theme::DANGER; +use crate::theme::PRIMARY; +use crate::theme::PRIMARY_HOVERED; +use crate::theme::SUCCESS; +use crate::theme::TEXT_DARKEST; +use crate::theme::TEXT_LIGHTEST; pub enum ButtonStyle { Primary, @@ -26,12 +36,11 @@ impl button::Catalog for GauntletSettingsTheme { Status::Active => active(class), Status::Hovered => hovered(class), Status::Pressed => pressed(class), - Status::Disabled => disabled(class) + Status::Disabled => disabled(class), } } } - fn active(class: &ButtonStyle) -> Style { let (background_color, text_color) = match class { ButtonStyle::Primary => (PRIMARY.to_iced(), TEXT_DARKEST.to_iced()), @@ -100,7 +109,7 @@ fn hovered(class: &ButtonStyle) -> Style { background: None, text_color: TEXT_LIGHTEST.to_iced(), // TODO ..Default::default() - } + }; } ButtonStyle::ViewSwitcher => { return Style { @@ -161,9 +170,7 @@ fn pressed(class: &ButtonStyle) -> Style { ..Default::default() } } - _ => { - active(class) - } + _ => active(class), } } @@ -171,10 +178,8 @@ fn disabled(class: &ButtonStyle) -> Style { let style = active(class); Style { - background: style - .background - .map(|background| background.scale_alpha(0.5)), + background: style.background.map(|background| background.scale_alpha(0.5)), text_color: style.text_color.scale_alpha(0.5), ..style } -} \ No newline at end of file +} diff --git a/rust/management_client/src/theme/checkbox.rs b/rust/management_client/src/theme/checkbox.rs index 574122d..fe9128a 100644 --- a/rust/management_client/src/theme/checkbox.rs +++ b/rust/management_client/src/theme/checkbox.rs @@ -1,8 +1,15 @@ -use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, BACKGROUND_DARKEST, BACKGROUND_LIGHTER, PRIMARY, PRIMARY_HOVERED}; use iced::widget::checkbox; -use iced::widget::checkbox::{Status, Style}; +use iced::widget::checkbox::Status; +use iced::widget::checkbox::Style; use iced::Border; +use crate::theme::GauntletSettingsTheme; +use crate::theme::BACKGROUND_DARKER; +use crate::theme::BACKGROUND_DARKEST; +use crate::theme::BACKGROUND_LIGHTER; +use crate::theme::PRIMARY; +use crate::theme::PRIMARY_HOVERED; + impl checkbox::Catalog for GauntletSettingsTheme { type Class<'a> = (); @@ -70,4 +77,4 @@ fn disabled(is_checked: bool) -> Style { border: Default::default(), text_color: None, } -} \ No newline at end of file +} diff --git a/rust/management_client/src/theme/container.rs b/rust/management_client/src/theme/container.rs index 14a240d..9b8ac84 100644 --- a/rust/management_client/src/theme/container.rs +++ b/rust/management_client/src/theme/container.rs @@ -1,13 +1,20 @@ -use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, BACKGROUND_LIGHTER, BACKGROUND_LIGHTEST, DANGER, TRANSPARENT}; use iced::widget::container; use iced::widget::container::Style; -use iced::{Border, Color}; +use iced::Border; +use iced::Color; + +use crate::theme::GauntletSettingsTheme; +use crate::theme::BACKGROUND_DARKER; +use crate::theme::BACKGROUND_LIGHTER; +use crate::theme::BACKGROUND_LIGHTEST; +use crate::theme::DANGER; +use crate::theme::TRANSPARENT; pub enum ContainerStyle { Transparent, Box, TextInputLike, - TextInputMissingValue + TextInputMissingValue, } impl container::Catalog for GauntletSettingsTheme { @@ -57,4 +64,4 @@ impl container::Catalog for GauntletSettingsTheme { } } } -} \ No newline at end of file +} diff --git a/rust/management_client/src/theme/number_input.rs b/rust/management_client/src/theme/number_input.rs index e696e2b..dce1110 100644 --- a/rust/management_client/src/theme/number_input.rs +++ b/rust/management_client/src/theme/number_input.rs @@ -1,6 +1,12 @@ +use iced_aw::number_input::number_input; +use iced_aw::number_input::Style; use iced_aw::style::Status; -use iced_aw::number_input::{number_input, Style}; -use crate::theme::{GauntletSettingsTheme, PRIMARY, PRIMARY_HOVERED, TEXT_DARKER, TEXT_LIGHTEST}; + +use crate::theme::GauntletSettingsTheme; +use crate::theme::PRIMARY; +use crate::theme::PRIMARY_HOVERED; +use crate::theme::TEXT_DARKER; +use crate::theme::TEXT_LIGHTEST; impl number_input::ExtendedCatalog for GauntletSettingsTheme { fn style(&self, class: &(), status: Status) -> Style { @@ -21,7 +27,7 @@ impl number_input::Catalog for GauntletSettingsTheme { Status::Hovered => active(), // TODO proper style Status::Pressed => pressed(), Status::Disabled => disabled(), - Status::Focused => active(), // TODO proper style + Status::Focused => active(), // TODO proper style Status::Selected => pressed(), // TODO proper style } } @@ -46,4 +52,4 @@ fn disabled() -> Style { button_background: None, icon_color: TEXT_LIGHTEST.to_iced(), } -} \ No newline at end of file +} diff --git a/rust/management_client/src/theme/pick_list.rs b/rust/management_client/src/theme/pick_list.rs index 24ccbda..49a6e56 100644 --- a/rust/management_client/src/theme/pick_list.rs +++ b/rust/management_client/src/theme/pick_list.rs @@ -1,6 +1,15 @@ -use iced::{Border, overlay}; +use iced::overlay; use iced::widget::pick_list; -use crate::theme::{BUTTON_BORDER_RADIUS, GauntletSettingsTheme, PRIMARY, PRIMARY_HOVERED, TEXT_DARKEST, BACKGROUND_DARKER, BACKGROUND_DARKEST, TEXT_LIGHTEST}; +use iced::Border; + +use crate::theme::GauntletSettingsTheme; +use crate::theme::BACKGROUND_DARKER; +use crate::theme::BACKGROUND_DARKEST; +use crate::theme::BUTTON_BORDER_RADIUS; +use crate::theme::PRIMARY; +use crate::theme::PRIMARY_HOVERED; +use crate::theme::TEXT_DARKEST; +use crate::theme::TEXT_LIGHTEST; impl pick_list::Catalog for GauntletSettingsTheme { type Class<'a> = (); diff --git a/rust/management_client/src/theme/rule.rs b/rust/management_client/src/theme/rule.rs index 5f17689..caa3215 100644 --- a/rust/management_client/src/theme/rule.rs +++ b/rust/management_client/src/theme/rule.rs @@ -1,7 +1,8 @@ -use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER}; use iced::widget::rule; use iced::widget::rule::Style; +use crate::theme::GauntletSettingsTheme; +use crate::theme::BACKGROUND_DARKER; impl rule::Catalog for GauntletSettingsTheme { type Class<'a> = (); diff --git a/rust/management_client/src/theme/scrollable.rs b/rust/management_client/src/theme/scrollable.rs index ac79030..fd46c59 100644 --- a/rust/management_client/src/theme/scrollable.rs +++ b/rust/management_client/src/theme/scrollable.rs @@ -1,8 +1,13 @@ -use iced::widget::scrollable::{Status, Style}; -use iced::widget::{container, scrollable}; -use iced::{border, Border, Color}; +use iced::border; +use iced::widget::container; +use iced::widget::scrollable; +use iced::widget::scrollable::Status; +use iced::widget::scrollable::Style; +use iced::Border; +use iced::Color; -use crate::theme::{GauntletSettingsTheme, PRIMARY}; +use crate::theme::GauntletSettingsTheme; +use crate::theme::PRIMARY; impl scrollable::Catalog for GauntletSettingsTheme { type Class<'a> = (); @@ -22,12 +27,14 @@ impl scrollable::Catalog for GauntletSettingsTheme { }; match status { - Status::Active => Style { - container: container::Style::default(), - vertical_rail: scrollbar, - horizontal_rail: scrollbar, - gap: None, - }, + Status::Active => { + Style { + container: container::Style::default(), + vertical_rail: scrollbar, + horizontal_rail: scrollbar, + gap: None, + } + } Status::Hovered { is_horizontal_scrollbar_hovered, is_vertical_scrollbar_hovered, @@ -84,4 +91,4 @@ impl scrollable::Catalog for GauntletSettingsTheme { } } } -} \ No newline at end of file +} diff --git a/rust/management_client/src/theme/shortcut_selector.rs b/rust/management_client/src/theme/shortcut_selector.rs index d7eb99d..a5a6adc 100644 --- a/rust/management_client/src/theme/shortcut_selector.rs +++ b/rust/management_client/src/theme/shortcut_selector.rs @@ -1,9 +1,13 @@ -use crate::components::shortcut_selector; -use crate::components::shortcut_selector::Status; -use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, BUTTON_BORDER_RADIUS, PRIMARY}; use iced::widget::container::Style; use iced::Border; +use crate::components::shortcut_selector; +use crate::components::shortcut_selector::Status; +use crate::theme::GauntletSettingsTheme; +use crate::theme::BACKGROUND_DARKER; +use crate::theme::BUTTON_BORDER_RADIUS; +use crate::theme::PRIMARY; + impl shortcut_selector::Catalog for GauntletSettingsTheme { type Class<'a> = (); diff --git a/rust/management_client/src/theme/table.rs b/rust/management_client/src/theme/table.rs index bb44927..69b625a 100644 --- a/rust/management_client/src/theme/table.rs +++ b/rust/management_client/src/theme/table.rs @@ -1,8 +1,9 @@ use iced::widget::container; use iced::Border; -use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, TEXT_LIGHTEST}; - +use crate::theme::GauntletSettingsTheme; +use crate::theme::BACKGROUND_DARKER; +use crate::theme::TEXT_LIGHTEST; impl iced_table::Catalog for GauntletSettingsTheme { type Style = (); @@ -50,8 +51,6 @@ impl iced_table::Catalog for GauntletSettingsTheme { } fn divider(&self, _: &Self::Style, _hovered: bool) -> container::Style { - container::Style { - ..Default::default() - } + container::Style { ..Default::default() } } } diff --git a/rust/management_client/src/theme/text.rs b/rust/management_client/src/theme/text.rs index c924ec4..e7e6cea 100644 --- a/rust/management_client/src/theme/text.rs +++ b/rust/management_client/src/theme/text.rs @@ -1,6 +1,10 @@ use iced::widget::text; use iced::widget::text::Style; -use crate::theme::{DANGER_BRIGHT, GauntletSettingsTheme, SUCCESS, TEXT_DARKER}; + +use crate::theme::GauntletSettingsTheme; +use crate::theme::DANGER_BRIGHT; +use crate::theme::SUCCESS; +use crate::theme::TEXT_DARKER; pub enum TextStyle { Default, @@ -18,11 +22,7 @@ impl text::Catalog for GauntletSettingsTheme { fn style(&self, class: &Self::Class<'_>) -> Style { match class { - TextStyle::Default => { - Style { - color: None, - } - } + TextStyle::Default => Style { color: None }, TextStyle::Subtitle => { Style { color: Some(TEXT_DARKER.to_iced()), diff --git a/rust/management_client/src/theme/text_input.rs b/rust/management_client/src/theme/text_input.rs index 903cb4a..e2353ef 100644 --- a/rust/management_client/src/theme/text_input.rs +++ b/rust/management_client/src/theme/text_input.rs @@ -1,10 +1,17 @@ -use crate::theme::{GauntletSettingsTheme, BACKGROUND_DARKER, TEXT_DARKER, TEXT_LIGHTEST, TRANSPARENT}; use iced::widget::text_input; -use iced::widget::text_input::{Status, Style}; -use iced::{Background, Border}; +use iced::widget::text_input::Status; +use iced::widget::text_input::Style; +use iced::Background; +use iced::Border; + +use crate::theme::GauntletSettingsTheme; +use crate::theme::BACKGROUND_DARKER; +use crate::theme::TEXT_DARKER; +use crate::theme::TEXT_LIGHTEST; +use crate::theme::TRANSPARENT; pub enum TextInputStyle { - FormInput + FormInput, } impl text_input::Catalog for GauntletSettingsTheme { @@ -30,19 +37,25 @@ impl text_input::Catalog for GauntletSettingsTheme { match status { Status::Active => active, - Status::Hovered => Style { - background: Background::Color(BACKGROUND_DARKER.to_iced().into()), - ..active - }, - Status::Focused => Style { - background: Background::Color(BACKGROUND_DARKER.to_iced().into()), - ..active - }, - Status::Disabled => Style { - background: Background::Color(BACKGROUND_DARKER.to_iced().into()), - value: active.placeholder, - ..active - }, + Status::Hovered => { + Style { + background: Background::Color(BACKGROUND_DARKER.to_iced().into()), + ..active + } + } + Status::Focused => { + Style { + background: Background::Color(BACKGROUND_DARKER.to_iced().into()), + ..active + } + } + Status::Disabled => { + Style { + background: Background::Color(BACKGROUND_DARKER.to_iced().into()), + value: active.placeholder, + ..active + } + } } } } diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index 7a48918..7d6fdcf 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -1,33 +1,71 @@ use std::collections::HashMap; use std::time::Duration; -use iced::{Alignment, alignment, font, futures, Length, Padding, Size, Subscription, time, window, Task, Renderer, padding}; +use gauntlet_common::model::DownloadStatus; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::SettingsTheme; +use gauntlet_common::model::WindowPositionMode; +use gauntlet_common::rpc::backend_api::BackendApi; +use gauntlet_common::rpc::backend_api::BackendApiError; +use gauntlet_common_ui::padding; use iced::advanced::text::Shaping; -use iced::widget::{button, column, container, horizontal_rule, horizontal_space, mouse_area, row, scrollable, stack, text, value}; +use iced::alignment; +use iced::font; +use iced::futures; +use iced::padding; +use iced::time; +use iced::widget::button; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_rule; +use iced::widget::horizontal_space; +use iced::widget::mouse_area; +use iced::widget::row; +use iced::widget::scrollable; +use iced::widget::stack; +use iced::widget::text; +use iced::widget::value; +use iced::window; +use iced::Alignment; +use iced::Length; +use iced::Padding; +use iced::Renderer; +use iced::Size; +use iced::Subscription; +use iced::Task; use iced_aw::Spinner; -use iced_fonts::{Bootstrap, BOOTSTRAP_FONT, BOOTSTRAP_FONT_BYTES}; +use iced_fonts::Bootstrap; +use iced_fonts::BOOTSTRAP_FONT; +use iced_fonts::BOOTSTRAP_FONT_BYTES; use itertools::Itertools; -use gauntlet_common::model::{DownloadStatus, PhysicalShortcut, PluginId, SettingsTheme, WindowPositionMode}; -use gauntlet_common::rpc::backend_api::{BackendApi, BackendApiError}; -use gauntlet_common_ui::padding; -use crate::theme::{Element, GauntletSettingsTheme}; use crate::theme::button::ButtonStyle; use crate::theme::container::ContainerStyle; use crate::theme::text::TextStyle; -use crate::views::general::{ManagementAppGeneralMsgIn, ManagementAppGeneralMsgOut, ManagementAppGeneralState}; -use crate::views::plugins::{ManagementAppPluginMsgIn, ManagementAppPluginMsgOut, ManagementAppPluginsState}; +use crate::theme::Element; +use crate::theme::GauntletSettingsTheme; +use crate::views::general::ManagementAppGeneralMsgIn; +use crate::views::general::ManagementAppGeneralMsgOut; +use crate::views::general::ManagementAppGeneralState; +use crate::views::plugins::ManagementAppPluginMsgIn; +use crate::views::plugins::ManagementAppPluginMsgOut; +use crate::views::plugins::ManagementAppPluginsState; pub fn run() { - iced::application::("Gauntlet Settings", update, view) - .window(window::Settings { - size: Size::new(1000.0, 600.0), - ..Default::default() - }) - .subscription(subscription) - .theme(|_| GauntletSettingsTheme::default()) - .run_with(new) - .expect("Unable to start settings application"); + iced::application::( + "Gauntlet Settings", + update, + view, + ) + .window(window::Settings { + size: Size::new(1000.0, 600.0), + ..Default::default() + }) + .subscription(subscription) + .theme(|_| GauntletSettingsTheme::default()) + .run_with(new) + .expect("Unable to start settings application"); } struct ManagementAppModel { @@ -37,10 +75,9 @@ struct ManagementAppModel { download_info_shown: bool, current_settings_view: SettingsView, general_state: ManagementAppGeneralState, - plugins_state: ManagementAppPluginsState + plugins_state: ManagementAppPluginsState, } - #[derive(Debug, Clone)] enum ManagementAppMsg { FontLoaded(Result<(), font::Error>), @@ -58,30 +95,24 @@ enum ManagementAppMsg { #[derive(Debug, Clone, PartialEq, Eq)] enum SettingsView { General, - Plugins + Plugins, } #[derive(Debug, Clone, PartialEq, Eq)] enum ErrorView { - UnknownError { - display: String - }, + UnknownError { display: String }, Timeout, } #[derive(PartialOrd, Ord, PartialEq, Eq, Clone)] // ordering used in sorting items in ui pub enum DownloadInfo { InProgress, - Error { - message: String - }, + Error { message: String }, Successful, } fn new() -> (ManagementAppModel, Task) { - let backend_api = futures::executor::block_on(async { - anyhow::Ok(BackendApi::new().await?) - }) + let backend_api = futures::executor::block_on(async { anyhow::Ok(BackendApi::new().await?) }) .inspect_err(|err| tracing::error!("Unable to connect to server: {:?}", err)) .ok(); @@ -102,7 +133,7 @@ fn new() -> (ManagementAppModel, Task) { async { match backend_api { Some(backend_api) => Some(init_data(backend_api).await), - None => None + None => None, } }, |shortcut| { @@ -115,14 +146,14 @@ fn new() -> (ManagementAppModel, Task) { theme: init.theme, window_position_mode: init.window_position_mode, shortcut: init.global_shortcut, - shortcut_error: init.global_shortcut_error + shortcut_error: init.global_shortcut_error, }) - }, - Err(err) => ManagementAppMsg::HandleBackendError(err) + } + Err(err) => ManagementAppMsg::HandleBackendError(err), } } } - } + }, ), ]), ) @@ -132,73 +163,64 @@ struct InitSettingsData { global_shortcut: Option, global_shortcut_error: Option, theme: SettingsTheme, - window_position_mode: WindowPositionMode + window_position_mode: WindowPositionMode, } async fn init_data(mut backend_api: BackendApi) -> Result { - let (global_shortcut, global_shortcut_error) = backend_api.get_global_shortcut() - .await?; + let (global_shortcut, global_shortcut_error) = backend_api.get_global_shortcut().await?; - let theme = backend_api.get_theme() - .await?; + let theme = backend_api.get_theme().await?; - let window_position_mode = backend_api.get_window_position_mode() - .await?; + let window_position_mode = backend_api.get_window_position_mode().await?; Ok(InitSettingsData { global_shortcut, global_shortcut_error, theme, - window_position_mode + window_position_mode, }) } fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task { let backend_api = match &state.backend_api { Some(backend_api) => backend_api.clone(), - None => { - return Task::none() - } + None => return Task::none(), }; match message { ManagementAppMsg::Plugin(message) => { - state.plugins_state.update(message) - .map(|msg| { - match msg { - ManagementAppPluginMsgOut::PluginsReloaded(plugins) => { - ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::PluginsFetched(plugins)) - } - ManagementAppPluginMsgOut::Noop => { - ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::Noop) - } - ManagementAppPluginMsgOut::DownloadPlugin { plugin_id } => { - ManagementAppMsg::DownloadPlugin { plugin_id } - } - ManagementAppPluginMsgOut::SelectedItem(selected_item) => { - ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::SelectItem(selected_item)) - } - ManagementAppPluginMsgOut::HandleBackendError(err) => { - ManagementAppMsg::HandleBackendError(err) - } + state.plugins_state.update(message).map(|msg| { + match msg { + ManagementAppPluginMsgOut::PluginsReloaded(plugins) => { + ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::PluginsFetched(plugins)) } - }) + ManagementAppPluginMsgOut::Noop => ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::Noop), + ManagementAppPluginMsgOut::DownloadPlugin { plugin_id } => { + ManagementAppMsg::DownloadPlugin { plugin_id } + } + ManagementAppPluginMsgOut::SelectedItem(selected_item) => { + ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::SelectItem(selected_item)) + } + ManagementAppPluginMsgOut::HandleBackendError(err) => ManagementAppMsg::HandleBackendError(err), + } + }) } ManagementAppMsg::General(message) => { - state.general_state.update(message) - .map(|msg| { - match msg { - ManagementAppGeneralMsgOut::Noop => { - ManagementAppMsg::General(ManagementAppGeneralMsgIn::Noop) - }, - ManagementAppGeneralMsgOut::HandleBackendError(err) => { - ManagementAppMsg::HandleBackendError(err) - } - ManagementAppGeneralMsgOut::SetGlobalShortcutResponse { shortcut, shortcut_error } => { - ManagementAppMsg::General(ManagementAppGeneralMsgIn::SetGlobalShortcutResponse { shortcut, shortcut_error }) - } + state.general_state.update(message).map(|msg| { + match msg { + ManagementAppGeneralMsgOut::Noop => ManagementAppMsg::General(ManagementAppGeneralMsgIn::Noop), + ManagementAppGeneralMsgOut::HandleBackendError(err) => ManagementAppMsg::HandleBackendError(err), + ManagementAppGeneralMsgOut::SetGlobalShortcutResponse { + shortcut, + shortcut_error, + } => { + ManagementAppMsg::General(ManagementAppGeneralMsgIn::SetGlobalShortcutResponse { + shortcut, + shortcut_error, + }) } - }) + } + }) } ManagementAppMsg::FontLoaded(result) => { result.expect("unable to load font"); @@ -212,7 +234,7 @@ fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task { state.error_view = Some(match err { BackendApiError::Timeout => ErrorView::Timeout, - BackendApiError::Internal { display } => ErrorView::UnknownError { display } + BackendApiError::Internal { display } => ErrorView::UnknownError { display }, }); Task::none() @@ -227,7 +249,9 @@ fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task { - state.downloads_info.insert(plugin.clone(), DownloadInfo::Error { message }); + state + .downloads_info + .insert(plugin.clone(), DownloadInfo::Error { message }); } } } @@ -236,12 +260,15 @@ fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task { @@ -252,8 +279,7 @@ fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task Task { let mut backend_client = backend_api.clone(); - let already_downloading = state.downloads_info.insert(plugin_id.clone(), DownloadInfo::InProgress) + let already_downloading = state + .downloads_info + .insert(plugin_id.clone(), DownloadInfo::InProgress) .is_some(); if already_downloading { @@ -272,12 +300,11 @@ fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task Task Element<'_, ManagementAppMsg> { if let None = &state.backend_api { - let description: Element<_> = text("Unable to connect to server. Please check if you have Gauntlet running on your PC") - .into(); + let description: Element<_> = + text("Unable to connect to server. Please check if you have Gauntlet running on your PC").into(); let content: Element<_> = container(description) .align_x(Alignment::Center) @@ -301,14 +328,13 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .height(Length::Fill) .into(); - return content + return content; } if let Some(err) = &state.error_view { return match err { ErrorView::Timeout => { - let description: Element<_> = text("Error occurred") - .into(); + let description: Element<_> = text("Error occurred").into(); let description = container(description) .width(Length::Fill) @@ -316,8 +342,8 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .padding(12) .into(); - let sub_description: Element<_> = text("Backend was unable to process message in a timely manner") - .into(); + let sub_description: Element<_> = + text("Backend was unable to process message in a timely manner").into(); let sub_description = container(sub_description) .width(Length::Fill) @@ -325,10 +351,7 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .padding(12) .into(); - let content: Element<_> = column([ - description, - sub_description, - ]).into(); + let content: Element<_> = column([description, sub_description]).into(); let content: Element<_> = container(content) .align_x(Alignment::Center) @@ -340,8 +363,7 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { content } ErrorView::UnknownError { display } => { - let description: Element<_> = text("Unknown error occurred") - .into(); + let description: Element<_> = text("Unknown error occurred").into(); let description = container(description) .width(Length::Fill) @@ -349,8 +371,7 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .padding(12) .into(); - let sub_description: Element<_> = text("Please report") - .into(); + let sub_description: Element<_> = text("Please report").into(); let sub_description = container(sub_description) .width(Length::Fill) @@ -358,9 +379,7 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .padding(12) .into(); - let error_description: Element<_> = text(display) - .shaping(Shaping::Advanced) - .into(); + let error_description: Element<_> = text(display).shaping(Shaping::Advanced).into(); let error_description = container(error_description) .width(Length::Fill) @@ -368,11 +387,7 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .padding(12) .into(); - let content: Element<_> = column([ - description, - sub_description, - error_description, - ]).into(); + let content: Element<_> = column([description, sub_description, error_description]).into(); let content: Element<_> = container(content) .align_x(Alignment::Center) @@ -383,19 +398,12 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { content } - } + }; } - let content = match state.current_settings_view { - SettingsView::General => { - state.general_state.view() - .map(|msg| ManagementAppMsg::General(msg)) - } - SettingsView::Plugins => { - state.plugins_state.view() - .map(|msg| ManagementAppMsg::Plugin(msg)) - } + SettingsView::General => state.general_state.view().map(|msg| ManagementAppMsg::General(msg)), + SettingsView::Plugins => state.plugins_state.view().map(|msg| ManagementAppMsg::Plugin(msg)), }; let icon_general: Element<_> = value(Bootstrap::GearFill) @@ -422,12 +430,16 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .on_press(ManagementAppMsg::SwitchView(SettingsView::General)) .height(Length::Fill) .width(80) - .class(if state.current_settings_view == SettingsView::General { ButtonStyle::ViewSwitcherSelected } else { ButtonStyle::ViewSwitcher }) + .class( + if state.current_settings_view == SettingsView::General { + ButtonStyle::ViewSwitcherSelected + } else { + ButtonStyle::ViewSwitcher + }, + ) .into(); - let general_button: Element<_> = container(general_button) - .padding(8.0) - .into(); + let general_button: Element<_> = container(general_button).padding(8.0).into(); let icon_plugins: Element<_> = value(Bootstrap::PuzzleFill) .font(BOOTSTRAP_FONT) @@ -453,24 +465,25 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .on_press(ManagementAppMsg::SwitchView(SettingsView::Plugins)) .height(Length::Fill) .width(80) - .class(if state.current_settings_view == SettingsView::Plugins { ButtonStyle::ViewSwitcherSelected } else { ButtonStyle::ViewSwitcher }) + .class( + if state.current_settings_view == SettingsView::Plugins { + ButtonStyle::ViewSwitcherSelected + } else { + ButtonStyle::ViewSwitcher + }, + ) .into(); - let plugins_button: Element<_> = container(plugins_button) - .padding(8.0) - .into(); + let plugins_button: Element<_> = container(plugins_button).padding(8.0).into(); - let top_bar_buttons: Element<_> = row(vec![general_button, plugins_button]) - .into(); + let top_bar_buttons: Element<_> = row(vec![general_button, plugins_button]).into(); let top_bar_buttons: Element<_> = container(top_bar_buttons) .width(Length::Fill) .align_x(alignment::Horizontal::Center) .into(); - let top_bar_left_space: Element<_> = horizontal_space() - .width(Length::Fill) - .into(); + let top_bar_left_space: Element<_> = horizontal_space().width(Length::Fill).into(); let top_bar_right = { let mut successful_count = 0; @@ -494,23 +507,16 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { let mut download_info_icons = vec![]; if in_progress_count > 0 { - let spinner: Element<_> = Spinner::new() - .width(Length::Fixed(16.0)) - .height(Length::Fill) - .into(); + let spinner: Element<_> = Spinner::new().width(Length::Fixed(16.0)).height(Length::Fill).into(); - let spinner: Element<_> = container(spinner) - .height(Length::Fill) - .into(); + let spinner: Element<_> = container(spinner).height(Length::Fill).into(); let text: Element<_> = text(in_progress_count) .height(Length::Fill) .align_y(alignment::Vertical::Center) .into(); - let spinner: Element<_> = row(vec![text, spinner]) - .spacing(8.0) - .into(); + let spinner: Element<_> = row(vec![text, spinner]).spacing(8.0).into(); download_info_icons.push(spinner); } @@ -523,18 +529,14 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .class(TextStyle::Positive) .into(); - let icon: Element<_> = container(icon) - .height(Length::Fill) - .into(); + let icon: Element<_> = container(icon).height(Length::Fill).into(); let text: Element<_> = text(successful_count) .height(Length::Fill) .align_y(alignment::Vertical::Center) .into(); - let icon: Element<_> = row(vec![text, icon]) - .spacing(8.0) - .into(); + let icon: Element<_> = row(vec![text, icon]).spacing(8.0).into(); download_info_icons.push(icon); } @@ -547,26 +549,20 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .class(TextStyle::Destructive) .into(); - let icon: Element<_> = container(icon) - .height(Length::Fill) - .into(); + let icon: Element<_> = container(icon).height(Length::Fill).into(); let text: Element<_> = text(error_count) .height(Length::Fill) .align_y(alignment::Vertical::Center) .into(); - let icon: Element<_> = row(vec![text, icon]) - .spacing(8.0) - .into(); + let icon: Element<_> = row(vec![text, icon]).spacing(8.0).into(); download_info_icons.push(icon); } if download_info_icons.is_empty() { - horizontal_space() - .width(Length::Fill) - .into() + horizontal_space().width(Length::Fill).into() } else { let top_bar_right: Element<_> = row(download_info_icons) .spacing(12.0) @@ -609,24 +605,21 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .max_height(70) .into(); - let separator: Element<_> = horizontal_rule(1) - .into(); + let separator: Element<_> = horizontal_rule(1).into(); - let content: Element<_> = column(vec![top_bar, separator, content]) - .into(); + let content: Element<_> = column(vec![top_bar, separator, content]).into(); let download_info_panel: Element<_> = { - let downloads: Vec> = state.downloads_info.iter() + let downloads: Vec> = state + .downloads_info + .iter() .sorted_by_key(|(_, info)| info.clone()) .map(|(plugin_id, info)| { match info { DownloadInfo::InProgress => { - let kind_text: Element<_> = text("Download in progress") - .into(); + let kind_text: Element<_> = text("Download in progress").into(); - let kind_text: Element<_> = container(kind_text) - .padding(padding(16, 0, 8, 0)) - .into(); + let kind_text: Element<_> = container(kind_text).padding(padding(16, 0, 8, 0)).into(); let plugin_id: Element<_> = text(plugin_id.to_string()) .shaping(Shaping::Advanced) @@ -634,35 +627,22 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .size(14) .into(); - let plugin_id: Element<_> = container(plugin_id) - .padding(padding::bottom(16)) - .into(); + let plugin_id: Element<_> = container(plugin_id).padding(padding::bottom(16)).into(); - let spinner: Element<_> = Spinner::new() - .width(Length::Fixed(32.0)) - .into(); + let spinner: Element<_> = Spinner::new().width(Length::Fixed(32.0)).into(); - let spinner: Element<_> = container(spinner) - .padding(16) - .into(); + let spinner: Element<_> = container(spinner).padding(16).into(); - let content: Element<_> = column(vec![kind_text, plugin_id]) - .into(); + let content: Element<_> = column(vec![kind_text, plugin_id]).into(); - let content: Element<_> = row(vec![spinner, content]) - .into(); + let content: Element<_> = row(vec![spinner, content]).into(); - container(content) - .width(Length::Fill) - .into() + container(content).width(Length::Fill).into() } DownloadInfo::Error { message } => { - let kind_text: Element<_> = text("Download failed") - .into(); + let kind_text: Element<_> = text("Download failed").into(); - let kind_text: Element<_> = container(kind_text) - .padding(padding(16, 0, 8, 0)) - .into(); + let kind_text: Element<_> = container(kind_text).padding(padding(16, 0, 8, 0)).into(); let plugin_id: Element<_> = text(plugin_id.to_string()) .shaping(Shaping::Advanced) @@ -677,35 +657,22 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .class(TextStyle::Destructive) .into(); - let icon: Element<_> = container(icon) - .padding(16) - .into(); + let icon: Element<_> = container(icon).padding(16).into(); - let message: Element<_> = text(message.to_string()) - .shaping(Shaping::Advanced) - .into(); + let message: Element<_> = text(message.to_string()).shaping(Shaping::Advanced).into(); - let message: Element<_> = container(message) - .padding(padding(8, 0, 16, 0)) - .into(); + let message: Element<_> = container(message).padding(padding(8, 0, 16, 0)).into(); - let content: Element<_> = column(vec![kind_text, plugin_id, message]) - .into(); + let content: Element<_> = column(vec![kind_text, plugin_id, message]).into(); - let content: Element<_> = row(vec![icon, content]) - .into(); + let content: Element<_> = row(vec![icon, content]).into(); - container(content) - .width(Length::Fill) - .into() + container(content).width(Length::Fill).into() } DownloadInfo::Successful => { - let kind_text: Element<_> = text("Download successful") - .into(); + let kind_text: Element<_> = text("Download successful").into(); - let kind_text: Element<_> = container(kind_text) - .padding(padding(16, 0, 8, 0)) - .into(); + let kind_text: Element<_> = container(kind_text).padding(padding(16, 0, 8, 0)).into(); let plugin_id: Element<_> = text(plugin_id.to_string()) .shaping(Shaping::Advanced) @@ -713,9 +680,7 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .class(TextStyle::Subtitle) .into(); - let plugin_id: Element<_> = container(plugin_id) - .padding(padding::bottom(16)) - .into(); + let plugin_id: Element<_> = container(plugin_id).padding(padding::bottom(16)).into(); let icon: Element<_> = value(Bootstrap::PatchCheckFill) .size(32) @@ -724,31 +689,22 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { .class(TextStyle::Positive) .into(); - let icon: Element<_> = container(icon) - .padding(16) - .into(); + let icon: Element<_> = container(icon).padding(16).into(); - let content: Element<_> = column(vec![kind_text, plugin_id]) - .into(); + let content: Element<_> = column(vec![kind_text, plugin_id]).into(); - let content: Element<_> = row(vec![icon, content]) - .into(); + let content: Element<_> = row(vec![icon, content]).into(); - container(content) - .width(Length::Fill) - .into() + container(content).width(Length::Fill).into() } } }) .intersperse_with(|| horizontal_rule(1).into()) .collect(); - let downloads: Element<_> = column(downloads) - .into(); + let downloads: Element<_> = column(downloads).into(); - let downloads: Element<_> = scrollable(downloads) - .width(Length::Fill) - .into(); + let downloads: Element<_> = scrollable(downloads).width(Length::Fill).into(); let content: Element<_> = container(downloads) .padding(4) @@ -765,7 +721,13 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { }; let content: Element<_> = mouse_area(content) - .on_press(if state.download_info_shown { ManagementAppMsg::ToggleDownloadInfo } else { ManagementAppMsg::Noop }) + .on_press( + if state.download_info_shown { + ManagementAppMsg::ToggleDownloadInfo + } else { + ManagementAppMsg::Noop + }, + ) .into(); let mut content = vec![content]; @@ -774,19 +736,19 @@ fn view(state: &ManagementAppModel) -> Element<'_, ManagementAppMsg> { content.push(download_info_panel); } - stack(content) - .into() + stack(content).into() } fn subscription(_state: &ManagementAppModel) -> Subscription { - time::every(Duration::from_millis(300)) - .map(|_| ManagementAppMsg::CheckDownloadStatus) + time::every(Duration::from_millis(300)).map(|_| ManagementAppMsg::CheckDownloadStatus) } - -pub fn handle_backend_error(result: Result, convert: impl FnOnce(T) -> ManagementAppMsg) -> ManagementAppMsg { +pub fn handle_backend_error( + result: Result, + convert: impl FnOnce(T) -> ManagementAppMsg, +) -> ManagementAppMsg { match result { Ok(val) => convert(val), - Err(err) => ManagementAppMsg::HandleBackendError(err) + Err(err) => ManagementAppMsg::HandleBackendError(err), } } diff --git a/rust/management_client/src/views/general.rs b/rust/management_client/src/views/general.rs index 0741c6a..7f8520f 100644 --- a/rust/management_client/src/views/general.rs +++ b/rust/management_client/src/views/general.rs @@ -1,15 +1,31 @@ +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::SettingsTheme; +use gauntlet_common::model::WindowPositionMode; +use gauntlet_common::rpc::backend_api::BackendApi; +use gauntlet_common::rpc::backend_api::BackendApiError; +use iced::alignment; +use iced::alignment::Horizontal; +use iced::widget::column; +use iced::widget::container; +use iced::widget::pick_list; +use iced::widget::row; +use iced::widget::text; +use iced::widget::text::Shaping; +use iced::widget::tooltip; +use iced::widget::tooltip::Position; +use iced::widget::value; +use iced::widget::Space; +use iced::Alignment; +use iced::Length; +use iced::Padding; +use iced::Task; +use iced_fonts::Bootstrap; +use iced_fonts::BOOTSTRAP_FONT; + use crate::components::shortcut_selector::ShortcutSelector; +use crate::theme::container::ContainerStyle; use crate::theme::text::TextStyle; use crate::theme::Element; -use gauntlet_common::model::{PhysicalShortcut, SettingsTheme, WindowPositionMode}; -use gauntlet_common::rpc::backend_api::{BackendApi, BackendApiError}; -use iced::alignment::Horizontal; -use iced::widget::text::Shaping; -use iced::widget::tooltip::Position; -use iced::widget::{column, container, pick_list, row, text, tooltip, value, Space}; -use iced::{alignment, Alignment, Length, Padding, Task}; -use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; -use crate::theme::container::ContainerStyle; pub struct ManagementAppGeneralState { backend_api: Option, @@ -17,7 +33,7 @@ pub struct ManagementAppGeneralState { window_position_mode: WindowPositionMode, current_shortcut: Option, current_shortcut_error: Option, - currently_capturing: bool + currently_capturing: bool, } #[derive(Debug, Clone)] @@ -28,15 +44,15 @@ pub enum ManagementAppGeneralMsgIn { WindowPositionModeChanged(WindowPositionMode), SetGlobalShortcutResponse { shortcut: Option, - shortcut_error: Option + shortcut_error: Option, }, InitSetting { theme: SettingsTheme, window_position_mode: WindowPositionMode, shortcut: Option, - shortcut_error: Option + shortcut_error: Option, }, - Noop + Noop, } #[derive(Debug, Clone)] @@ -44,9 +60,9 @@ pub enum ManagementAppGeneralMsgOut { Noop, SetGlobalShortcutResponse { shortcut: Option, - shortcut_error: Option + shortcut_error: Option, }, - HandleBackendError(BackendApiError) + HandleBackendError(BackendApiError), } impl ManagementAppGeneralState { @@ -64,9 +80,7 @@ impl ManagementAppGeneralState { pub fn update(&mut self, message: ManagementAppGeneralMsgIn) -> Task { let backend_api = match &self.backend_api { Some(backend_api) => backend_api.clone(), - None => { - return Task::none() - } + None => return Task::none(), }; match message { @@ -78,8 +92,7 @@ impl ManagementAppGeneralState { let shortcut = shortcut.clone(); async move { - let error = backend_api.set_global_shortcut(shortcut) - .await?; + let error = backend_api.set_global_shortcut(shortcut).await?; Ok(error) } @@ -87,17 +100,22 @@ impl ManagementAppGeneralState { move |result| { let shortcut = shortcut.clone(); - handle_backend_error(result, move |shortcut_error| ManagementAppGeneralMsgOut::SetGlobalShortcutResponse { - shortcut, - shortcut_error, + handle_backend_error(result, move |shortcut_error| { + ManagementAppGeneralMsgOut::SetGlobalShortcutResponse { + shortcut, + shortcut_error, + } }) }, ) } - ManagementAppGeneralMsgIn::Noop => { - Task::none() - } - ManagementAppGeneralMsgIn::InitSetting { theme, window_position_mode, shortcut, shortcut_error } => { + ManagementAppGeneralMsgIn::Noop => Task::none(), + ManagementAppGeneralMsgIn::InitSetting { + theme, + window_position_mode, + shortcut, + shortcut_error, + } => { self.theme = theme; self.window_position_mode = window_position_mode; self.current_shortcut = shortcut; @@ -115,27 +133,33 @@ impl ManagementAppGeneralState { let mut backend_api = backend_api.clone(); - Task::perform(async move { - backend_api.set_theme(theme) - .await?; - - Ok(()) - }, |result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop)) + Task::perform( + async move { + backend_api.set_theme(theme).await?; + Ok(()) + }, + |result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop), + ) } ManagementAppGeneralMsgIn::WindowPositionModeChanged(mode) => { self.window_position_mode = mode.clone(); let mut backend_api = backend_api.clone(); - Task::perform(async move { - backend_api.set_window_position_mode(mode) - .await?; + Task::perform( + async move { + backend_api.set_window_position_mode(mode).await?; - Ok(()) - }, |result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop)) + Ok(()) + }, + |result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop), + ) } - ManagementAppGeneralMsgIn::SetGlobalShortcutResponse { shortcut, shortcut_error } => { + ManagementAppGeneralMsgIn::SetGlobalShortcutResponse { + shortcut, + shortcut_error, + } => { self.current_shortcut = shortcut; self.current_shortcut_error = shortcut_error; @@ -145,12 +169,12 @@ impl ManagementAppGeneralState { } pub fn view(&self) -> Element { - let global_shortcut_selector: Element<_> = ShortcutSelector::new( &self.current_shortcut, - move |value| { ManagementAppGeneralMsgIn::ShortcutCaptured(value) }, - move |value| { ManagementAppGeneralMsgIn::CapturingChanged(value) }, - ).into(); + move |value| ManagementAppGeneralMsgIn::ShortcutCaptured(value), + move |value| ManagementAppGeneralMsgIn::CapturingChanged(value), + ) + .into(); let global_shortcut_field: Element<_> = container(global_shortcut_selector) .width(Length::Fill) @@ -160,7 +184,7 @@ impl ManagementAppGeneralState { let global_shortcut_field = self.view_field( "Global Shortcut", global_shortcut_field, - Some(self.shortcut_capture_after()) + Some(self.shortcut_capture_after()), ); let theme_field = self.theme_field(); @@ -172,16 +196,11 @@ impl ManagementAppGeneralState { content.push(self.window_position_mode_field()) } - let content: Element<_> = column(content) - .into(); + let content: Element<_> = column(content).into(); - let content: Element<_> = container(content) - .width(Length::Fill) - .into(); + let content: Element<_> = container(content).width(Length::Fill).into(); - let content: Element<_> = container(content) - .width(Length::Fill) - .into(); + let content: Element<_> = container(content).width(Length::Fill).into(); content } @@ -214,86 +233,58 @@ impl ManagementAppGeneralState { SettingsTheme::Legacy, ]; - let theme_field: Element<_> = pick_list( - theme_items, - Some(self.theme.clone()), - move |item| ManagementAppGeneralMsgIn::ThemeChanged(item), - ).into(); + let theme_field: Element<_> = pick_list(theme_items, Some(self.theme.clone()), move |item| { + ManagementAppGeneralMsgIn::ThemeChanged(item) + }) + .into(); theme_field } }; - let theme_field: Element<_> = container(theme_field) - .width(Length::Fill) - .into(); + let theme_field: Element<_> = container(theme_field).width(Length::Fill).into(); - let theme_field = self.view_field( - "Theme", - theme_field, - None - ); + let theme_field = self.view_field("Theme", theme_field, None); theme_field } fn window_position_mode_field(&self) -> Element { - let items = [ - WindowPositionMode::Static, - WindowPositionMode::ActiveMonitor, - ]; + let items = [WindowPositionMode::Static, WindowPositionMode::ActiveMonitor]; - let field: Element<_> = pick_list( - items, - Some(self.window_position_mode.clone()), - move |item| ManagementAppGeneralMsgIn::WindowPositionModeChanged(item), - ).into(); + let field: Element<_> = pick_list(items, Some(self.window_position_mode.clone()), move |item| { + ManagementAppGeneralMsgIn::WindowPositionModeChanged(item) + }) + .into(); - let field: Element<_> = container(field) - .width(Length::Fill) - .into(); + let field: Element<_> = container(field).width(Length::Fill).into(); - let field = self.view_field( - "Window Position Mode", - field, - None - ); + let field = self.view_field("Window Position Mode", field, None); field } - fn view_field<'a>(&'a self, label: &'a str, input: Element<'a, ManagementAppGeneralMsgIn>, after: Option>) -> Element<'a, ManagementAppGeneralMsgIn> { + fn view_field<'a>( + &'a self, + label: &'a str, + input: Element<'a, ManagementAppGeneralMsgIn>, + after: Option>, + ) -> Element<'a, ManagementAppGeneralMsgIn> { let label: Element<_> = text(label) .shaping(Shaping::Advanced) .align_x(Horizontal::Right) .width(Length::Fill) .into(); - let label: Element<_> = container(label) - .width(Length::FillPortion(3)) - .padding(4) - .into(); + let label: Element<_> = container(label).width(Length::FillPortion(3)).padding(4).into(); - let input_field = container(input) - .width(Length::FillPortion(3)) - .padding(4) - .into(); + let input_field = container(input).width(Length::FillPortion(3)).padding(4).into(); - let after = after.unwrap_or_else(|| { - Space::with_width(Length::FillPortion(3)) - .into() - }); + let after = after.unwrap_or_else(|| Space::with_width(Length::FillPortion(3)).into()); - let content = vec![ - label, - input_field, - after, - ]; + let content = vec![label, input_field, after]; - let row: Element<_> = row(content) - .align_y(Alignment::Center) - .padding(12) - .into(); + let row: Element<_> = row(content).align_y(Alignment::Center).padding(12).into(); row } @@ -322,9 +313,7 @@ impl ManagementAppGeneralState { .class(TextStyle::Destructive) .into(); - let error_text: Element<_> = text(current_shortcut_error) - .class(TextStyle::Destructive) - .into(); + let error_text: Element<_> = text(current_shortcut_error).class(TextStyle::Destructive).into(); let error_text: Element<_> = container(error_text) .padding(16.0) @@ -332,8 +321,7 @@ impl ManagementAppGeneralState { .class(ContainerStyle::Box) .into(); - let tooltip: Element<_> = tooltip(error_icon, error_text, Position::Bottom) - .into(); + let tooltip: Element<_> = tooltip(error_icon, error_text, Position::Bottom).into(); let content = container(tooltip) .width(Length::FillPortion(3)) @@ -343,16 +331,18 @@ impl ManagementAppGeneralState { content } else { - Space::with_width(Length::FillPortion(3)) - .into() + Space::with_width(Length::FillPortion(3)).into() } } } } -pub fn handle_backend_error(result: Result, convert: impl FnOnce(T) -> ManagementAppGeneralMsgOut) -> ManagementAppGeneralMsgOut { +pub fn handle_backend_error( + result: Result, + convert: impl FnOnce(T) -> ManagementAppGeneralMsgOut, +) -> ManagementAppGeneralMsgOut { match result { Ok(val) => convert(val), - Err(err) => ManagementAppGeneralMsgOut::HandleBackendError(err) + Err(err) => ManagementAppGeneralMsgOut::HandleBackendError(err), } } diff --git a/rust/management_client/src/views/mod.rs b/rust/management_client/src/views/mod.rs index 96b87ed..c904c33 100644 --- a/rust/management_client/src/views/mod.rs +++ b/rust/management_client/src/views/mod.rs @@ -1,2 +1,2 @@ pub mod general; -pub mod plugins; \ No newline at end of file +pub mod plugins; diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index f6d01f8..9147edb 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -2,19 +2,43 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use iced::{padding, Alignment, Length, Padding, Task}; -use iced::widget::{button, column, container, row, scrollable, text, text_input, value, vertical_rule}; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::PluginPreferenceUserData; +use gauntlet_common::model::SettingsPlugin; +use gauntlet_common::rpc::backend_api::BackendApi; +use gauntlet_common::rpc::backend_api::BackendApiError; +use gauntlet_common::settings_env_data_from_string; +use gauntlet_common::SettingsEnvData; +use gauntlet_common::SETTINGS_ENV; +use iced::padding; +use iced::widget::button; +use iced::widget::column; +use iced::widget::container; +use iced::widget::row; +use iced::widget::scrollable; +use iced::widget::text; use iced::widget::text::Shaping; -use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; -use gauntlet_common::{settings_env_data_from_string, SettingsEnvData, SETTINGS_ENV}; -use gauntlet_common::model::{EntrypointId, PluginId, PluginPreferenceUserData, SettingsPlugin}; -use gauntlet_common::rpc::backend_api::{BackendApi, BackendApiError}; +use iced::widget::text_input; +use iced::widget::value; +use iced::widget::vertical_rule; +use iced::Alignment; +use iced::Length; +use iced::Padding; +use iced::Task; +use iced_fonts::Bootstrap; +use iced_fonts::BOOTSTRAP_FONT; use crate::theme::button::ButtonStyle; -use crate::theme::Element; use crate::theme::text::TextStyle; -use crate::views::plugins::preferences::{PluginPreferencesMsg, preferences_ui, SelectItem}; -use crate::views::plugins::table::{PluginTableMsgIn, PluginTableMsgOut, PluginTableState, PluginTableUpdateResult}; +use crate::theme::Element; +use crate::views::plugins::preferences::preferences_ui; +use crate::views::plugins::preferences::PluginPreferencesMsg; +use crate::views::plugins::preferences::SelectItem; +use crate::views::plugins::table::PluginTableMsgIn; +use crate::views::plugins::table::PluginTableMsgOut; +use crate::views::plugins::table::PluginTableState; +use crate::views::plugins::table::PluginTableUpdateResult; mod preferences; mod table; @@ -25,24 +49,18 @@ pub enum ManagementAppPluginMsgIn { PluginPreferenceMsg(PluginPreferencesMsg), FetchPlugins, PluginsFetched(HashMap), - RemovePlugin { - plugin_id: PluginId - }, - DownloadPlugin { - plugin_id: PluginId, - }, + RemovePlugin { plugin_id: PluginId }, + DownloadPlugin { plugin_id: PluginId }, SelectItem(SelectedItem), - Noop + Noop, } pub enum ManagementAppPluginMsgOut { PluginsReloaded(HashMap), SelectedItem(SelectedItem), - DownloadPlugin { - plugin_id: PluginId, - }, + DownloadPlugin { plugin_id: PluginId }, HandleBackendError(BackendApiError), - Noop + Noop, } pub struct ManagementAppPluginsState { @@ -62,13 +80,20 @@ impl ManagementAppPluginsState { let select_item = match settings_env_data { None => SelectedItem::None, - Some(SettingsEnvData::OpenEntrypointPreferences { plugin_id, entrypoint_id }) => SelectedItem::Entrypoint { - plugin_id: PluginId::from_string(plugin_id), - entrypoint_id: EntrypointId::from_string(entrypoint_id), - }, - Some(SettingsEnvData::OpenPluginPreferences { plugin_id }) => SelectedItem::Plugin { - plugin_id: PluginId::from_string(plugin_id), - }, + Some(SettingsEnvData::OpenEntrypointPreferences { + plugin_id, + entrypoint_id, + }) => { + SelectedItem::Entrypoint { + plugin_id: PluginId::from_string(plugin_id), + entrypoint_id: EntrypointId::from_string(entrypoint_id), + } + } + Some(SettingsEnvData::OpenPluginPreferences { plugin_id }) => { + SelectedItem::Plugin { + plugin_id: PluginId::from_string(plugin_id), + } + } }; tracing::debug!("Opening selected item: {:?}", select_item); @@ -85,9 +110,7 @@ impl ManagementAppPluginsState { pub fn update(&mut self, message: ManagementAppPluginMsgIn) -> Task { let backend_api = match &self.backend_api { Some(backend_api) => backend_api.clone(), - None => { - return Task::none() - } + None => return Task::none(), }; match message { @@ -101,31 +124,41 @@ impl ManagementAppPluginsState { Task::perform( async move { - backend_client.set_plugin_state(plugin_id, enabled) - .await?; + backend_client.set_plugin_state(plugin_id, enabled).await?; - let plugins = backend_client.plugins() - .await?; + let plugins = backend_client.plugins().await?; Ok(plugins) }, - |result| handle_backend_error(result, |plugins| ManagementAppPluginMsgOut::PluginsReloaded(plugins)) + |result| { + handle_backend_error(result, |plugins| { + ManagementAppPluginMsgOut::PluginsReloaded(plugins) + }) + }, ) } - PluginTableMsgOut::SetEntrypointState { enabled, plugin_id, entrypoint_id } => { + PluginTableMsgOut::SetEntrypointState { + enabled, + plugin_id, + entrypoint_id, + } => { let mut backend_client = backend_api.clone(); Task::perform( async move { - backend_client.set_entrypoint_state(plugin_id, entrypoint_id, enabled) + backend_client + .set_entrypoint_state(plugin_id, entrypoint_id, enabled) .await?; - let plugins = backend_client.plugins() - .await?; + let plugins = backend_client.plugins().await?; Ok(plugins) }, - |result| handle_backend_error(result, |plugins| ManagementAppPluginMsgOut::PluginsReloaded(plugins)) + |result| { + handle_backend_error(result, |plugins| { + ManagementAppPluginMsgOut::PluginsReloaded(plugins) + }) + }, ) } PluginTableMsgOut::SelectItem(selected_item) => { @@ -150,20 +183,28 @@ impl ManagementAppPluginsState { } ManagementAppPluginMsgIn::PluginPreferenceMsg(msg) => { match msg { - PluginPreferencesMsg::UpdatePreferenceValue { plugin_id, entrypoint_id, id, user_data } => { - self.preference_user_data - .insert((plugin_id.clone(), entrypoint_id.clone(), id.clone()), user_data.clone()); + PluginPreferencesMsg::UpdatePreferenceValue { + plugin_id, + entrypoint_id, + id, + user_data, + } => { + self.preference_user_data.insert( + (plugin_id.clone(), entrypoint_id.clone(), id.clone()), + user_data.clone(), + ); let mut backend_api = backend_api.clone(); Task::perform( async move { - backend_api.set_preference_value(plugin_id, entrypoint_id, id, user_data.to_user_data()) + backend_api + .set_preference_value(plugin_id, entrypoint_id, id, user_data.to_user_data()) .await?; Ok(()) }, - |result| handle_backend_error(result, |()| ManagementAppPluginMsgOut::Noop) + |result| handle_backend_error(result, |()| ManagementAppPluginMsgOut::Noop), ) } } @@ -173,12 +214,13 @@ impl ManagementAppPluginsState { Task::perform( async move { - let plugins = backend_api.plugins() - .await?; + let plugins = backend_api.plugins().await?; Ok(plugins) }, - |result| handle_backend_error(result, |plugins| ManagementAppPluginMsgOut::PluginsReloaded(plugins)) + |result| { + handle_backend_error(result, |plugins| ManagementAppPluginMsgOut::PluginsReloaded(plugins)) + }, ) } ManagementAppPluginMsgIn::PluginsFetched(plugins) => { @@ -193,15 +235,15 @@ impl ManagementAppPluginsState { Task::perform( async move { - backend_client.remove_plugin(plugin_id) - .await?; + backend_client.remove_plugin(plugin_id).await?; - let plugins = backend_client.plugins() - .await?; + let plugins = backend_client.plugins().await?; Ok(plugins) }, - |result| handle_backend_error(result, |plugins| ManagementAppPluginMsgOut::PluginsReloaded(plugins)) + |result| { + handle_backend_error(result, |plugins| ManagementAppPluginMsgOut::PluginsReloaded(plugins)) + }, ) } ManagementAppPluginMsgIn::DownloadPlugin { plugin_id } => { @@ -212,24 +254,29 @@ impl ManagementAppPluginsState { Task::none() } - ManagementAppPluginMsgIn::Noop => { - Task::none() - } + ManagementAppPluginMsgIn::Noop => Task::none(), } } fn apply_plugin_fetch(&mut self, plugins: HashMap) { - self.preference_user_data = plugins.iter() + self.preference_user_data = plugins + .iter() .map(|(plugin_id, plugin)| { let mut result = vec![]; for (id, user_data) in &plugin.preferences_user_data { - result.push(((plugin_id.clone(), None, id.clone()), PluginPreferenceUserDataState::from_user_data(user_data.clone()))) + result.push(( + (plugin_id.clone(), None, id.clone()), + PluginPreferenceUserDataState::from_user_data(user_data.clone()), + )) } for (entrypoint_id, entrypoint) in &plugin.entrypoints { for (id, user_data) in &entrypoint.preferences_user_data { - result.push(((plugin_id.clone(), Some(entrypoint_id.clone()), id.clone()), PluginPreferenceUserDataState::from_user_data(user_data.clone()))) + result.push(( + (plugin_id.clone(), Some(entrypoint_id.clone()), id.clone()), + PluginPreferenceUserDataState::from_user_data(user_data.clone()), + )) } } @@ -240,9 +287,11 @@ impl ManagementAppPluginsState { let mut plugin_data = self.plugin_data.borrow_mut(); - plugin_data.plugins_state = plugins.iter() + plugin_data.plugins_state = plugins + .iter() .map(|(id, _plugin)| { - let show_entrypoints = plugin_data.plugins_state + let show_entrypoints = plugin_data + .plugins_state .get(&id) .map(|data| data.show_entrypoints) .unwrap_or(true); @@ -253,23 +302,25 @@ impl ManagementAppPluginsState { plugin_data.plugins = plugins; - let mut plugin_refs: Vec<_> = plugin_data.plugins + let mut plugin_refs: Vec<_> = plugin_data + .plugins .iter() .map(|(_, plugin)| (plugin, plugin_data.plugins_state.get(&plugin.plugin_id).unwrap())) .collect(); plugin_refs.sort_by_key(|(plugin, _)| &plugin.plugin_name); - - self.table_state.apply_plugin_reload(self.plugin_data.clone(), plugin_refs) + + self.table_state + .apply_plugin_reload(self.plugin_data.clone(), plugin_refs) } pub fn view(&self) -> Element { - let table: Element<_> = self.table_state.view() + let table: Element<_> = self + .table_state + .view() .map(|msg| ManagementAppPluginMsgIn::PluginTableMsg(msg)); - let table: Element<_> = container(table) - .padding(Padding::new(8.0)) - .into(); + let table: Element<_> = container(table).padding(Padding::new(8.0)).into(); let sidebar_content: Element<_> = match &self.selected_item { SelectedItem::None => { @@ -277,8 +328,7 @@ impl ManagementAppPluginsState { let text2: Element<_> = text("or").into(); let text3: Element<_> = text("Click '+' to add new plugin").into(); - let text_column = column(vec![text1, text2, text3]) - .align_x(Alignment::Center); + let text_column = column(vec![text1, text2, text3]).align_x(Alignment::Center); container(text_column) .align_y(Alignment::Center) @@ -304,69 +354,47 @@ impl ManagementAppPluginsState { .into() } Some(plugin) => { - let name = text(plugin.plugin_name.to_string()) - .shaping(Shaping::Advanced); + let name = text(plugin.plugin_name.to_string()).shaping(Shaping::Advanced); - let name = container(name) - .padding(Padding::new(8.0)) - .into(); + let name = container(name).padding(Padding::new(8.0)).into(); let id: Element<_> = text(plugin.plugin_id.to_string()) .shaping(Shaping::Advanced) .class(TextStyle::Subtitle) .into(); - let id = container(id) - .padding(padding::bottom(8.0)) - .into(); + let id = container(id).padding(padding::bottom(8.0)).into(); - let mut column_content = vec![ - name, - id, - ]; + let mut column_content = vec![name, id]; if !plugin.plugin_description.is_empty() { - let description_label: Element<_> = text("Description") - .size(14) - .class(TextStyle::Subtitle) - .into(); + let description_label: Element<_> = + text("Description").size(14).class(TextStyle::Subtitle).into(); - let description_label = container(description_label) - .padding(padding::bottom(8.0)) - .into(); + let description_label = container(description_label).padding(padding::bottom(8.0)).into(); - let description = text(plugin.plugin_description.to_string()) - .shaping(Shaping::Advanced); + let description = text(plugin.plugin_description.to_string()).shaping(Shaping::Advanced); - let description = container(description) - .padding(Padding::new(8.0)) - .into(); + let description = container(description).padding(Padding::new(8.0)).into(); - let content: Element<_> = column(vec![description_label, description]) - .into(); + let content: Element<_> = column(vec![description_label, description]).into(); column_content.push(content); } column_content.push( preferences_ui(plugin_id.clone(), None, &plugin.preferences, &self.preference_user_data) - .map(|msg| ManagementAppPluginMsgIn::PluginPreferenceMsg(msg)) + .map(|msg| ManagementAppPluginMsgIn::PluginPreferenceMsg(msg)), ); - let content: Element<_> = column(column_content) - .spacing(12) - .into(); + let content: Element<_> = column(column_content).spacing(12).into(); - let content: Element<_> = scrollable(content) - .height(Length::Fill) - .width(Length::Fill) - .into(); + let content: Element<_> = scrollable(content).height(Length::Fill).width(Length::Fill).into(); let mut column_content = vec![content]; if !plugin.plugin_id.to_string().starts_with("bundled://") { - let check_for_updates_text: Element<_> = text("Check for updates") - .into(); + let check_for_updates_text: Element<_> = text("Check for updates").into(); let check_for_updates_text_container: Element<_> = container(check_for_updates_text) .width(Length::Fill) @@ -377,13 +405,14 @@ impl ManagementAppPluginsState { let check_for_updates_button: Element<_> = button(check_for_updates_text_container) .width(Length::Fill) .class(ButtonStyle::Primary) - .on_press(ManagementAppPluginMsgIn::DownloadPlugin { plugin_id: plugin.plugin_id.clone() }) + .on_press(ManagementAppPluginMsgIn::DownloadPlugin { + plugin_id: plugin.plugin_id.clone(), + }) .into(); column_content.push(check_for_updates_button); - let remove_text: Element<_> = text("Remove plugin") - .into(); + let remove_text: Element<_> = text("Remove plugin").into(); let remove_button_text_container: Element<_> = container(remove_text) .width(Length::Fill) @@ -394,27 +423,28 @@ impl ManagementAppPluginsState { let remove_button: Element<_> = button(remove_button_text_container) .width(Length::Fill) .class(ButtonStyle::Destructive) - .on_press(ManagementAppPluginMsgIn::RemovePlugin { plugin_id: plugin.plugin_id.clone() }) + .on_press(ManagementAppPluginMsgIn::RemovePlugin { + plugin_id: plugin.plugin_id.clone(), + }) .into(); column_content.push(remove_button); } - let content: Element<_> = column(column_content) - .spacing(8.0) - .into(); + let content: Element<_> = column(column_content).spacing(8.0).into(); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .into() + container(content).width(Length::Fill).height(Length::Fill).into() } } } - SelectedItem::Entrypoint { plugin_id, entrypoint_id } => { + SelectedItem::Entrypoint { + plugin_id, + entrypoint_id, + } => { let plugin_data = self.plugin_data.borrow(); - let entrypoint = plugin_data.plugins + let entrypoint = plugin_data + .plugins .get(&plugin_id) .map(|plugin| plugin.entrypoints.get(entrypoint_id)) .flatten(); @@ -431,49 +461,40 @@ impl ManagementAppPluginsState { .into() } Some(entrypoint) => { - let name = text(entrypoint.entrypoint_name.to_string()) - .shaping(Shaping::Advanced); + let name = text(entrypoint.entrypoint_name.to_string()).shaping(Shaping::Advanced); - let name = container(name) - .padding(Padding::new(8.0)) - .into(); + let name = container(name).padding(Padding::new(8.0)).into(); - let mut column_content = vec![ - name, - ]; + let mut column_content = vec![name]; if !entrypoint.entrypoint_description.is_empty() { - let description_label: Element<_> = text("Description") - .size(14) - .class(TextStyle::Subtitle) - .into(); + let description_label: Element<_> = + text("Description").size(14).class(TextStyle::Subtitle).into(); - let description_label = container(description_label) - .padding(padding::bottom(8.0)) - .into(); + let description_label = container(description_label).padding(padding::bottom(8.0)).into(); let description = container(text(entrypoint.entrypoint_description.to_string())) .padding(Padding::new(8.0)) .into(); - let content: Element<_> = column(vec![description_label, description]) - .into(); + let content: Element<_> = column(vec![description_label, description]).into(); column_content.push(content); } column_content.push( - preferences_ui(plugin_id.clone(), Some(entrypoint_id.clone()), &entrypoint.preferences, &self.preference_user_data) - .map(|msg| ManagementAppPluginMsgIn::PluginPreferenceMsg(msg)) + preferences_ui( + plugin_id.clone(), + Some(entrypoint_id.clone()), + &entrypoint.preferences, + &self.preference_user_data, + ) + .map(|msg| ManagementAppPluginMsgIn::PluginPreferenceMsg(msg)), ); - let column: Element<_> = column(column_content) - .spacing(12) - .into(); + let column: Element<_> = column(column_content).spacing(12).into(); - let column: Element<_> = scrollable(column) - .width(Length::Fill) - .into(); + let column: Element<_> = scrollable(column).width(Length::Fill).into(); container(column) .width(Length::Fill) @@ -485,15 +506,20 @@ impl ManagementAppPluginsState { } SelectedItem::NewPlugin { repository_url } => { let url_input: Element<_> = text_input("Enter Git Repository URL", &repository_url) - .on_input(|value| ManagementAppPluginMsgIn::SelectItem(SelectedItem::NewPlugin { repository_url: value })) - .on_submit(ManagementAppPluginMsgIn::DownloadPlugin { plugin_id: PluginId::from_string(repository_url) }) + .on_input(|value| { + ManagementAppPluginMsgIn::SelectItem(SelectedItem::NewPlugin { repository_url: value }) + }) + .on_submit(ManagementAppPluginMsgIn::DownloadPlugin { + plugin_id: PluginId::from_string(repository_url), + }) .into(); let content: Element<_> = column(vec![ url_input, text("Supported protocols:").into(), text("http(s), ssh, git").into(), - ]).into(); + ]) + .into(); container(content) .padding(Padding::new(8.0)) @@ -504,7 +530,6 @@ impl ManagementAppPluginsState { } }; - let plugin_url = if let SelectedItem::NewPlugin { repository_url } = &self.selected_item { if !repository_url.is_empty() { Some(repository_url) @@ -518,8 +543,7 @@ impl ManagementAppPluginsState { let top_button_text = if plugin_url.is_some() { text("Download plugin") } else { - value(Bootstrap::Plus) - .font(BOOTSTRAP_FONT) + value(Bootstrap::Plus).font(BOOTSTRAP_FONT) }; let top_button_text_container: Element<_> = container(top_button_text) @@ -529,8 +553,16 @@ impl ManagementAppPluginsState { .into(); let top_button_action = match plugin_url { - Some(plugin_url) => ManagementAppPluginMsgIn::DownloadPlugin { plugin_id: PluginId::from_string(plugin_url) }, - None => ManagementAppPluginMsgIn::SelectItem(SelectedItem::NewPlugin { repository_url: Default::default() }) + Some(plugin_url) => { + ManagementAppPluginMsgIn::DownloadPlugin { + plugin_id: PluginId::from_string(plugin_url), + } + } + None => { + ManagementAppPluginMsgIn::SelectItem(SelectedItem::NewPlugin { + repository_url: Default::default(), + }) + } }; let top_button = button(top_button_text_container) @@ -542,18 +574,16 @@ impl ManagementAppPluginsState { .padding(Padding::new(8.0)) .into(); - let separator: Element<_> = vertical_rule(1) - .into(); + let separator: Element<_> = vertical_rule(1).into(); - let content: Element<_> = row(vec![table, separator, sidebar]) - .into(); + let content: Element<_> = row(vec![table, separator, sidebar]).into(); let content = container(content) .padding(Padding::new(4.0)) .height(Length::Fill) .width(Length::Fill) .into(); - + content } } @@ -561,7 +591,7 @@ impl ManagementAppPluginsState { #[derive(Debug, Clone)] struct PluginDataContainer { plugins: HashMap, - plugins_state: HashMap + plugins_state: HashMap, } impl PluginDataContainer { @@ -577,10 +607,10 @@ impl PluginDataContainer { pub enum SelectedItem { None, NewPlugin { - repository_url: String + repository_url: String, }, Plugin { - plugin_id: PluginId + plugin_id: PluginId, }, Entrypoint { plugin_id: PluginId, @@ -593,7 +623,6 @@ struct SettingsPluginData { show_entrypoints: bool, } - #[derive(Debug, Clone)] pub enum PluginPreferenceUserDataState { Number { @@ -610,16 +639,16 @@ pub enum PluginPreferenceUserDataState { }, ListOfStrings { value: Option>, - new_value: String + new_value: String, }, ListOfNumbers { value: Option>, - new_value: f64 + new_value: f64, }, ListOfEnums { value: Option>, - new_value: Option - } + new_value: Option, + }, } impl PluginPreferenceUserDataState { @@ -629,18 +658,18 @@ impl PluginPreferenceUserDataState { PluginPreferenceUserData::String { value } => PluginPreferenceUserDataState::String { value }, PluginPreferenceUserData::Enum { value } => PluginPreferenceUserDataState::Enum { value }, PluginPreferenceUserData::Bool { value } => PluginPreferenceUserDataState::Bool { value }, - PluginPreferenceUserData::ListOfStrings { value } => PluginPreferenceUserDataState::ListOfStrings { - value, - new_value: "".to_owned() - }, - PluginPreferenceUserData::ListOfNumbers { value } => PluginPreferenceUserDataState::ListOfNumbers { - value, - new_value: 0.0 - }, - PluginPreferenceUserData::ListOfEnums { value } => PluginPreferenceUserDataState::ListOfEnums { - value, - new_value: None - }, + PluginPreferenceUserData::ListOfStrings { value } => { + PluginPreferenceUserDataState::ListOfStrings { + value, + new_value: "".to_owned(), + } + } + PluginPreferenceUserData::ListOfNumbers { value } => { + PluginPreferenceUserDataState::ListOfNumbers { value, new_value: 0.0 } + } + PluginPreferenceUserData::ListOfEnums { value } => { + PluginPreferenceUserDataState::ListOfEnums { value, new_value: None } + } } } @@ -650,16 +679,23 @@ impl PluginPreferenceUserDataState { PluginPreferenceUserDataState::String { value } => PluginPreferenceUserData::String { value }, PluginPreferenceUserDataState::Enum { value } => PluginPreferenceUserData::Enum { value }, PluginPreferenceUserDataState::Bool { value } => PluginPreferenceUserData::Bool { value }, - PluginPreferenceUserDataState::ListOfStrings { value, .. } => PluginPreferenceUserData::ListOfStrings { value }, - PluginPreferenceUserDataState::ListOfNumbers { value, .. } => PluginPreferenceUserData::ListOfNumbers { value }, + PluginPreferenceUserDataState::ListOfStrings { value, .. } => { + PluginPreferenceUserData::ListOfStrings { value } + } + PluginPreferenceUserDataState::ListOfNumbers { value, .. } => { + PluginPreferenceUserData::ListOfNumbers { value } + } PluginPreferenceUserDataState::ListOfEnums { value, .. } => PluginPreferenceUserData::ListOfEnums { value }, } } } -pub fn handle_backend_error(result: Result, convert: impl FnOnce(T) -> ManagementAppPluginMsgOut) -> ManagementAppPluginMsgOut { +pub fn handle_backend_error( + result: Result, + convert: impl FnOnce(T) -> ManagementAppPluginMsgOut, +) -> ManagementAppPluginMsgOut { match result { Ok(val) => convert(val), - Err(err) => ManagementAppPluginMsgOut::HandleBackendError(err) + Err(err) => ManagementAppPluginMsgOut::HandleBackendError(err), } } diff --git a/rust/management_client/src/views/plugins/preferences.rs b/rust/management_client/src/views/plugins/preferences.rs index f923d44..de7a450 100644 --- a/rust/management_client/src/views/plugins/preferences.rs +++ b/rust/management_client/src/views/plugins/preferences.rs @@ -1,16 +1,31 @@ +use std::collections::HashMap; +use std::fmt::Display; + +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::PluginPreference; +use iced::padding; +use iced::widget; +use iced::widget::button; +use iced::widget::checkbox; +use iced::widget::column; +use iced::widget::container; +use iced::widget::pick_list; +use iced::widget::row; +use iced::widget::text; +use iced::widget::text::Shaping; +use iced::widget::text_input; +use iced::Length; +use iced::Padding; +use iced_aw::number_input; +use iced_fonts::Bootstrap; +use iced_fonts::BOOTSTRAP_FONT; + use crate::theme::button::ButtonStyle; use crate::theme::container::ContainerStyle; use crate::theme::text::TextStyle; use crate::theme::Element; use crate::views::plugins::PluginPreferenceUserDataState; -use gauntlet_common::model::{EntrypointId, PluginId, PluginPreference}; -use iced::widget::{button, checkbox, column, container, pick_list, row, text, text_input}; -use iced::{padding, widget, Length, Padding}; -use iced_aw::number_input; -use std::collections::HashMap; -use std::fmt::Display; -use iced::widget::text::Shaping; -use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; #[derive(Debug, Clone)] pub enum PluginPreferencesMsg { @@ -18,14 +33,15 @@ pub enum PluginPreferencesMsg { plugin_id: PluginId, entrypoint_id: Option, id: String, - user_data: PluginPreferenceUserDataState + user_data: PluginPreferenceUserDataState, }, } #[derive(Clone, Debug, Eq, PartialEq)] -pub struct SelectItem { // TODO private +pub struct SelectItem { + // TODO private value: String, - label: String + label: String, } impl Display for SelectItem { @@ -38,13 +54,11 @@ pub fn preferences_ui<'a>( plugin_id: PluginId, entrypoint_id: Option, preferences: &HashMap, - preference_user_data: &HashMap<(PluginId, Option, String), PluginPreferenceUserDataState> + preference_user_data: &HashMap<(PluginId, Option, String), PluginPreferenceUserDataState>, ) -> Element<'a, PluginPreferencesMsg> { let mut column_content = vec![]; - let mut preferences: Vec<_> = preferences.iter() - .map(|entry| entry) - .collect(); + let mut preferences: Vec<_> = preferences.iter().map(|entry| entry).collect(); preferences.sort_by_key(|(&ref key, _)| key); @@ -74,21 +88,16 @@ pub fn preferences_ui<'a>( .class(TextStyle::Subtitle) .into(); - let preference_label = container(preference_label) - .padding(padding::left(8.0)) - .into(); + let preference_label = container(preference_label).padding(padding::left(8.0)).into(); let mut input_field_column = vec![]; input_field_column.push(preference_label); if !description.trim().is_empty() { - let description = text(description) - .shaping(Shaping::Advanced); + let description = text(description).shaping(Shaping::Advanced); - let description = container(description) - .padding(Padding::from([4.0, 8.0])) - .into(); + let description = container(description).padding(Padding::from([4.0, 8.0])).into(); input_field_column.push(description); } @@ -98,7 +107,7 @@ pub fn preferences_ui<'a>( let value = match user_data { None => None, Some(PluginPreferenceUserDataState::Number { value }) => value.to_owned(), - Some(_) => unreachable!() + Some(_) => unreachable!(), }; let missing = value.as_ref().or(default.as_ref()).is_none(); @@ -114,16 +123,20 @@ pub fn preferences_ui<'a>( plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), id: preference_id.to_owned(), - user_data: PluginPreferenceUserDataState::Number { - value: Some(value), - }, + user_data: PluginPreferenceUserDataState::Number { value: Some(value) }, } })); let input_field = container(input_field) .width(Length::Fill) .padding(Padding::from([4.0, 8.0])) - .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class( + if missing { + ContainerStyle::TextInputMissingValue + } else { + ContainerStyle::Transparent + }, + ) .into(); input_field @@ -132,7 +145,7 @@ pub fn preferences_ui<'a>( let value = match user_data { None => None, Some(PluginPreferenceUserDataState::String { value }) => value.to_owned(), - Some(_) => unreachable!() + Some(_) => unreachable!(), }; let missing = value.as_ref().or(default.as_ref()).is_none(); @@ -145,34 +158,47 @@ pub fn preferences_ui<'a>( plugin_id: plugin_id.clone(), entrypoint_id: entrypoint_id.clone(), id: preference_id.to_owned(), - user_data: PluginPreferenceUserDataState::String { - value: Some(value), - }, + user_data: PluginPreferenceUserDataState::String { value: Some(value) }, } })) .into(); let input_field = container(input_field) .padding(Padding::new(8.0)) - .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class( + if missing { + ContainerStyle::TextInputMissingValue + } else { + ContainerStyle::Transparent + }, + ) .into(); input_field } - PluginPreference::Enum { default, enum_values, .. } => { + PluginPreference::Enum { + default, enum_values, .. + } => { let value = match user_data { None => None, Some(PluginPreferenceUserDataState::Enum { value }) => value.to_owned(), - Some(_) => unreachable!() + Some(_) => unreachable!(), }; let missing = value.as_ref().or(default.as_ref()).is_none(); - let enum_values: Vec<_> = enum_values.iter() - .map(|enum_item| SelectItem { label: enum_item.label.to_owned(), value: enum_item.value.to_owned() }) + let enum_values: Vec<_> = enum_values + .iter() + .map(|enum_item| { + SelectItem { + label: enum_item.label.to_owned(), + value: enum_item.value.to_owned(), + } + }) .collect(); - let value = value.or(default.to_owned()) + let value = value + .or(default.to_owned()) .map(|value| enum_values.iter().find(|item| item.value == value)) .flatten() .map(|value| value.clone()); @@ -180,22 +206,30 @@ pub fn preferences_ui<'a>( let input_field: Element<_> = pick_list( enum_values, value, - Box::new(move |item: SelectItem| PluginPreferencesMsg::UpdatePreferenceValue { - plugin_id: plugin_id.clone(), - entrypoint_id: entrypoint_id.clone(), - id: preference_id.to_owned(), - user_data: PluginPreferenceUserDataState::Enum { - value: Some(item.value), - }, - }) + Box::new(move |item: SelectItem| { + PluginPreferencesMsg::UpdatePreferenceValue { + plugin_id: plugin_id.clone(), + entrypoint_id: entrypoint_id.clone(), + id: preference_id.to_owned(), + user_data: PluginPreferenceUserDataState::Enum { + value: Some(item.value), + }, + } + }), ) - .width(Length::Fill) - .into(); + .width(Length::Fill) + .into(); let input_field = container(input_field) .padding(Padding::new(8.0)) .width(Length::Fill) - .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class( + if missing { + ContainerStyle::TextInputMissingValue + } else { + ContainerStyle::Transparent + }, + ) .into(); input_field @@ -204,27 +238,32 @@ pub fn preferences_ui<'a>( let value = match user_data { None => None, Some(PluginPreferenceUserDataState::Bool { value }) => value.to_owned(), - Some(_) => unreachable!() + Some(_) => unreachable!(), }; let missing = value.as_ref().or(default.as_ref()).is_none(); - let input_field: Element<_> = checkbox(preference_name.clone(), value.or(default.to_owned()).unwrap_or(false)) - .on_toggle(Box::new(move |value| { - PluginPreferencesMsg::UpdatePreferenceValue { - plugin_id: plugin_id.clone(), - entrypoint_id: entrypoint_id.clone(), - id: preference_id.to_owned(), - user_data: PluginPreferenceUserDataState::Bool { - value: Some(value), - }, - } - })) - .into(); + let input_field: Element<_> = + checkbox(preference_name.clone(), value.or(default.to_owned()).unwrap_or(false)) + .on_toggle(Box::new(move |value| { + PluginPreferencesMsg::UpdatePreferenceValue { + plugin_id: plugin_id.clone(), + entrypoint_id: entrypoint_id.clone(), + id: preference_id.to_owned(), + user_data: PluginPreferenceUserDataState::Bool { value: Some(value) }, + } + })) + .into(); let input_field = container(input_field) .padding(Padding::new(8.0)) - .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class( + if missing { + ContainerStyle::TextInputMissingValue + } else { + ContainerStyle::Transparent + }, + ) .into(); input_field @@ -232,18 +271,20 @@ pub fn preferences_ui<'a>( PluginPreference::ListOfStrings { default, .. } => { let (value, new_value) = match user_data { None => (None, "".to_owned()), - Some(PluginPreferenceUserDataState::ListOfStrings { value, new_value }) => (value.to_owned(), new_value.to_owned()), - Some(_) => unreachable!() + Some(PluginPreferenceUserDataState::ListOfStrings { value, new_value }) => { + (value.to_owned(), new_value.to_owned()) + } + Some(_) => unreachable!(), }; let missing = value.as_ref().or(default.as_ref()).is_none(); - let mut items: Vec<_> = value.clone() + let mut items: Vec<_> = value + .clone() .unwrap_or(vec![]) .iter() .enumerate() .map(|(index, value_item)| { - let mut value = value.clone(); if let Some(value) = &mut value { value.remove(index); @@ -254,8 +295,7 @@ pub fn preferences_ui<'a>( .padding(Padding::new(4.0)) .into(); - let remove_icon = widget::value(Bootstrap::Dash) - .font(BOOTSTRAP_FONT); + let remove_icon = widget::value(Bootstrap::Dash).font(BOOTSTRAP_FONT); let remove_button: Element<_> = button(remove_icon) .class(ButtonStyle::Primary) @@ -271,22 +311,16 @@ pub fn preferences_ui<'a>( .padding(Padding::from([5.0, 7.0])) .into(); - let remove_button = container(remove_button) - .padding(padding::bottom(8.0)) - .into(); + let remove_button = container(remove_button).padding(padding::bottom(8.0)).into(); - let item: Element<_> = row([item_text, remove_button]) - .into(); + let item: Element<_> = row([item_text, remove_button]).into(); - let item = container(item) - .padding(Padding::from([4.0, 8.0])) - .into(); + let item = container(item).padding(Padding::from([4.0, 8.0])).into(); item }) .collect(); - let save_value = match &value { None => vec![new_value.clone()], Some(value) => { @@ -310,9 +344,7 @@ pub fn preferences_ui<'a>( }) }; - let add_icon: Element<_> = widget::value(Bootstrap::Plus) - .font(BOOTSTRAP_FONT) - .into(); + let add_icon: Element<_> = widget::value(Bootstrap::Plus).font(BOOTSTRAP_FONT).into(); let add_button: Element<_> = button(add_icon) .class(ButtonStyle::Primary) @@ -320,37 +352,39 @@ pub fn preferences_ui<'a>( .padding(Padding::from([5.0, 7.0])) .into(); - let add_button: Element<_> = container(add_button) - .padding(padding::bottom(8.0)) - .into(); + let add_button: Element<_> = container(add_button).padding(padding::bottom(8.0)).into(); let add_text_input: Element<_> = text_input("Enter value...", &new_value) - .on_input(move |new_value| PluginPreferencesMsg::UpdatePreferenceValue { - plugin_id: plugin_id.clone(), - entrypoint_id: entrypoint_id.clone(), - id: preference_id.to_owned(), - user_data: PluginPreferenceUserDataState::ListOfStrings { - value: value.clone(), - new_value, - }, + .on_input(move |new_value| { + PluginPreferencesMsg::UpdatePreferenceValue { + plugin_id: plugin_id.clone(), + entrypoint_id: entrypoint_id.clone(), + id: preference_id.to_owned(), + user_data: PluginPreferenceUserDataState::ListOfStrings { + value: value.clone(), + new_value, + }, + } }) .into(); - let add_item: Element<_> = row([add_text_input, add_button]) - .into(); + let add_item: Element<_> = row([add_text_input, add_button]).into(); - let add_item: Element<_> = container(add_item) - .padding(Padding::new(8.0)) - .into(); + let add_item: Element<_> = container(add_item).padding(Padding::new(8.0)).into(); items.push(add_item); - let content: Element<_> = column(items) - .into(); + let content: Element<_> = column(items).into(); - let content: Element<_> = container(content) + let content: Element<_> = container(content) .padding(0) - .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class( + if missing { + ContainerStyle::TextInputMissingValue + } else { + ContainerStyle::Transparent + }, + ) .into(); content @@ -358,18 +392,20 @@ pub fn preferences_ui<'a>( PluginPreference::ListOfNumbers { default, .. } => { let (value, new_value) = match user_data { None => (None, 0.0), - Some(PluginPreferenceUserDataState::ListOfNumbers { value, new_value }) => (value.to_owned(), new_value.to_owned()), - Some(_) => unreachable!() + Some(PluginPreferenceUserDataState::ListOfNumbers { value, new_value }) => { + (value.to_owned(), new_value.to_owned()) + } + Some(_) => unreachable!(), }; let missing = value.as_ref().or(default.as_ref()).is_none(); - let mut items: Vec<_> = value.clone() + let mut items: Vec<_> = value + .clone() .unwrap_or(vec![]) .iter() .enumerate() .map(|(index, value_item)| { - let mut value = value.clone(); if let Some(value) = &mut value { value.remove(index); @@ -380,8 +416,7 @@ pub fn preferences_ui<'a>( .padding(Padding::new(4.0)) .into(); - let remove_icon = widget::value(Bootstrap::Dash) - .font(BOOTSTRAP_FONT); + let remove_icon = widget::value(Bootstrap::Dash).font(BOOTSTRAP_FONT); let remove_button: Element<_> = button(remove_icon) .class(ButtonStyle::Primary) @@ -397,22 +432,16 @@ pub fn preferences_ui<'a>( .padding(Padding::from([5.0, 7.0])) .into(); - let remove_button = container(remove_button) - .padding(padding::bottom(8.0)) - .into(); + let remove_button = container(remove_button).padding(padding::bottom(8.0)).into(); - let item: Element<_> = row([item_text, remove_button]) - .into(); + let item: Element<_> = row([item_text, remove_button]).into(); - let item = container(item) - .padding(Padding::from([4.0, 8.0])) - .into(); + let item = container(item).padding(Padding::from([4.0, 8.0])).into(); item }) .collect(); - let save_value = match &value { None => vec![new_value.clone()], Some(value) => { @@ -422,9 +451,7 @@ pub fn preferences_ui<'a>( } }; - let add_icon: Element<_> = widget::value(Bootstrap::Plus) - .font(BOOTSTRAP_FONT) - .into(); + let add_icon: Element<_> = widget::value(Bootstrap::Plus).font(BOOTSTRAP_FONT).into(); let add_button: Element<_> = button(add_icon) .class(ButtonStyle::Primary) @@ -440,9 +467,7 @@ pub fn preferences_ui<'a>( .padding(Padding::from([5.0, 7.0])) .into(); - let add_button: Element<_> = container(add_button) - .padding(padding::bottom(8.0)) - .into(); + let add_button: Element<_> = container(add_button).padding(padding::bottom(8.0)).into(); let add_number_input: Element<_> = number_input(new_value, f64::MIN..f64::MAX, std::convert::identity) .width(Length::Fill) @@ -460,40 +485,46 @@ pub fn preferences_ui<'a>( } })); - let add_item: Element<_> = row([add_number_input, add_button]) - .into(); + let add_item: Element<_> = row([add_number_input, add_button]).into(); - let add_item: Element<_> = container(add_item) - .padding(Padding::new(8.0)) - .into(); + let add_item: Element<_> = container(add_item).padding(Padding::new(8.0)).into(); items.push(add_item); - let content: Element<_> = column(items) - .into(); + let content: Element<_> = column(items).into(); - let content: Element<_> = container(content) + let content: Element<_> = container(content) .padding(0) - .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class( + if missing { + ContainerStyle::TextInputMissingValue + } else { + ContainerStyle::Transparent + }, + ) .into(); content } - PluginPreference::ListOfEnums { enum_values, default, .. } => { + PluginPreference::ListOfEnums { + enum_values, default, .. + } => { let (value, new_value) = match user_data { None => (None, None), - Some(PluginPreferenceUserDataState::ListOfEnums { value, new_value }) => (value.to_owned(), new_value.to_owned()), - Some(_) => unreachable!() + Some(PluginPreferenceUserDataState::ListOfEnums { value, new_value }) => { + (value.to_owned(), new_value.to_owned()) + } + Some(_) => unreachable!(), }; let missing = value.as_ref().or(default.as_ref()).is_none(); - let mut items: Vec<_> = value.clone() + let mut items: Vec<_> = value + .clone() .unwrap_or(vec![]) .iter() .enumerate() .map(|(index, value_item)| { - let mut value = value.clone(); if let Some(value) = &mut value { value.remove(index); @@ -504,8 +535,7 @@ pub fn preferences_ui<'a>( .padding(Padding::new(4.0)) .into(); - let remove_icon = widget::value(Bootstrap::Dash) - .font(BOOTSTRAP_FONT); + let remove_icon = widget::value(Bootstrap::Dash).font(BOOTSTRAP_FONT); let remove_button: Element<_> = button(remove_icon) .class(ButtonStyle::Primary) @@ -521,22 +551,16 @@ pub fn preferences_ui<'a>( .padding(Padding::from([5.0, 7.0])) .into(); - let remove_button = container(remove_button) - .padding(padding::bottom(8.0)) - .into(); + let remove_button = container(remove_button).padding(padding::bottom(8.0)).into(); - let item: Element<_> = row([item_text, remove_button]) - .into(); + let item: Element<_> = row([item_text, remove_button]).into(); - let item = container(item) - .padding(Padding::from([4.0, 8.0])) - .into(); + let item = container(item).padding(Padding::from([4.0, 8.0])).into(); item }) .collect(); - let add_msg = match &new_value { None => None, Some(new_value) => { @@ -561,10 +585,7 @@ pub fn preferences_ui<'a>( } }; - - let add_icon: Element<_> = widget::value(Bootstrap::Plus) - .font(BOOTSTRAP_FONT) - .into(); + let add_icon: Element<_> = widget::value(Bootstrap::Plus).font(BOOTSTRAP_FONT).into(); let add_button: Element<_> = button(add_icon) .class(ButtonStyle::Primary) @@ -572,46 +593,54 @@ pub fn preferences_ui<'a>( .padding(Padding::from([5.0, 7.0])) .into(); - let add_button: Element<_> = container(add_button) - .padding(padding::bottom(8.0)) - .into(); + let add_button: Element<_> = container(add_button).padding(padding::bottom(8.0)).into(); - let enum_values: Vec<_> = enum_values.iter() - .map(|enum_item| SelectItem { label: enum_item.label.to_owned(), value: enum_item.value.to_owned() }) + let enum_values: Vec<_> = enum_values + .iter() + .map(|enum_item| { + SelectItem { + label: enum_item.label.to_owned(), + value: enum_item.value.to_owned(), + } + }) .collect(); let add_enum_input: Element<_> = pick_list( enum_values, new_value, - Box::new(move |new_value: SelectItem| PluginPreferencesMsg::UpdatePreferenceValue { - plugin_id: plugin_id.clone(), - entrypoint_id: entrypoint_id.clone(), - id: preference_id.to_owned(), - user_data: PluginPreferenceUserDataState::ListOfEnums { - value: value.clone(), - new_value: Some(new_value), - }, + Box::new(move |new_value: SelectItem| { + PluginPreferencesMsg::UpdatePreferenceValue { + plugin_id: plugin_id.clone(), + entrypoint_id: entrypoint_id.clone(), + id: preference_id.to_owned(), + user_data: PluginPreferenceUserDataState::ListOfEnums { + value: value.clone(), + new_value: Some(new_value), + }, + } }), ) - .placeholder("Select value...") - .width(Length::Fill) - .into(); + .placeholder("Select value...") + .width(Length::Fill) + .into(); - let add_item: Element<_> = row([add_enum_input, add_button]) - .into(); + let add_item: Element<_> = row([add_enum_input, add_button]).into(); - let add_item: Element<_> = container(add_item) - .padding(Padding::new(8.0)) - .into(); + let add_item: Element<_> = container(add_item).padding(Padding::new(8.0)).into(); items.push(add_item); - let content: Element<_> = column(items) - .into(); + let content: Element<_> = column(items).into(); - let content: Element<_> = container(content) + let content: Element<_> = container(content) .padding(0) - .class(if missing { ContainerStyle::TextInputMissingValue } else { ContainerStyle::Transparent }) + .class( + if missing { + ContainerStyle::TextInputMissingValue + } else { + ContainerStyle::Transparent + }, + ) .into(); content @@ -620,15 +649,12 @@ pub fn preferences_ui<'a>( input_field_column.push(input_field); - let content: Element<_> = column(input_field_column) - .into(); + let content: Element<_> = column(input_field_column).into(); column_content.push(content); } - let element: Element<_> = column(column_content) - .spacing(12) - .into(); + let element: Element<_> = column(column_content).spacing(12).into(); element -} \ No newline at end of file +} diff --git a/rust/management_client/src/views/plugins/table.rs b/rust/management_client/src/views/plugins/table.rs index b6d697b..bb35d10 100644 --- a/rust/management_client/src/views/plugins/table.rs +++ b/rust/management_client/src/views/plugins/table.rs @@ -1,27 +1,42 @@ use std::cell::RefCell; use std::rc::Rc; -use iced::{Alignment, Length, Renderer, Task}; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::SettingsEntrypointType; +use gauntlet_common::model::SettingsPlugin; use iced::advanced::text::Shaping; -use iced::widget::{button, checkbox, container, horizontal_space, row, scrollable, Space, text, value}; +use iced::widget::button; +use iced::widget::checkbox; +use iced::widget::container; +use iced::widget::horizontal_space; +use iced::widget::row; +use iced::widget::scrollable; use iced::widget::scrollable::Id; -use iced_fonts::{Bootstrap, BOOTSTRAP_FONT}; +use iced::widget::text; +use iced::widget::value; +use iced::widget::Space; +use iced::Alignment; +use iced::Length; +use iced::Renderer; +use iced::Task; +use iced_fonts::Bootstrap; +use iced_fonts::BOOTSTRAP_FONT; use iced_table::table; -use gauntlet_common::model::{EntrypointId, PluginId, SettingsEntrypointType, SettingsPlugin}; - -use crate::theme::{Element, GauntletSettingsTheme}; use crate::theme::button::ButtonStyle; -use crate::views::plugins::{PluginDataContainer, SelectedItem, SettingsPluginData}; +use crate::theme::Element; +use crate::theme::GauntletSettingsTheme; +use crate::views::plugins::PluginDataContainer; +use crate::views::plugins::SelectedItem; +use crate::views::plugins::SettingsPluginData; #[derive(Debug, Clone)] pub enum PluginTableMsgIn { TableSyncHeader(scrollable::AbsoluteOffset), SelectItem(SelectedItem), EnabledToggleItem(EnabledItem), - ToggleShowEntrypoints { - plugin_id: PluginId, - }, + ToggleShowEntrypoints { plugin_id: PluginId }, } pub enum PluginTableMsgOut { @@ -37,7 +52,7 @@ pub enum PluginTableMsgOut { SelectItem(SelectedItem), ToggleShowEntrypoints { plugin_id: PluginId, - } + }, } pub struct PluginTableState { @@ -49,7 +64,7 @@ pub struct PluginTableState { pub enum PluginTableUpdateResult { Command(Task<()>), - Value(PluginTableMsgOut) + Value(PluginTableMsgOut), } impl PluginTableState { @@ -70,38 +85,38 @@ impl PluginTableState { pub fn update(&mut self, message: PluginTableMsgIn) -> PluginTableUpdateResult { match message { PluginTableMsgIn::TableSyncHeader(offset) => { - PluginTableUpdateResult::Command( - scrollable::scroll_to(self.header.clone(), offset) - ) + PluginTableUpdateResult::Command(scrollable::scroll_to(self.header.clone(), offset)) } PluginTableMsgIn::EnabledToggleItem(item) => { match item { EnabledItem::Plugin { enabled, plugin_id } => { - PluginTableUpdateResult::Value( - PluginTableMsgOut::SetPluginState { enabled, plugin_id } - ) + PluginTableUpdateResult::Value(PluginTableMsgOut::SetPluginState { enabled, plugin_id }) } - EnabledItem::Entrypoint { enabled, plugin_id, entrypoint_id } => { - PluginTableUpdateResult::Value( - PluginTableMsgOut::SetEntrypointState { enabled, plugin_id, entrypoint_id } - ) + EnabledItem::Entrypoint { + enabled, + plugin_id, + entrypoint_id, + } => { + PluginTableUpdateResult::Value(PluginTableMsgOut::SetEntrypointState { + enabled, + plugin_id, + entrypoint_id, + }) } } } - PluginTableMsgIn::SelectItem(item) => { - PluginTableUpdateResult::Value( - PluginTableMsgOut::SelectItem(item) - ) - }, + PluginTableMsgIn::SelectItem(item) => PluginTableUpdateResult::Value(PluginTableMsgOut::SelectItem(item)), PluginTableMsgIn::ToggleShowEntrypoints { plugin_id } => { - PluginTableUpdateResult::Value( - PluginTableMsgOut::ToggleShowEntrypoints { plugin_id } - ) + PluginTableUpdateResult::Value(PluginTableMsgOut::ToggleShowEntrypoints { plugin_id }) } } } - pub fn apply_plugin_reload(&mut self, plugin_data: Rc>, plugin_refs: Vec<(&SettingsPlugin, &SettingsPluginData)>) { + pub fn apply_plugin_reload( + &mut self, + plugin_data: Rc>, + plugin_refs: Vec<(&SettingsPlugin, &SettingsPluginData)>, + ) { self.rows = plugin_refs .iter() .flat_map(|(plugin, plugin_state)| { @@ -109,14 +124,11 @@ impl PluginTableState { result.push(Row::Plugin { plugin_data: plugin_data.clone(), - plugin_id: plugin.plugin_id.clone() + plugin_id: plugin.plugin_id.clone(), }); if plugin_state.show_entrypoints { - let mut entrypoints: Vec<_> = plugin.entrypoints - .iter() - .map(|(_, entrypoint)| entrypoint) - .collect(); + let mut entrypoints: Vec<_> = plugin.entrypoints.iter().map(|(_, entrypoint)| entrypoint).collect(); entrypoints.sort_by_key(|entrypoint| &entrypoint.entrypoint_name); @@ -140,13 +152,18 @@ impl PluginTableState { } pub fn view(&self) -> Element { - table(self.header.clone(), self.body.clone(), &self.columns, &self.rows, PluginTableMsgIn::TableSyncHeader) - .cell_padding(0.0) - .into() + table( + self.header.clone(), + self.body.clone(), + &self.columns, + &self.rows, + PluginTableMsgIn::TableSyncHeader, + ) + .cell_padding(0.0) + .into() } } - #[derive(Debug, Clone)] enum EnabledItem { Plugin { @@ -163,7 +180,7 @@ enum EnabledItem { enum Row { Plugin { plugin_data: Rc>, - plugin_id: PluginId + plugin_id: PluginId, }, Entrypoint { plugin_data: Rc>, @@ -185,9 +202,7 @@ struct Column { impl Column { fn new(kind: ColumnKind) -> Self { - Self { - kind - } + Self { kind } } } @@ -196,10 +211,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo fn header(&'a self, _col_index: usize) -> Element<'a, PluginTableMsgIn> { match self.kind { - ColumnKind::ShowEntrypointsToggle => { - horizontal_space() - .into() - } + ColumnKind::ShowEntrypointsToggle => horizontal_space().into(), ColumnKind::Name => { container(text("Name")) .height(Length::Fixed(30.0)) @@ -221,12 +233,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo } } - fn cell( - &'a self, - _col_index: usize, - _row_index: usize, - row_entry: &'a Self::Row, - ) -> Element<'a, PluginTableMsgIn> { + fn cell(&'a self, _col_index: usize, _row_index: usize, row_entry: &'a Self::Row) -> Element<'a, PluginTableMsgIn> { match self.kind { ColumnKind::ShowEntrypointsToggle => { match row_entry { @@ -234,24 +241,25 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo let plugin_data = plugin_data.borrow(); let plugin_data = plugin_data.plugins_state.get(&plugin_id).unwrap(); - let icon = if plugin_data.show_entrypoints { Bootstrap::CaretDown } else { Bootstrap::CaretRight }; + let icon = if plugin_data.show_entrypoints { + Bootstrap::CaretDown + } else { + Bootstrap::CaretRight + }; - let icon: Element<_> = value(icon) - .font(BOOTSTRAP_FONT) - .into(); + let icon: Element<_> = value(icon).font(BOOTSTRAP_FONT).into(); button(icon) - .on_press(PluginTableMsgIn::ToggleShowEntrypoints { plugin_id: plugin_id.clone() }) + .on_press(PluginTableMsgIn::ToggleShowEntrypoints { + plugin_id: plugin_id.clone(), + }) .width(Length::Fill) .height(Length::Fixed(40.0)) .padding(8.0) .class(ButtonStyle::TableRow) .into() } - Row::Entrypoint { .. } => { - horizontal_space() - .into() - } + Row::Entrypoint { .. } => horizontal_space().into(), } } ColumnKind::Name => { @@ -260,14 +268,15 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); - let plugin_name = text(plugin.plugin_name.to_string()) - .shaping(Shaping::Advanced); + let plugin_name = text(plugin.plugin_name.to_string()).shaping(Shaping::Advanced); - container(plugin_name) - .align_y(Alignment::Center) - .into() + container(plugin_name).align_y(Alignment::Center).into() } - Row::Entrypoint { plugin_data, plugin_id, entrypoint_id } => { + Row::Entrypoint { + plugin_data, + plugin_id, + entrypoint_id, + } => { let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); let entrypoint = plugin.entrypoints.get(&entrypoint_id).unwrap(); @@ -276,14 +285,9 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo .shaping(Shaping::Advanced) .into(); - let text: Element<_> = row(vec![ - Space::with_width(Length::Fixed(30.0)).into(), - text, - ]).into(); + let text: Element<_> = row(vec![Space::with_width(Length::Fixed(30.0)).into(), text]).into(); - container(text) - .align_y(Alignment::Center) - .into() + container(text).align_y(Alignment::Center).into() } }; @@ -293,10 +297,14 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); SelectedItem::Plugin { - plugin_id: plugin.plugin_id.clone() + plugin_id: plugin.plugin_id.clone(), } - }, - Row::Entrypoint { plugin_data, entrypoint_id, plugin_id } => { + } + Row::Entrypoint { + plugin_data, + entrypoint_id, + plugin_id, + } => { let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); let entrypoint = plugin.entrypoints.get(&entrypoint_id).unwrap(); @@ -318,11 +326,12 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo } ColumnKind::Type => { let content: Element<_> = match row_entry { - Row::Plugin { .. } => { - horizontal_space() - .into() - } - Row::Entrypoint { plugin_data, plugin_id, entrypoint_id } => { + Row::Plugin { .. } => horizontal_space().into(), + Row::Entrypoint { + plugin_data, + plugin_id, + entrypoint_id, + } => { let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); let entrypoint = plugin.entrypoints.get(&entrypoint_id).unwrap(); @@ -331,7 +340,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo SettingsEntrypointType::Command => "Command", SettingsEntrypointType::View => "View", SettingsEntrypointType::InlineView => "Inline View", - SettingsEntrypointType::EntrypointGenerator => "Entrypoint Generator" + SettingsEntrypointType::EntrypointGenerator => "Entrypoint Generator", }; container(text(entrypoint_type.to_string())) @@ -346,10 +355,14 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); SelectedItem::Plugin { - plugin_id: plugin.plugin_id.clone() + plugin_id: plugin.plugin_id.clone(), } - }, - Row::Entrypoint { plugin_data, entrypoint_id, plugin_id } => { + } + Row::Entrypoint { + plugin_data, + entrypoint_id, + plugin_id, + } => { let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); let entrypoint = plugin.entrypoints.get(&entrypoint_id).unwrap(); @@ -375,14 +388,13 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); - ( - plugin.enabled, - true, - plugin.plugin_id.clone(), - None - ) + (plugin.enabled, true, plugin.plugin_id.clone(), None) } - Row::Entrypoint { plugin_data, entrypoint_id, plugin_id } => { + Row::Entrypoint { + plugin_data, + entrypoint_id, + plugin_id, + } => { let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); let entrypoint = plugin.entrypoints.get(&entrypoint_id).unwrap(); @@ -391,7 +403,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo entrypoint.enabled, plugin.enabled, plugin.plugin_id.clone(), - Some(entrypoint.entrypoint_id.clone()) + Some(entrypoint.entrypoint_id.clone()), ) } }; @@ -399,14 +411,18 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo let on_toggle = if show_checkbox { Some(move |enabled| { let enabled_item = match &entrypoint_id { - None => EnabledItem::Plugin { - enabled, - plugin_id: plugin_id.clone(), - }, - Some(entrypoint_id) => EnabledItem::Entrypoint { - enabled, - plugin_id: plugin_id.clone(), - entrypoint_id: entrypoint_id.clone(), + None => { + EnabledItem::Plugin { + enabled, + plugin_id: plugin_id.clone(), + } + } + Some(entrypoint_id) => { + EnabledItem::Entrypoint { + enabled, + plugin_id: plugin_id.clone(), + entrypoint_id: entrypoint_id.clone(), + } } }; PluginTableMsgIn::EnabledToggleItem(enabled_item) @@ -415,9 +431,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo None }; - let checkbox: Element<_> = checkbox("", enabled) - .on_toggle_maybe(on_toggle) - .into(); + let checkbox: Element<_> = checkbox("", enabled).on_toggle_maybe(on_toggle).into(); container(checkbox) .width(Length::Fill) @@ -434,7 +448,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo ColumnKind::ShowEntrypointsToggle => 35.0, ColumnKind::Name => 350.0, ColumnKind::Type => 200.0, - ColumnKind::EnableToggle => 75.0 + ColumnKind::EnableToggle => 75.0, } } diff --git a/rust/plugin_runtime/src/api.rs b/rust/plugin_runtime/src/api.rs index 0a49beb..02fb265 100644 --- a/rust/plugin_runtime/src/api.rs +++ b/rust/plugin_runtime/src/api.rs @@ -1,17 +1,33 @@ -use crate::model::{JsGeneratedSearchItem, JsClipboardData, JsPreferenceUserData}; -use crate::{JsRequest, JsResponse, JsUiRenderLocation}; -use gauntlet_common::model::{EntrypointId, RootWidget, UiRenderLocation}; use std::collections::HashMap; + use anyhow::anyhow; -use gauntlet_utils::channel::{RequestError, RequestSender}; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::RootWidget; +use gauntlet_common::model::UiRenderLocation; +use gauntlet_utils::channel::RequestError; +use gauntlet_utils::channel::RequestSender; + +use crate::model::JsClipboardData; +use crate::model::JsGeneratedSearchItem; +use crate::model::JsPreferenceUserData; +use crate::JsRequest; +use crate::JsResponse; +use crate::JsUiRenderLocation; #[allow(async_fn_in_trait)] pub trait BackendForPluginRuntimeApi { - async fn reload_search_index(&self, generated_entrypoints: Vec, refresh_search_list: bool) -> anyhow::Result<()> ; + async fn reload_search_index( + &self, + generated_entrypoints: Vec, + refresh_search_list: bool, + ) -> anyhow::Result<()>; async fn get_asset_data(&self, path: &str) -> anyhow::Result>; async fn get_entrypoint_generator_entrypoint_ids(&self) -> anyhow::Result>; async fn get_plugin_preferences(&self) -> anyhow::Result>; - async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result>; + async fn get_entrypoint_preferences( + &self, + entrypoint_id: EntrypointId, + ) -> anyhow::Result>; async fn plugin_preferences_required(&self) -> anyhow::Result; async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result; async fn clipboard_read(&self) -> anyhow::Result; @@ -29,7 +45,7 @@ pub trait BackendForPluginRuntimeApi { modifier_shift: bool, modifier_control: bool, modifier_alt: bool, - modifier_meta: bool + modifier_meta: bool, ) -> anyhow::Result>; async fn ui_render( &self, @@ -42,27 +58,25 @@ pub trait BackendForPluginRuntimeApi { async fn ui_show_plugin_error_view( &self, entrypoint_id: EntrypointId, - render_location: UiRenderLocation + render_location: UiRenderLocation, ) -> anyhow::Result<()>; async fn ui_show_preferences_required_view( &self, entrypoint_id: EntrypointId, plugin_preferences_required: bool, - entrypoint_preferences_required: bool + entrypoint_preferences_required: bool, ) -> anyhow::Result<()>; async fn ui_clear_inline_view(&self) -> anyhow::Result<()>; } #[derive(Clone)] pub struct BackendForPluginRuntimeApiProxy { - request_sender: RequestSender> + request_sender: RequestSender>, } impl BackendForPluginRuntimeApiProxy { pub fn new(request_sender: RequestSender>) -> Self { - Self { - request_sender - } + Self { request_sender } } async fn request(&self, request: JsRequest) -> anyhow::Result { @@ -70,8 +84,10 @@ impl BackendForPluginRuntimeApiProxy { Ok(ok) => Ok(ok.map_err(|e| anyhow!(e))?), Err(err) => { match err { - RequestError::TimeoutError => Err(anyhow!("Backend was unable to process message in a timely manner")), - RequestError::OtherSideWasDropped => Err(anyhow!("Plugin runtime is being stopped")) + RequestError::TimeoutError => { + Err(anyhow!("Backend was unable to process message in a timely manner")) + } + RequestError::OtherSideWasDropped => Err(anyhow!("Plugin runtime is being stopped")), } } } @@ -79,7 +95,11 @@ impl BackendForPluginRuntimeApiProxy { } impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { - async fn reload_search_index(&self, generated_entrypoints: Vec, refresh_search_list: bool) -> anyhow::Result<()> { + async fn reload_search_index( + &self, + generated_entrypoints: Vec, + refresh_search_list: bool, + ) -> anyhow::Result<()> { let request = JsRequest::ReloadSearchIndex { generated_entrypoints, refresh_search_list, @@ -87,18 +107,16 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { match self.request(request).await? { JsResponse::Nothing => Ok(()), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } async fn get_asset_data(&self, path: &str) -> anyhow::Result> { - let request = JsRequest::GetAssetData { - path: path.to_string(), - }; + let request = JsRequest::GetAssetData { path: path.to_string() }; match self.request(request).await? { JsResponse::AssetData { data } => Ok(data), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } @@ -107,7 +125,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { match self.request(request).await? { JsResponse::EntrypointGeneratorEntrypointIds { data } => Ok(data), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } @@ -116,18 +134,19 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { match self.request(request).await? { JsResponse::PluginPreferences { data } => Ok(data), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } - async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result> { - let request = JsRequest::GetEntrypointPreferences { - entrypoint_id, - }; + async fn get_entrypoint_preferences( + &self, + entrypoint_id: EntrypointId, + ) -> anyhow::Result> { + let request = JsRequest::GetEntrypointPreferences { entrypoint_id }; match self.request(request).await? { JsResponse::EntrypointPreferences { data } => Ok(data), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } @@ -136,18 +155,16 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { match self.request(request).await? { JsResponse::PluginPreferencesRequired { data } => Ok(data), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result { - let request = JsRequest::EntrypointPreferencesRequired { - entrypoint_id, - }; + let request = JsRequest::EntrypointPreferencesRequired { entrypoint_id }; match self.request(request).await? { JsResponse::EntrypointPreferencesRequired { data } => Ok(data), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } @@ -156,7 +173,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { match self.request(request).await? { JsResponse::ClipboardRead { data } => Ok(data), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } @@ -165,29 +182,25 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { match self.request(request).await? { JsResponse::ClipboardReadText { data } => Ok(data), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } async fn clipboard_write(&self, data: JsClipboardData) -> anyhow::Result<()> { - let request = JsRequest::ClipboardWrite { - data - }; + let request = JsRequest::ClipboardWrite { data }; match self.request(request).await? { JsResponse::Nothing => Ok(()), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } async fn clipboard_write_text(&self, data: String) -> anyhow::Result<()> { - let request = JsRequest::ClipboardWriteText { - data, - }; + let request = JsRequest::ClipboardWriteText { data }; match self.request(request).await? { JsResponse::Nothing => Ok(()), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } @@ -196,30 +209,25 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { match self.request(request).await? { JsResponse::Nothing => Ok(()), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } async fn ui_update_loading_bar(&self, entrypoint_id: EntrypointId, show: bool) -> anyhow::Result<()> { - let request = JsRequest::UpdateLoadingBar { - entrypoint_id, - show, - }; + let request = JsRequest::UpdateLoadingBar { entrypoint_id, show }; match self.request(request).await? { JsResponse::Nothing => Ok(()), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } async fn ui_show_hud(&self, display: String) -> anyhow::Result<()> { - let request = JsRequest::ShowHud { - display, - }; + let request = JsRequest::ShowHud { display }; match self.request(request).await? { JsResponse::Nothing => Ok(()), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } @@ -228,11 +236,19 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { match self.request(request).await? { JsResponse::Nothing => Ok(()), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } - async fn ui_get_action_id_for_shortcut(&self, entrypoint_id: EntrypointId, key: String, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) -> anyhow::Result> { + async fn ui_get_action_id_for_shortcut( + &self, + entrypoint_id: EntrypointId, + key: String, + modifier_shift: bool, + modifier_control: bool, + modifier_alt: bool, + modifier_meta: bool, + ) -> anyhow::Result> { let request = JsRequest::GetActionIdForShortcut { entrypoint_id, key, @@ -244,7 +260,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { match self.request(request).await? { JsResponse::ActionIdForShortcut { data } => Ok(data), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } @@ -261,7 +277,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { entrypoint_name, render_location: match render_location { UiRenderLocation::InlineView => JsUiRenderLocation::InlineView, - UiRenderLocation::View => JsUiRenderLocation::View + UiRenderLocation::View => JsUiRenderLocation::View, }, top_level_view, container, @@ -269,26 +285,35 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { match self.request(request).await? { JsResponse::Nothing => Ok(()), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } - async fn ui_show_plugin_error_view(&self, entrypoint_id: EntrypointId, render_location: UiRenderLocation) -> anyhow::Result<()> { + async fn ui_show_plugin_error_view( + &self, + entrypoint_id: EntrypointId, + render_location: UiRenderLocation, + ) -> anyhow::Result<()> { let request = JsRequest::ShowPluginErrorView { entrypoint_id, render_location: match render_location { UiRenderLocation::InlineView => JsUiRenderLocation::InlineView, - UiRenderLocation::View => JsUiRenderLocation::View + UiRenderLocation::View => JsUiRenderLocation::View, }, }; match self.request(request).await? { JsResponse::Nothing => Ok(()), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } - async fn ui_show_preferences_required_view(&self, entrypoint_id: EntrypointId, plugin_preferences_required: bool, entrypoint_preferences_required: bool) -> anyhow::Result<()> { + async fn ui_show_preferences_required_view( + &self, + entrypoint_id: EntrypointId, + plugin_preferences_required: bool, + entrypoint_preferences_required: bool, + ) -> anyhow::Result<()> { let request = JsRequest::ShowPreferenceRequiredView { entrypoint_id, plugin_preferences_required, @@ -297,7 +322,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { match self.request(request).await? { JsResponse::Nothing => Ok(()), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } @@ -306,7 +331,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { match self.request(request).await? { JsResponse::Nothing => Ok(()), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value) + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } -} \ No newline at end of file +} diff --git a/rust/plugin_runtime/src/assets.rs b/rust/plugin_runtime/src/assets.rs index b2aac4e..cfa8aee 100644 --- a/rust/plugin_runtime/src/assets.rs +++ b/rust/plugin_runtime/src/assets.rs @@ -1,9 +1,13 @@ -use deno_core::futures::executor::block_on; -use deno_core::{op2, OpState}; use std::cell::RefCell; use std::rc::Rc; + +use deno_core::futures::executor::block_on; +use deno_core::op2; +use deno_core::OpState; use tokio::runtime::Handle; -use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; + +use crate::api::BackendForPluginRuntimeApi; +use crate::api::BackendForPluginRuntimeApiProxy; #[op2(async)] #[buffer] @@ -11,9 +15,7 @@ pub async fn asset_data(state: Rc>, #[string] path: String) -> let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; @@ -29,13 +31,9 @@ pub fn asset_data_blocking(state: Rc>, #[string] path: String) let (api, outer_handle) = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); - let outer_handle = state - .borrow::() - .clone(); + let outer_handle = state.borrow::().clone(); (api, outer_handle) }; @@ -47,4 +45,4 @@ pub fn asset_data_blocking(state: Rc>, #[string] path: String) Ok(data) }) -} \ No newline at end of file +} diff --git a/rust/plugin_runtime/src/clipboard.rs b/rust/plugin_runtime/src/clipboard.rs index 25995c4..22e8298 100644 --- a/rust/plugin_runtime/src/clipboard.rs +++ b/rust/plugin_runtime/src/clipboard.rs @@ -1,14 +1,19 @@ -use deno_core::{op2, OpState}; -use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::rc::Rc; -use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; + +use deno_core::op2; +use deno_core::OpState; +use serde::Deserialize; +use serde::Serialize; + +use crate::api::BackendForPluginRuntimeApi; +use crate::api::BackendForPluginRuntimeApiProxy; use crate::model::JsClipboardData; #[derive(Debug, Serialize, Deserialize)] struct JSClipboardData { text_data: Option, - png_data: Option> + png_data: Option>, } #[op2(async)] @@ -17,9 +22,7 @@ pub async fn clipboard_read(state: Rc>) -> anyhow::Result() - .clone(); + let api = state.borrow::().clone(); api }; @@ -32,16 +35,13 @@ pub async fn clipboard_read(state: Rc>) -> anyhow::Result>) -> anyhow::Result> { let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; @@ -54,9 +54,7 @@ pub async fn clipboard_write(state: Rc>, #[serde] data: JSClipb let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; @@ -74,9 +72,7 @@ pub async fn clipboard_write_text(state: Rc>, #[string] data: S let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; @@ -89,9 +85,7 @@ pub async fn clipboard_clear(state: Rc>) -> anyhow::Result<()> let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; diff --git a/rust/plugin_runtime/src/component_model.rs b/rust/plugin_runtime/src/component_model.rs index 6ea6668..e4134f7 100644 --- a/rust/plugin_runtime/src/component_model.rs +++ b/rust/plugin_runtime/src/component_model.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; -use gauntlet_component_model::{create_component_model, Component}; + +use gauntlet_component_model::create_component_model; +use gauntlet_component_model::Component; pub struct ComponentModel { components: HashMap, @@ -11,19 +13,21 @@ impl ComponentModel { .into_iter() .filter_map(|component| { match &component { - Component::Standard { internal_name, .. } => Some((format!("gauntlet:{}", internal_name), component)), + Component::Standard { internal_name, .. } => { + Some((format!("gauntlet:{}", internal_name), component)) + } Component::Root { internal_name, .. } => Some((format!("gauntlet:{}", internal_name), component)), - Component::TextPart { internal_name, .. } => Some((format!("gauntlet:{}", internal_name), component)), + Component::TextPart { internal_name, .. } => { + Some((format!("gauntlet:{}", internal_name), component)) + } } }) .collect(); - Self { - components - } + Self { components } } pub fn components(&self) -> &HashMap { &self.components } -} \ No newline at end of file +} diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index 5120fd4..5e46983 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -1,42 +1,87 @@ use std::collections::HashMap; use std::fs::File; -use std::path::{Path, PathBuf}; +use std::path::Path; +use std::path::PathBuf; use std::pin::Pin; use std::rc::Rc; use std::sync::Arc; -use anyhow::{anyhow, Context}; -use deno_core::{FastString, ModuleLoadResponse, ModuleLoader, ModuleSource, ModuleSourceCode, ModuleSpecifier, ModuleType, RequestedModuleType, ResolutionKind, StaticModuleLoader}; + +use anyhow::anyhow; +use anyhow::Context; use deno_core::futures::Stream; use deno_core::url::Url; +use deno_core::FastString; +use deno_core::ModuleLoadResponse; +use deno_core::ModuleLoader; +use deno_core::ModuleSource; +use deno_core::ModuleSourceCode; +use deno_core::ModuleSpecifier; +use deno_core::ModuleType; +use deno_core::RequestedModuleType; +use deno_core::ResolutionKind; +use deno_core::StaticModuleLoader; +use deno_runtime::deno_fs::FileSystem; +use deno_runtime::deno_fs::RealFs; +use deno_runtime::deno_io::Stdio; +use deno_runtime::deno_io::StdioPipe; +use deno_runtime::worker::MainWorker; +use deno_runtime::worker::WorkerOptions; +use deno_runtime::worker::WorkerServiceOptions; use deno_runtime::BootstrapOptions; -use deno_runtime::deno_fs::{FileSystem, RealFs}; -use deno_runtime::deno_io::{Stdio, StdioPipe}; -use deno_runtime::worker::{MainWorker, WorkerOptions, WorkerServiceOptions}; +use gauntlet_common::model::PluginId; use once_cell::sync::Lazy; use regex::Regex; use tokio::runtime::Handle; use tokio::sync::mpsc::Receiver; -use gauntlet_common::model::PluginId; + use crate::api::BackendForPluginRuntimeApiProxy; -use crate::assets::{asset_data, asset_data_blocking}; -use crate::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; -use crate::entrypoint_generators::get_entrypoint_generator_entrypoint_ids; +use crate::assets::asset_data; +use crate::assets::asset_data_blocking; +use crate::clipboard::clipboard_clear; +use crate::clipboard::clipboard_read; +use crate::clipboard::clipboard_read_text; +use crate::clipboard::clipboard_write; +use crate::clipboard::clipboard_write_text; use crate::component_model::ComponentModel; -use crate::environment::{environment_gauntlet_version, environment_is_development, environment_plugin_cache_dir, environment_plugin_data_dir}; -use crate::events::{op_plugin_get_pending_event, EventReceiver, JsEvent}; -use crate::JsPluginCode; -use crate::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; +use crate::entrypoint_generators::get_entrypoint_generator_entrypoint_ids; +use crate::environment::environment_gauntlet_version; +use crate::environment::environment_is_development; +use crate::environment::environment_plugin_cache_dir; +use crate::environment::environment_plugin_data_dir; +use crate::events::op_plugin_get_pending_event; +use crate::events::EventReceiver; +use crate::events::JsEvent; +use crate::logs::op_log_debug; +use crate::logs::op_log_error; +use crate::logs::op_log_info; +use crate::logs::op_log_trace; +use crate::logs::op_log_warn; use crate::model::JsInit; -use crate::permissions::{permissions_to_deno}; +use crate::permissions::permissions_to_deno; use crate::plugin_data::PluginData; -use crate::plugins::applications::{current_os, wayland, ApplicationContext}; -use crate::plugins::numbat::{run_numbat, NumbatContext}; +use crate::plugins::applications::current_os; +use crate::plugins::applications::wayland; +use crate::plugins::applications::ApplicationContext; +use crate::plugins::numbat::run_numbat; +use crate::plugins::numbat::NumbatContext; use crate::plugins::settings::open_settings; -use crate::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; +use crate::preferences::entrypoint_preferences_required; +use crate::preferences::get_entrypoint_preferences; +use crate::preferences::get_plugin_preferences; +use crate::preferences::plugin_preferences_required; use crate::search::reload_search_index; -use crate::ui::{clear_inline_view, fetch_action_id_for_shortcut, hide_window, op_component_model, op_entrypoint_names, op_inline_view_entrypoint_id, op_react_replace_view, show_hud, show_plugin_error_view, show_preferences_required_view, update_loading_bar}; - - +use crate::ui::clear_inline_view; +use crate::ui::fetch_action_id_for_shortcut; +use crate::ui::hide_window; +use crate::ui::op_component_model; +use crate::ui::op_entrypoint_names; +use crate::ui::op_inline_view_entrypoint_id; +use crate::ui::op_react_replace_view; +use crate::ui::show_hud; +use crate::ui::show_plugin_error_view; +use crate::ui::show_preferences_required_view; +use crate::ui::update_loading_bar; +use crate::JsPluginCode; pub struct CustomModuleLoader { code: JsPluginCode, @@ -46,29 +91,98 @@ pub struct CustomModuleLoader { impl CustomModuleLoader { fn new(code: JsPluginCode, dev_plugin: bool) -> Self { - let module_map: HashMap<_, _> = MODULES.iter() - .map(|(key, value)| (key.parse().expect("provided key is not valid url"), FastString::from_static(value))) + let module_map: HashMap<_, _> = MODULES + .iter() + .map(|(key, value)| { + ( + key.parse().expect("provided key is not valid url"), + FastString::from_static(value), + ) + }) .collect(); Self { code, static_loader: StaticModuleLoader::new(module_map), - dev_plugin + dev_plugin, } } } const MODULES: [(&str, &str); 11] = [ - ("gauntlet:init", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/core/dist/init.js"))), - ("gauntlet:bridge/components", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-components.js"))), - ("gauntlet:bridge/hooks", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-hooks.js"))), - ("gauntlet:bridge/helpers", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-helpers.js"))), - ("gauntlet:bridge/core", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-core.js"))), - ("gauntlet:bridge/react", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-react.js"))), - ("gauntlet:bridge/react-jsx-runtime", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-react-jsx-runtime.js"))), - ("gauntlet:bridge/internal-all", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-all.js"))), - ("gauntlet:bridge/internal-linux", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-linux.js"))), - ("gauntlet:bridge/internal-macos", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-macos.js"))), - ("gauntlet:bridge/internal-windows", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/bridge_build/dist/bridge-internal-windows.js"))), + ( + "gauntlet:init", + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js/core/dist/init.js")), + ), + ( + "gauntlet:bridge/components", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../js/bridge_build/dist/bridge-components.js" + )), + ), + ( + "gauntlet:bridge/hooks", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../js/bridge_build/dist/bridge-hooks.js" + )), + ), + ( + "gauntlet:bridge/helpers", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../js/bridge_build/dist/bridge-helpers.js" + )), + ), + ( + "gauntlet:bridge/core", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../js/bridge_build/dist/bridge-core.js" + )), + ), + ( + "gauntlet:bridge/react", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../js/bridge_build/dist/bridge-react.js" + )), + ), + ( + "gauntlet:bridge/react-jsx-runtime", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../js/bridge_build/dist/bridge-react-jsx-runtime.js" + )), + ), + ( + "gauntlet:bridge/internal-all", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../js/bridge_build/dist/bridge-internal-all.js" + )), + ), + ( + "gauntlet:bridge/internal-linux", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../js/bridge_build/dist/bridge-internal-linux.js" + )), + ), + ( + "gauntlet:bridge/internal-macos", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../js/bridge_build/dist/bridge-internal-macos.js" + )), + ), + ( + "gauntlet:bridge/internal-windows", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../js/bridge_build/dist/bridge-internal-windows.js" + )), + ), ]; impl ModuleLoader for CustomModuleLoader { @@ -78,9 +192,13 @@ impl ModuleLoader for CustomModuleLoader { referrer: &str, _kind: ResolutionKind, ) -> Result { - static PLUGIN_ENTRYPOINT_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^gauntlet:entrypoint\?(?[a-zA-Z0-9_-]+)$").expect("invalid regex")); - static PLUGIN_MODULE_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^gauntlet:module\?(?[a-zA-Z0-9_-]+)$").expect("invalid regex")); - static PATH_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^\./(?[a-zA-Z0-9_-]+)\.js$").expect("invalid regex")); + static PLUGIN_ENTRYPOINT_PATTERN: Lazy = Lazy::new(|| { + Regex::new(r"^gauntlet:entrypoint\?(?[a-zA-Z0-9_-]+)$").expect("invalid regex") + }); + static PLUGIN_MODULE_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"^gauntlet:module\?(?[a-zA-Z0-9_-]+)$").expect("invalid regex")); + static PATH_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"^\./(?[a-zA-Z0-9_-]+)\.js$").expect("invalid regex")); if PLUGIN_ENTRYPOINT_PATTERN.is_match(specifier) { return Ok(specifier.parse()?); @@ -105,7 +223,11 @@ impl ModuleLoader for CustomModuleLoader { ("@project-gauntlet/api/hooks", _) => "gauntlet:bridge/hooks", ("@project-gauntlet/api/helpers", _) => "gauntlet:bridge/helpers", _ => { - return Err(anyhow!("Illegal import with specifier '{}' and referrer '{}'", specifier, referrer)) + return Err(anyhow!( + "Illegal import with specifier '{}' and referrer '{}'", + specifier, + referrer + )) } }; @@ -119,21 +241,21 @@ impl ModuleLoader for CustomModuleLoader { is_dyn_import: bool, requested_module_type: RequestedModuleType, ) -> ModuleLoadResponse { - let mut specifier = module_specifier.clone(); specifier.set_query(None); match specifier.as_str() { "gauntlet:init" => { - self.static_loader.load(module_specifier, maybe_referrer, is_dyn_import, requested_module_type) + self.static_loader + .load(module_specifier, maybe_referrer, is_dyn_import, requested_module_type) } "gauntlet:entrypoint" | "gauntlet:module" => { match module_specifier.query() { - None => { - ModuleLoadResponse::Sync(Err(anyhow!("Module specifier doesn't have query part"))) - }, + None => ModuleLoadResponse::Sync(Err(anyhow!("Module specifier doesn't have query part"))), Some(entrypoint_id) => { - let result = self.code.js + let result = self + .code + .js .get(entrypoint_id) .ok_or(anyhow!("Cannot find JS code path: {:?}", entrypoint_id)) .map(|js| ModuleSourceCode::String(js.clone().into())) @@ -144,10 +266,15 @@ impl ModuleLoader for CustomModuleLoader { } } _ => { - if specifier.as_str().starts_with("gauntlet:bridge/"){ - self.static_loader.load(module_specifier, maybe_referrer, is_dyn_import, requested_module_type) + if specifier.as_str().starts_with("gauntlet:bridge/") { + self.static_loader + .load(module_specifier, maybe_referrer, is_dyn_import, requested_module_type) } else { - ModuleLoadResponse::Sync(Err(anyhow!("Module not found: specifier '{}' and referrer '{:?}'", specifier, maybe_referrer.map(|url| url.as_str())))) + ModuleLoadResponse::Sync(Err(anyhow!( + "Module not found: specifier '{}' and referrer '{:?}'", + specifier, + maybe_referrer.map(|url| url.as_str()) + ))) } } } @@ -230,14 +357,14 @@ mod prod { gauntlet_esm, esm_entry_point = "ext:gauntlet/bootstrap.js", esm = [ - "ext:gauntlet/bootstrap.js" = "../../js/bridge_build/dist/bridge-bootstrap.js", - "ext:gauntlet/core.js" = "../../js/core/dist/core.js", - "ext:gauntlet/api/components.js" = "../../js/api/dist/gen/components.js", - "ext:gauntlet/api/hooks.js" = "../../js/api/dist/hooks.js", - "ext:gauntlet/api/helpers.js" = "../../js/api/dist/helpers.js", - "ext:gauntlet/renderer.js" = "../../js/react_renderer/dist/prod/renderer.js", - "ext:gauntlet/react.js" = "../../js/react/dist/prod/react.production.min.js", - "ext:gauntlet/react-jsx-runtime.js" = "../../js/react/dist/prod/react-jsx-runtime.production.min.js", + "ext:gauntlet/bootstrap.js" = "../../js/bridge_build/dist/bridge-bootstrap.js", + "ext:gauntlet/core.js" = "../../js/core/dist/core.js", + "ext:gauntlet/api/components.js" = "../../js/api/dist/gen/components.js", + "ext:gauntlet/api/hooks.js" = "../../js/api/dist/hooks.js", + "ext:gauntlet/api/helpers.js" = "../../js/api/dist/helpers.js", + "ext:gauntlet/renderer.js" = "../../js/react_renderer/dist/prod/renderer.js", + "ext:gauntlet/react.js" = "../../js/react/dist/prod/react.production.min.js", + "ext:gauntlet/react-jsx-runtime.js" = "../../js/react/dist/prod/react-jsx-runtime.production.min.js", ], ); } @@ -248,14 +375,14 @@ mod dev { gauntlet_esm, esm_entry_point = "ext:gauntlet/bootstrap.js", esm = [ - "ext:gauntlet/bootstrap.js" = "../../js/bridge_build/dist/bridge-bootstrap.js", - "ext:gauntlet/core.js" = "../../js/core/dist/core.js", - "ext:gauntlet/api/components.js" = "../../js/api/dist/gen/components.js", - "ext:gauntlet/api/hooks.js" = "../../js/api/dist/hooks.js", - "ext:gauntlet/api/helpers.js" = "../../js/api/dist/helpers.js", - "ext:gauntlet/renderer.js" = "../../js/react_renderer/dist/dev/renderer.js", - "ext:gauntlet/react.js" = "../../js/react/dist/dev/react.development.js", - "ext:gauntlet/react-jsx-runtime.js" = "../../js/react/dist/dev/react-jsx-runtime.development.js", + "ext:gauntlet/bootstrap.js" = "../../js/bridge_build/dist/bridge-bootstrap.js", + "ext:gauntlet/core.js" = "../../js/core/dist/core.js", + "ext:gauntlet/api/components.js" = "../../js/api/dist/gen/components.js", + "ext:gauntlet/api/hooks.js" = "../../js/api/dist/hooks.js", + "ext:gauntlet/api/helpers.js" = "../../js/api/dist/helpers.js", + "ext:gauntlet/renderer.js" = "../../js/react_renderer/dist/dev/renderer.js", + "ext:gauntlet/react.js" = "../../js/react/dist/dev/react.development.js", + "ext:gauntlet/react-jsx-runtime.js" = "../../js/react/dist/dev/react-jsx-runtime.development.js", ], ); } @@ -306,19 +433,17 @@ deno_core::extension!( ], esm_entry_point = "ext:gauntlet/internal-macos/bootstrap.js", esm = [ - "ext:gauntlet/internal-macos/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-macos-bootstrap.js", - "ext:gauntlet/internal-macos.js" = "../../js/core/dist/internal-macos.js", + "ext:gauntlet/internal-macos/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-macos-bootstrap.js", + "ext:gauntlet/internal-macos.js" = "../../js/core/dist/internal-macos.js", ] ); - pub async fn start_js_runtime( outer_handle: Handle, init: JsInit, event_stream: Receiver, api: BackendForPluginRuntimeApiProxy, ) -> anyhow::Result<()> { - let stdout = if let Some(stdout_file) = init.stdout_file { let stdout_file = PathBuf::from(stdout_file); @@ -357,11 +482,9 @@ pub async fn start_js_runtime( #[cfg(windows)] let stdin = StdioPipe::file(File::options().read(true).open("nul")?); - std::fs::create_dir_all(&init.plugin_cache_dir) - .context("Unable to create plugin cache directory")?; + std::fs::create_dir_all(&init.plugin_cache_dir).context("Unable to create plugin cache directory")?; - std::fs::create_dir_all(&init.plugin_data_dir) - .context("Unable to create plugin data directory")?; + std::fs::create_dir_all(&init.plugin_data_dir).context("Unable to create plugin data directory")?; let init_url: ModuleSpecifier = "gauntlet:init".parse().expect("should be valid"); @@ -393,11 +516,11 @@ pub async fn start_js_runtime( init.plugin_data_dir, init.inline_view_entrypoint_id, init.entrypoint_names, - home_dir + home_dir, ), ComponentModel::new(), api, - outer_handle + outer_handle, ), gauntlet_esm, ]; @@ -405,7 +528,7 @@ pub async fn start_js_runtime( if init.plugin_id.to_string() == "bundled://gauntlet" { extensions.push(gauntlet_internal_all::init_ops_and_esm( NumbatContext::new(), - ApplicationContext::new()? + ApplicationContext::new()?, )); #[cfg(target_os = "macos")] @@ -446,11 +569,7 @@ pub async fn start_js_runtime( should_wait_for_inspector_session: false, should_break_on_first_statement: false, origin_storage_dir: Some(PathBuf::from(init.local_storage_dir)), - stdio: Stdio { - stdin, - stdout, - stderr, - }, + stdio: Stdio { stdin, stdout, stderr }, ..Default::default() }, ); @@ -460,4 +579,3 @@ pub async fn start_js_runtime( Ok(()) } - diff --git a/rust/plugin_runtime/src/entrypoint_generators.rs b/rust/plugin_runtime/src/entrypoint_generators.rs index 70bb2d3..ac39971 100644 --- a/rust/plugin_runtime/src/entrypoint_generators.rs +++ b/rust/plugin_runtime/src/entrypoint_generators.rs @@ -1,7 +1,11 @@ -use deno_core::{op2, OpState}; use std::cell::RefCell; use std::rc::Rc; -use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; + +use deno_core::op2; +use deno_core::OpState; + +use crate::api::BackendForPluginRuntimeApi; +use crate::api::BackendForPluginRuntimeApiProxy; #[op2(async)] #[serde] @@ -9,9 +13,7 @@ pub async fn get_entrypoint_generator_entrypoint_ids(state: Rc> let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; diff --git a/rust/plugin_runtime/src/environment.rs b/rust/plugin_runtime/src/environment.rs index a1349a3..ca499e6 100644 --- a/rust/plugin_runtime/src/environment.rs +++ b/rust/plugin_runtime/src/environment.rs @@ -1,4 +1,6 @@ -use deno_core::{op2, OpState}; +use deno_core::op2; +use deno_core::OpState; + use crate::plugin_data::PluginData; #[op2(fast)] @@ -10,29 +12,19 @@ pub fn environment_gauntlet_version() -> u16 { #[op2(fast)] pub fn environment_is_development(state: &mut OpState) -> bool { - let plugin_id = state - .borrow::() - .plugin_id(); + let plugin_id = state.borrow::().plugin_id(); - plugin_id - .to_string() - .starts_with("file://") + plugin_id.to_string().starts_with("file://") } #[op2] #[string] pub fn environment_plugin_data_dir(state: &mut OpState) -> String { - state - .borrow::() - .plugin_data_dir() - .to_string() + state.borrow::().plugin_data_dir().to_string() } #[op2] #[string] pub fn environment_plugin_cache_dir(state: &mut OpState) -> String { - state - .borrow::() - .plugin_cache_dir() - .to_string() -} \ No newline at end of file + state.borrow::().plugin_cache_dir().to_string() +} diff --git a/rust/plugin_runtime/src/events.rs b/rust/plugin_runtime/src/events.rs index cf7d0d8..4dc4ece 100644 --- a/rust/plugin_runtime/src/events.rs +++ b/rust/plugin_runtime/src/events.rs @@ -1,31 +1,36 @@ use std::cell::RefCell; use std::pin::Pin; use std::rc::Rc; + use anyhow::anyhow; -use bincode::{Decode, Encode}; -use deno_core::{op2, OpState}; -use deno_core::futures::{Stream, StreamExt}; -use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc::Receiver; +use bincode::Decode; +use bincode::Encode; +use deno_core::futures::Stream; +use deno_core::futures::StreamExt; +use deno_core::op2; +use deno_core::OpState; use gauntlet_common::model::UiWidgetId; +use serde::Deserialize; +use serde::Serialize; +use tokio::sync::mpsc::Receiver; #[derive(Debug, Deserialize, Serialize, Encode, Decode)] #[serde(tag = "type")] pub enum JsEvent { OpenView { #[serde(rename = "entrypointId")] - entrypoint_id: String + entrypoint_id: String, }, CloseView, RunCommand { #[serde(rename = "entrypointId")] - entrypoint_id: String + entrypoint_id: String, }, RunGeneratedEntrypoint { #[serde(rename = "entrypointId")] entrypoint_id: String, #[serde(rename = "actionIndex")] - action_index: usize + action_index: usize, }, ViewEvent { #[serde(rename = "widgetId")] @@ -47,7 +52,7 @@ pub enum JsEvent { #[serde(rename = "modifierAlt")] modifier_alt: bool, #[serde(rename = "modifierMeta")] - modifier_meta: bool + modifier_meta: bool, }, OpenInlineView { #[serde(rename = "text")] @@ -66,15 +71,9 @@ pub enum JsKeyboardEventOrigin { #[derive(Debug, Deserialize, Serialize, Encode, Decode)] #[serde(tag = "type")] pub enum JsUiPropertyValue { - String { - value: String - }, - Number { - value: f64 - }, - Bool { - value: bool - }, + String { value: String }, + Number { value: f64 }, + Bool { value: bool }, Undefined, } @@ -93,15 +92,11 @@ impl EventReceiver { #[op2(async)] #[serde] pub async fn op_plugin_get_pending_event(state: Rc>) -> anyhow::Result { - let event_stream = { - state.borrow() - .borrow::() - .event_stream - .clone() - }; + let event_stream = { state.borrow().borrow::().event_stream.clone() }; let mut event_stream = event_stream.borrow_mut(); - let event = event_stream.recv() + let event = event_stream + .recv() .await .ok_or_else(|| anyhow!("event stream was suddenly closed"))?; @@ -109,4 +104,3 @@ pub async fn op_plugin_get_pending_event(state: Rc>) -> anyhow: Ok(event) } - diff --git a/rust/plugin_runtime/src/lib.rs b/rust/plugin_runtime/src/lib.rs index c3c63c2..5dfbf3c 100644 --- a/rust/plugin_runtime/src/lib.rs +++ b/rust/plugin_runtime/src/lib.rs @@ -1,9 +1,9 @@ mod api; mod assets; mod clipboard; -mod entrypoint_generators; mod component_model; mod deno; +mod entrypoint_generators; mod environment; mod events; mod logs; @@ -15,38 +15,56 @@ mod preferences; mod search; mod ui; -use crate::api::BackendForPluginRuntimeApiProxy; -use crate::deno::start_js_runtime; -use anyhow::{anyhow, Context}; -use bincode::{Decode, Encode}; -use deno_core::futures::SinkExt; -use interprocess::local_socket::tokio::prelude::*; -use interprocess::local_socket::tokio::{RecvHalf, SendHalf, Stream}; -use interprocess::local_socket::{GenericFilePath, NameType, ToNsName}; -use once_cell::sync::Lazy; -use regex::Regex; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use std::cell::{RefCell, RefMut}; +use std::cell::RefCell; +use std::cell::RefMut; use std::convert; use std::fmt::Debug; -use std::ops::{Deref, DerefMut}; -use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; +use std::ops::Deref; +use std::ops::DerefMut; +use std::sync::atomic::AtomicU32; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; use std::sync::Arc; -use tokio::io::AsyncWriteExt; -use tokio::io::{AsyncBufReadExt, AsyncReadExt}; -use tokio::runtime::Handle; -use tokio::sync::mpsc::{channel, Receiver, Sender}; -use tokio::sync::{oneshot, Mutex, MutexGuard}; -use tokio_util::sync::CancellationToken; -use gauntlet_utils::channel::{Payload, RequestReceiver}; +use anyhow::anyhow; +use anyhow::Context; pub use api::BackendForPluginRuntimeApi; +use bincode::Decode; +use bincode::Encode; +use deno_core::futures::SinkExt; pub use events::JsEvent; pub use events::JsKeyboardEventOrigin; pub use events::JsUiPropertyValue; +use gauntlet_utils::channel::Payload; +use gauntlet_utils::channel::RequestReceiver; +use interprocess::local_socket::tokio::prelude::*; +use interprocess::local_socket::tokio::RecvHalf; +use interprocess::local_socket::tokio::SendHalf; +use interprocess::local_socket::tokio::Stream; +use interprocess::local_socket::GenericFilePath; +use interprocess::local_socket::NameType; +use interprocess::local_socket::ToNsName; pub use model::*; +use once_cell::sync::Lazy; pub use permissions::PERMISSIONS_VARIABLE_PATTERN; +use regex::Regex; +use serde::de::DeserializeOwned; +use serde::Deserialize; +use serde::Serialize; +use tokio::io::AsyncBufReadExt; +use tokio::io::AsyncReadExt; +use tokio::io::AsyncWriteExt; +use tokio::runtime::Handle; +use tokio::sync::mpsc::channel; +use tokio::sync::mpsc::Receiver; +use tokio::sync::mpsc::Sender; +use tokio::sync::oneshot; +use tokio::sync::Mutex; +use tokio::sync::MutexGuard; +use tokio_util::sync::CancellationToken; + +use crate::api::BackendForPluginRuntimeApiProxy; +use crate::deno::start_js_runtime; pub fn run_plugin_runtime(socket_name: String) { tokio::runtime::Builder::new_current_thread() @@ -66,14 +84,14 @@ async fn run_outer(socket_name: String) -> anyhow::Result<()> { let name = socket_name.to_ns_name::()?; #[cfg(unix)] - let name = socket_name - .to_fs_name::()?; + let name = socket_name.to_fs_name::()?; let conn = Stream::connect(name).await?; let (mut recver, mut sender) = conn.split(); - let (request_sender, mut request_receiver) = gauntlet_utils::channel::channel::>(); + let (request_sender, mut request_receiver) = + gauntlet_utils::channel::channel::>(); let (event_sender, event_receiver) = channel::(10); let response_oneshot = Mutex::new(None); @@ -120,7 +138,12 @@ async fn run_outer(socket_name: String) -> anyhow::Result<()> { } } - send_message(JsMessageSide::PluginRuntime, &mut sender, JsPluginRuntimeMessage::Stopped).await?; + send_message( + JsMessageSide::PluginRuntime, + &mut sender, + JsPluginRuntimeMessage::Stopped, + ) + .await?; tracing::debug!("Plugin runtime outer loop has been stopped {:?}", plugin_id); @@ -129,19 +152,32 @@ async fn run_outer(socket_name: String) -> anyhow::Result<()> { Ok(()) } -async fn run_new_tokio(outer_handle: Handle, stop_token: CancellationToken, init: JsInit, event_receiver: Receiver, api: BackendForPluginRuntimeApiProxy) -> anyhow::Result<()> { +async fn run_new_tokio( + outer_handle: Handle, + stop_token: CancellationToken, + init: JsInit, + event_receiver: Receiver, + api: BackendForPluginRuntimeApiProxy, +) -> anyhow::Result<()> { tokio::task::spawn_blocking(|| { tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("unable to start tokio runtime for plugin") .block_on(run(outer_handle, stop_token, init, event_receiver, api)) - }).await??; + }) + .await??; Ok(()) } -async fn run(outer_handle: Handle, stop_token: CancellationToken, init: JsInit, event_receiver: Receiver, api: BackendForPluginRuntimeApiProxy) -> anyhow::Result<()> { +async fn run( + outer_handle: Handle, + stop_token: CancellationToken, + init: JsInit, + event_receiver: Receiver, + api: BackendForPluginRuntimeApiProxy, +) -> anyhow::Result<()> { let plugin_id = init.plugin_id.clone(); tokio::select! { @@ -167,7 +203,7 @@ async fn run(outer_handle: Handle, stop_token: CancellationToken, init: JsInit, async fn request_loop( send: &mut SendHalf, request_receiver: &mut RequestReceiver>, - response_oneshot: &Mutex>>> + response_oneshot: &Mutex>>>, ) -> anyhow::Result<()> { let (request, responder) = request_receiver.recv().await; @@ -177,7 +213,9 @@ async fn request_loop( let mut response_oneshot = response_oneshot.lock().await; let None = response_oneshot.deref() else { - return Err(anyhow!("Trying to set response one shot while previous is not fulfilled")) + return Err(anyhow!( + "Trying to set response one shot while previous is not fulfilled" + )); }; let (tx, rx) = oneshot::channel::>(); @@ -187,7 +225,12 @@ async fn request_loop( rx }; - send_message(JsMessageSide::PluginRuntime, send, JsPluginRuntimeMessage::Request(request)).await?; + send_message( + JsMessageSide::PluginRuntime, + send, + JsPluginRuntimeMessage::Request(request), + ) + .await?; tracing::trace!("Waiting for oneshot response..."); @@ -204,62 +247,61 @@ async fn message_loop( recv: &mut RecvHalf, event_sender: &Sender, response_oneshot: &Mutex>>>, - stop_token: CancellationToken + stop_token: CancellationToken, ) -> anyhow::Result<()> { match recv_message::(JsMessageSide::PluginRuntime, recv).await { Err(e) => { tracing::error!("Unable to handle message: {:?}", e); Err(e) } - Ok(msg) => match msg { - JsMessage::Event(event) => { - tracing::trace!("Received plugin event from backend {:?}", event); + Ok(msg) => { + match msg { + JsMessage::Event(event) => { + tracing::trace!("Received plugin event from backend {:?}", event); - let event_sender = event_sender.clone(); + let event_sender = event_sender.clone(); - tokio::spawn(async move { - event_sender - .send(event) - .await - .expect("event receiver was dropped"); - }); + tokio::spawn(async move { + event_sender.send(event).await.expect("event receiver was dropped"); + }); - Ok(()) - } - JsMessage::Response(response) => { - let mut response_oneshot = response_oneshot.lock().await; + Ok(()) + } + JsMessage::Response(response) => { + let mut response_oneshot = response_oneshot.lock().await; - match response_oneshot.take() { - Some(mut oneshot) => { - match oneshot.send(response) { - Err(_) => { - tracing::error!("Dropped oneshot receiving side"); - } - Ok(_) => { - tracing::trace!("Oneshot response sent"); + match response_oneshot.take() { + Some(mut oneshot) => { + match oneshot.send(response) { + Err(_) => { + tracing::error!("Dropped oneshot receiving side"); + } + Ok(_) => { + tracing::trace!("Oneshot response sent"); + } } } + None => { + tracing::error!("Received response without corresponding request: {:?}", response); + } } - None => { - tracing::error!("Received response without corresponding request: {:?}", response); - } + + Ok(()) } + JsMessage::Stop => { + stop_token.cancel(); - Ok(()) + Ok(()) + } } - JsMessage::Stop => { - stop_token.cancel(); - - Ok(()) - } - }, + } } } #[derive(Debug)] pub enum JsMessageSide { PluginRuntime, - Backend + Backend, } static MESSAGE_ID: AtomicU32 = AtomicU32::new(0); @@ -269,7 +311,13 @@ pub async fn send_message(side: JsMessageSide, send: &mut Sen let message_id = MESSAGE_ID.fetch_add(1, Ordering::SeqCst); - tracing::trace!(side = debug(&side), "Sending message with id {} and size of {} bytes: {:?}", message_id, encoded.len(), &value); + tracing::trace!( + side = debug(&side), + "Sending message with id {} and size of {} bytes: {:?}", + message_id, + encoded.len(), + &value + ); send.write_u32(message_id).await?; @@ -277,7 +325,12 @@ pub async fn send_message(side: JsMessageSide, send: &mut Sen send.write_all(&encoded[..]).await?; - tracing::trace!(side = debug(&side), "Message with id {} and size of {} bytes has been sent", message_id, encoded.len()); + tracing::trace!( + side = debug(&side), + "Message with id {} and size of {} bytes has been sent", + message_id, + encoded.len() + ); Ok(()) } @@ -298,7 +351,12 @@ pub async fn recv_message(side: JsMessageSide, recv: &mut Rec let (decoded, _) = bincode::decode_from_slice(&buffer[..], bincode::config::standard()) .context(format!("Unable to deserialize message with id: {}", message_id))?; - tracing::trace!(side = debug(&side), "Received message with id {}: {:?}", message_id, &decoded); + tracing::trace!( + side = debug(&side), + "Received message with id {}: {:?}", + message_id, + &decoded + ); Ok(decoded) } diff --git a/rust/plugin_runtime/src/logs.rs b/rust/plugin_runtime/src/logs.rs index 8a7cab3..4bd75d8 100644 --- a/rust/plugin_runtime/src/logs.rs +++ b/rust/plugin_runtime/src/logs.rs @@ -1,14 +1,18 @@ use std::cell::RefCell; use std::rc::Rc; -use deno_core::{op2, OpState}; + +use deno_core::op2; +use deno_core::OpState; + use crate::plugin_data::PluginData; #[op2(fast)] -pub fn op_log_trace(state: Rc>, #[string] target: String, #[string] message: String) -> anyhow::Result<()> { - let plugin_id = state.borrow() - .borrow::() - .plugin_id() - .to_string(); +pub fn op_log_trace( + state: Rc>, + #[string] target: String, + #[string] message: String, +) -> anyhow::Result<()> { + let plugin_id = state.borrow().borrow::().plugin_id().to_string(); tracing::trace!(target = target, plugin_id = plugin_id, message); @@ -16,11 +20,12 @@ pub fn op_log_trace(state: Rc>, #[string] target: String, #[str } #[op2(fast)] -pub fn op_log_debug(state: Rc>, #[string] target: String, #[string] message: String) -> anyhow::Result<()> { - let plugin_id = state.borrow() - .borrow::() - .plugin_id() - .to_string(); +pub fn op_log_debug( + state: Rc>, + #[string] target: String, + #[string] message: String, +) -> anyhow::Result<()> { + let plugin_id = state.borrow().borrow::().plugin_id().to_string(); tracing::debug!(target = target, plugin_id = plugin_id, message); @@ -28,11 +33,12 @@ pub fn op_log_debug(state: Rc>, #[string] target: String, #[str } #[op2(fast)] -pub fn op_log_info(state: Rc>, #[string] target: String, #[string] message: String) -> anyhow::Result<()> { - let plugin_id = state.borrow() - .borrow::() - .plugin_id() - .to_string(); +pub fn op_log_info( + state: Rc>, + #[string] target: String, + #[string] message: String, +) -> anyhow::Result<()> { + let plugin_id = state.borrow().borrow::().plugin_id().to_string(); tracing::info!(target = target, plugin_id = plugin_id, message); @@ -40,11 +46,12 @@ pub fn op_log_info(state: Rc>, #[string] target: String, #[stri } #[op2(fast)] -pub fn op_log_warn(state: Rc>, #[string] target: String, #[string] message: String) -> anyhow::Result<()> { - let plugin_id = state.borrow() - .borrow::() - .plugin_id() - .to_string(); +pub fn op_log_warn( + state: Rc>, + #[string] target: String, + #[string] message: String, +) -> anyhow::Result<()> { + let plugin_id = state.borrow().borrow::().plugin_id().to_string(); tracing::warn!(target = target, plugin_id = plugin_id, message); @@ -52,13 +59,14 @@ pub fn op_log_warn(state: Rc>, #[string] target: String, #[stri } #[op2(fast)] -pub fn op_log_error(state: Rc>, #[string] target: String, #[string] message: String) -> anyhow::Result<()> { - let plugin_id = state.borrow() - .borrow::() - .plugin_id() - .to_string(); +pub fn op_log_error( + state: Rc>, + #[string] target: String, + #[string] message: String, +) -> anyhow::Result<()> { + let plugin_id = state.borrow().borrow::().plugin_id().to_string(); tracing::error!(target = target, plugin_id = plugin_id, message); Ok(()) -} \ No newline at end of file +} diff --git a/rust/plugin_runtime/src/model.rs b/rust/plugin_runtime/src/model.rs index 2c53544..1294429 100644 --- a/rust/plugin_runtime/src/model.rs +++ b/rust/plugin_runtime/src/model.rs @@ -1,9 +1,16 @@ -use crate::JsEvent; -use gauntlet_common::model::{EntrypointId, Icons, PluginId, RootWidget}; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt; -use bincode::{Decode, Encode}; + +use bincode::Decode; +use bincode::Encode; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::Icons; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::RootWidget; +use serde::Deserialize; +use serde::Serialize; + +use crate::JsEvent; #[derive(Debug, Encode, Decode)] pub enum JsMessage { @@ -15,7 +22,7 @@ pub enum JsMessage { #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, Encode, Decode)] pub enum JsUiRenderLocation { InlineView, - View + View, } #[derive(Debug, Encode, Decode)] @@ -77,31 +84,31 @@ pub enum JsPluginRuntimeMessage { pub enum JsResponse { Nothing, AssetData { - data: Vec + data: Vec, }, EntrypointGeneratorEntrypointIds { - data: Vec + data: Vec, }, PluginPreferences { - data: HashMap + data: HashMap, }, EntrypointPreferences { - data: HashMap + data: HashMap, }, PluginPreferencesRequired { - data: bool + data: bool, }, EntrypointPreferencesRequired { - data: bool + data: bool, }, ClipboardRead { - data: JsClipboardData + data: JsClipboardData, }, ClipboardReadText { - data: Option + data: Option, }, ActionIdForShortcut { - data: Option + data: Option, }, } @@ -122,19 +129,19 @@ pub enum JsRequest { ShowPreferenceRequiredView { entrypoint_id: EntrypointId, plugin_preferences_required: bool, - entrypoint_preferences_required: bool + entrypoint_preferences_required: bool, }, ShowHud { - display: String + display: String, }, HideWindow, UpdateLoadingBar { entrypoint_id: EntrypointId, - show: bool + show: bool, }, ReloadSearchIndex { generated_entrypoints: Vec, - refresh_search_list: bool + refresh_search_list: bool, }, GetAssetData { path: String, @@ -151,10 +158,10 @@ pub enum JsRequest { ClipboardRead, ClipboardReadText, ClipboardWrite { - data: JsClipboardData + data: JsClipboardData, }, ClipboardWriteText { - data: String + data: String, }, ClipboardClear, GetActionIdForShortcut { @@ -163,7 +170,7 @@ pub enum JsRequest { modifier_shift: bool, modifier_control: bool, modifier_alt: bool, - modifier_meta: bool + modifier_meta: bool, }, } @@ -199,7 +206,6 @@ pub struct JsGeneratedSearchItemAction { pub label: String, } - #[derive(Debug, Deserialize, Serialize, Encode, Decode)] pub enum JsGeneratedSearchItemActionType { View, @@ -222,16 +228,16 @@ pub enum JsGeneratedSearchItemAccessory { TextAccessory { text: String, icon: Option, - tooltip: Option + tooltip: Option, }, IconAccessory { icon: Icons, - tooltip: Option + tooltip: Option, }, } #[derive(Debug, Serialize, Deserialize, Encode, Decode)] pub struct JsClipboardData { pub text_data: Option, - pub png_data: Option> -} \ No newline at end of file + pub png_data: Option>, +} diff --git a/rust/plugin_runtime/src/permissions.rs b/rust/plugin_runtime/src/permissions.rs index 3b10793..cff27fc 100644 --- a/rust/plugin_runtime/src/permissions.rs +++ b/rust/plugin_runtime/src/permissions.rs @@ -1,19 +1,37 @@ use std::collections::HashSet; use std::hash::Hash; -use std::path::{Path, PathBuf}; +use std::path::Path; +use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; + use anyhow::anyhow; -use deno_runtime::deno_fs::{FileSystemRc, RealFs}; -use deno_runtime::deno_permissions::{AllowRunDescriptor, EnvDescriptor, EnvQueryDescriptor, NetDescriptor, Permissions, PermissionsContainer, QueryDescriptor, ReadDescriptor, RunQueryDescriptor, SysDescriptor, SysDescriptorParseError, UnaryPermission, WriteDescriptor}; +use deno_runtime::deno_fs::FileSystemRc; +use deno_runtime::deno_fs::RealFs; +use deno_runtime::deno_permissions::AllowRunDescriptor; +use deno_runtime::deno_permissions::EnvDescriptor; +use deno_runtime::deno_permissions::EnvQueryDescriptor; +use deno_runtime::deno_permissions::NetDescriptor; +use deno_runtime::deno_permissions::Permissions; +use deno_runtime::deno_permissions::PermissionsContainer; +use deno_runtime::deno_permissions::QueryDescriptor; +use deno_runtime::deno_permissions::ReadDescriptor; +use deno_runtime::deno_permissions::RunQueryDescriptor; +use deno_runtime::deno_permissions::SysDescriptor; +use deno_runtime::deno_permissions::SysDescriptorParseError; +use deno_runtime::deno_permissions::UnaryPermission; +use deno_runtime::deno_permissions::WriteDescriptor; use deno_runtime::permissions::RuntimePermissionDescriptorParser; +use gauntlet_common::dirs::Dirs; use once_cell::sync::Lazy; use regex::Regex; use typed_path::Utf8TypedPath; -use gauntlet_common::dirs::Dirs; -use crate::{JsPluginPermissions, JsPluginPermissionsExec}; -pub static PERMISSIONS_VARIABLE_PATTERN: Lazy = Lazy::new(|| Regex::new(r"\{(?.+?):(?.+?)}").expect("invalid regex")); +use crate::JsPluginPermissions; +use crate::JsPluginPermissionsExec; + +pub static PERMISSIONS_VARIABLE_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"\{(?.+?):(?.+?)}").expect("invalid regex")); pub fn permissions_to_deno( fs: FileSystemRc, @@ -25,8 +43,20 @@ pub fn permissions_to_deno( Ok(PermissionsContainer::new( Arc::new(RuntimePermissionDescriptorParser::new(fs)), Permissions { - read: path_permission(&permissions.filesystem.read, ReadDescriptor, home_dir, plugin_data_dir, plugin_cache_dir)?, - write: path_permission(&permissions.filesystem.write, WriteDescriptor, home_dir, plugin_data_dir, plugin_cache_dir)?, + read: path_permission( + &permissions.filesystem.read, + ReadDescriptor, + home_dir, + plugin_data_dir, + plugin_cache_dir, + )?, + write: path_permission( + &permissions.filesystem.write, + WriteDescriptor, + home_dir, + plugin_data_dir, + plugin_cache_dir, + )?, net: net_permission(&permissions.network), env: env_permission(&permissions.environment), sys: sys_permission(&permissions.system)?, @@ -34,7 +64,7 @@ pub fn permissions_to_deno( ffi: Permissions::new_unary(None, None, false), import: UnaryPermission::default(), all: Permissions::new_all(false), - } + }, )) } @@ -56,11 +86,7 @@ fn path_permission .filter_map(std::convert::identity) .collect::>(); - let allow_list = if allow_list.is_empty() { - None - } else { - Some(allow_list) - }; + let allow_list = if allow_list.is_empty() { None } else { Some(allow_list) }; Ok(Permissions::new_unary(allow_list, None, false)) } @@ -69,11 +95,9 @@ fn net_permission(domain_and_ports: &[String]) -> UnaryPermission let allow_list = if domain_and_ports.is_empty() { None } else { - let allow_list = domain_and_ports.into_iter() - .map(|domain_and_port| { - NetDescriptor::parse(&domain_and_port) - .expect("should be validated when loading") - }) + let allow_list = domain_and_ports + .into_iter() + .map(|domain_and_port| NetDescriptor::parse(&domain_and_port).expect("should be validated when loading")) .collect(); Some(allow_list) @@ -86,9 +110,7 @@ fn env_permission(envs: &[String]) -> UnaryPermission { let allow_list = if envs.is_empty() { None } else { - let allow_list = envs.into_iter() - .map(|env| EnvDescriptor::new(env)) - .collect(); + let allow_list = envs.into_iter().map(|env| EnvDescriptor::new(env)).collect(); Some(allow_list) }; @@ -100,7 +122,8 @@ fn sys_permission(system: &[String]) -> anyhow::Result, _>>()? .into_iter() @@ -118,7 +141,8 @@ fn run_permission( plugin_data_dir: &Path, plugin_cache_dir: &Path, ) -> anyhow::Result> { - let granted_executable = permissions.executable + let granted_executable = permissions + .executable .iter() .map(|path| { augment_path(path, home_dir, plugin_data_dir, plugin_cache_dir) @@ -129,7 +153,8 @@ fn run_permission( .filter_map(std::convert::identity) .collect::>(); - let granted_command = permissions.command + let granted_command = permissions + .command .iter() .flat_map(|cmd| anyhow::Ok(AllowRunDescriptor(which::which_global(cmd)?))) .collect::>(); @@ -138,16 +163,17 @@ fn run_permission( granted.extend(granted_executable); granted.extend(granted_command); - let allow_list = if granted.is_empty() { - None - } else { - Some(granted) - }; + let allow_list = if granted.is_empty() { None } else { Some(granted) }; Ok(Permissions::new_unary(allow_list, None, false)) } -fn augment_path(path: &String, home_dir: &Path, plugin_data_dir: &Path, plugin_cache_dir: &Path) -> anyhow::Result> { +fn augment_path( + path: &String, + home_dir: &Path, + plugin_data_dir: &Path, + plugin_cache_dir: &Path, +) -> anyhow::Result> { if let Some(matches) = PERMISSIONS_VARIABLE_PATTERN.captures(path) { let namespace = &matches["namespace"]; let name = &matches["name"]; @@ -159,35 +185,39 @@ fn augment_path(path: &String, home_dir: &Path, plugin_data_dir: &Path, plugin_c } else { None } - }, + } ("linux", "user-home") => { if cfg!(target_os = "linux") { Some(home_dir) } else { None } - }, + } ("windows", "user-home") => { if cfg!(windows) { Some(home_dir) } else { None } - }, + } ("common", "plugin-data") => Some(plugin_data_dir), ("common", "plugin-cache") => Some(plugin_cache_dir), (_, _) => { - Err(anyhow!("Trying to load plugin with unknown variable in path in manifest permissions: {}", path))? + Err(anyhow!( + "Trying to load plugin with unknown variable in path in manifest permissions: {}", + path + ))? } }; match replacement { None => Ok(None), Some(replacement) => { - let replacement = replacement.to_str() - .expect("non-utf8 file paths are not supported"); + let replacement = replacement.to_str().expect("non-utf8 file paths are not supported"); - Ok(Some(PathBuf::from(PERMISSIONS_VARIABLE_PATTERN.replace(path, replacement).to_string()))) + Ok(Some(PathBuf::from( + PERMISSIONS_VARIABLE_PATTERN.replace(path, replacement).to_string(), + ))) } } } else { @@ -208,4 +238,4 @@ fn augment_path(path: &String, home_dir: &Path, plugin_data_dir: &Path, plugin_c } } } -} \ No newline at end of file +} diff --git a/rust/plugin_runtime/src/plugin_data.rs b/rust/plugin_runtime/src/plugin_data.rs index 13fb7be..58c0a2b 100644 --- a/rust/plugin_runtime/src/plugin_data.rs +++ b/rust/plugin_runtime/src/plugin_data.rs @@ -1,7 +1,9 @@ -use gauntlet_common::model::{EntrypointId, PluginId}; use std::collections::HashMap; use std::path::PathBuf; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::PluginId; + pub struct PluginData { plugin_id: PluginId, plugin_uuid: String, @@ -34,7 +36,7 @@ impl PluginData { plugin_data_dir, inline_view_entrypoint_id, entrypoint_names, - home_dir + home_dir, } } @@ -65,4 +67,4 @@ impl PluginData { pub fn home_dir(&self) -> PathBuf { self.home_dir.clone() } -} \ No newline at end of file +} diff --git a/rust/plugin_runtime/src/plugins/applications.rs b/rust/plugin_runtime/src/plugins/applications.rs index 9630d96..d15e9a5 100644 --- a/rust/plugin_runtime/src/plugins/applications.rs +++ b/rust/plugin_runtime/src/plugins/applications.rs @@ -1,14 +1,18 @@ use std::cell::RefCell; -use deno_core::{op2, OpState}; use std::path::PathBuf; use std::rc::Rc; + use anyhow::anyhow; -use image::ImageFormat; +use deno_core::op2; +use deno_core::OpState; use image::imageops::FilterType; -use serde::{Deserialize, Serialize}; +use image::ImageFormat; +use serde::Deserialize; +use serde::Serialize; use tokio::runtime::Handle; use tokio::sync::mpsc::Receiver; use tokio::task::spawn_blocking; + use crate::plugin_data::PluginData; #[cfg(target_os = "linux")] @@ -26,19 +30,13 @@ mod windows; #[cfg(target_os = "windows")] pub use windows::gauntlet_internal_windows; - #[derive(Debug, Serialize)] #[serde(tag = "type")] pub enum DesktopPathAction { #[serde(rename = "add")] - Add { - id: String, - data: DesktopApplication - }, + Add { id: String, data: DesktopApplication }, #[serde(rename = "remove")] - Remove { - id: String - } + Remove { id: String }, } #[cfg(target_os = "linux")] @@ -82,7 +80,6 @@ pub struct DesktopSettings13AndPostData { icon: Option>, } - #[op2] #[string] pub fn current_os() -> &'static str { @@ -91,13 +88,7 @@ pub fn current_os() -> &'static str { #[op2(fast)] pub fn wayland(state: Rc>) -> bool { - let wayland = { - state - .borrow() - .borrow::() - .desktop - .is_wayland() - }; + let wayland = { state.borrow().borrow::().desktop.is_wayland() }; wayland } @@ -123,7 +114,7 @@ impl DesktopEnvironment { match self { #[cfg(target_os = "linux")] DesktopEnvironment::Linux(linux) => linux.is_wayland(), - DesktopEnvironment::None => false + DesktopEnvironment::None => false, } } } @@ -183,7 +174,6 @@ pub fn macos_application_dirs() -> Vec { #[cfg(target_os = "macos")] #[op2(fast)] pub fn macos_open_application(#[string] app_path: String) -> anyhow::Result<()> { - spawn_detached("open", &[app_path])?; Ok(()) @@ -206,13 +196,7 @@ pub fn macos_settings_13_and_post() -> Vec { #[cfg(target_os = "macos")] #[op2(fast)] pub fn macos_open_setting_13_and_post(#[string] preferences_id: String) -> anyhow::Result<()> { - - spawn_detached( - "open", - &[ - format!("x-apple.systempreferences:{}", preferences_id) - ] - )?; + spawn_detached("open", &[format!("x-apple.systempreferences:{}", preferences_id)])?; Ok(()) } @@ -220,31 +204,21 @@ pub fn macos_open_setting_13_and_post(#[string] preferences_id: String) -> anyho #[cfg(target_os = "macos")] #[op2(fast)] pub fn macos_open_setting_pre_13(#[string] setting_path: String) -> anyhow::Result<()> { - - spawn_detached( - "open", - &[ - "-b", - "com.apple.systempreferences", - &setting_path, - ] - )?; + spawn_detached("open", &["-b", "com.apple.systempreferences", &setting_path])?; Ok(()) } #[cfg(unix)] -pub fn spawn_detached( - path: &str, - args: I, -) -> std::io::Result<()> +pub fn spawn_detached(path: &str, args: I) -> std::io::Result<()> where I: IntoIterator + Copy, S: AsRef, { // from https://github.com/alacritty/alacritty/blob/5abb4b73937b17fe501b9ca20b602950f1218b96/alacritty/src/daemon.rs#L65 use std::os::unix::prelude::CommandExt; - use std::process::{Command, Stdio}; + use std::process::Command; + use std::process::Stdio; let mut command = Command::new(path); @@ -284,4 +258,4 @@ pub(in crate::plugins::applications) fn resize_icon(data: Vec) -> anyhow::Re data.write_to(&mut buffer, ImageFormat::Png)?; Ok(buffer.into_inner()) -} \ No newline at end of file +} diff --git a/rust/plugin_runtime/src/plugins/applications/linux/mod.rs b/rust/plugin_runtime/src/plugins/applications/linux/mod.rs index f270e57..5880301 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/mod.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/mod.rs @@ -1,21 +1,30 @@ -use crate::plugin_data::PluginData; -use crate::plugins::applications::{linux, resize_icon, spawn_detached, DesktopApplication, DesktopPathAction}; -use deno_core::{op2, OpState}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::collections::HashSet; +use std::fs::Metadata; +use std::path::Path; +use std::path::PathBuf; +use std::rc::Rc; + +use deno_core::op2; +use deno_core::OpState; use freedesktop_entry_parser::parse_entry; use freedesktop_icons::lookup; use image::imageops::FilterType; use image::ImageFormat; -use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; -use std::fs::Metadata; -use std::path::{Path, PathBuf}; -use std::rc::Rc; use tokio::sync::mpsc::Sender; use tokio::task::spawn_blocking; use walkdir::WalkDir; -mod x11; +use crate::plugin_data::PluginData; +use crate::plugins::applications::linux; +use crate::plugins::applications::resize_icon; +use crate::plugins::applications::spawn_detached; +use crate::plugins::applications::DesktopApplication; +use crate::plugins::applications::DesktopPathAction; + mod wayland; +mod x11; deno_core::extension!( gauntlet_internal_linux, @@ -31,8 +40,8 @@ deno_core::extension!( ], esm_entry_point = "ext:gauntlet/internal-linux/bootstrap.js", esm = [ - "ext:gauntlet/internal-linux/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-linux-bootstrap.js", - "ext:gauntlet/internal-linux.js" = "../../js/core/dist/internal-linux.js", + "ext:gauntlet/internal-linux/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-linux-bootstrap.js", + "ext:gauntlet/internal-linux.js" = "../../js/core/dist/internal-linux.js", ] ); @@ -48,7 +57,9 @@ impl LinuxDesktopEnvironment { .is_ok(); if wayland { - Ok(LinuxDesktopEnvironment::Wayland(wayland::WaylandDesktopEnvironment::new()?)) + Ok(LinuxDesktopEnvironment::Wayland( + wayland::WaylandDesktopEnvironment::new()?, + )) } else { Ok(LinuxDesktopEnvironment::X11(x11::X11DesktopEnvironment::new())) } @@ -61,13 +72,14 @@ impl LinuxDesktopEnvironment { #[op2(async)] #[serde] -async fn linux_app_from_path(state: Rc>, #[string] path: String) -> anyhow::Result> { +async fn linux_app_from_path( + state: Rc>, + #[string] path: String, +) -> anyhow::Result> { let home_dir = { let state = state.borrow(); - let home_dir = state - .borrow::() - .home_dir(); + let home_dir = state.borrow::().home_dir(); home_dir }; @@ -81,9 +93,7 @@ fn linux_application_dirs(state: Rc>) -> Vec { let home_dir = { let state = state.borrow(); - let home_dir = state - .borrow::() - .home_dir(); + let home_dir = state.borrow::().home_dir(); home_dir }; @@ -96,29 +106,19 @@ fn linux_application_dirs(state: Rc>) -> Vec { #[op2(fast)] fn linux_open_application(#[string] desktop_file_id: String) -> anyhow::Result<()> { - spawn_detached("gtk-launch", &[desktop_file_id])?; Ok(()) } - fn linux_application_dirs_inner(home_dir: PathBuf) -> Vec { let data_home = match std::env::var_os("XDG_DATA_HOME") { - Some(val) => { - PathBuf::from(val) - }, - None => { - home_dir - .join(".local") - .join("share") - } + Some(val) => PathBuf::from(val), + None => home_dir.join(".local").join("share"), }; let mut extra_data_dirs = match std::env::var_os("XDG_DATA_DIRS") { - Some(val) => { - std::env::split_paths(&val).map(PathBuf::from).collect() - }, + Some(val) => std::env::split_paths(&val).map(PathBuf::from).collect(), None => { vec![ PathBuf::from("/usr/local/share"), @@ -128,19 +128,14 @@ fn linux_application_dirs_inner(home_dir: PathBuf) -> Vec { } }; - let flatpak = data_home.to_path_buf() - .join("flatpak") - .join("exports") - .join("share"); + let flatpak = data_home.to_path_buf().join("flatpak").join("exports").join("share"); let mut res = Vec::new(); res.push(data_home); res.push(flatpak); res.append(&mut extra_data_dirs); - res.into_iter() - .map(|d| d.join("applications")) - .collect() + res.into_iter().map(|d| d.join("applications")).collect() } fn linux_app_from_path_async(home_dir: PathBuf, path: PathBuf) -> Option { @@ -191,7 +186,7 @@ fn linux_app_from_path_async(home_dir: PathBuf, path: PathBuf) -> Option Option { .inspect_err(|err| tracing::warn!("error parsing .desktop file at path {:?}: {:?}", desktop_file_path, err)) .ok()?; - let desktop_file_path_str = desktop_file_path.to_str() + let desktop_file_path_str = desktop_file_path + .to_str() .expect("non-utf8 paths are not supported") .to_string(); @@ -214,11 +210,11 @@ fn create_app_entry(desktop_file_path: &Path) -> Option { let icon = entry.attr("Icon").map(|s| s.to_string()); let no_display = entry.attr("NoDisplay").map(|val| val == "true").unwrap_or(false); let hidden = entry.attr("Hidden").map(|val| val == "true").unwrap_or(false); - let startup_wm_class = entry.attr("StartupWMClass").map(|s| s.to_string());; + let startup_wm_class = entry.attr("StartupWMClass").map(|s| s.to_string()); // TODO NotShowIn, OnlyShowIn https://wiki.archlinux.org/title/desktop_entries if no_display || hidden { - return None + return None; } let icon = icon @@ -227,9 +223,7 @@ fn create_app_entry(desktop_file_path: &Path) -> Option { if icon_path.is_absolute() { Some(icon_path) } else { - lookup(&icon) - .with_size(48) - .find() + lookup(&icon).with_size(48).find() } }) .flatten() @@ -243,14 +237,15 @@ fn create_app_entry(desktop_file_path: &Path) -> Option { let data = std::fs::read(path)?; resize_icon(data) - }, + } Some("svg") => { let data = std::fs::read(path)?; let tree = resvg::usvg::Tree::from_data(&data, &resvg::usvg::Options::default())?; let pixmap_size = tree.size().to_int_size(); - let mut pixmap = resvg::tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); + let mut pixmap = + resvg::tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); resvg::render(&tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut()); @@ -259,7 +254,7 @@ fn create_app_entry(desktop_file_path: &Path) -> Option { let data = resize_icon(data)?; Ok(data) - }, + } Some("xpm") => Err(anyhow::anyhow!("xpm format")), _ => Err(anyhow::anyhow!("unsupported by spec format {:?}", extension)), } @@ -267,8 +262,7 @@ fn create_app_entry(desktop_file_path: &Path) -> Option { } }) .map(|res| { - res - .inspect_err(|err| tracing::warn!("error processing icon of {:?}: {:?}", desktop_file_path, err)) + res.inspect_err(|err| tracing::warn!("error processing icon of {:?}: {:?}", desktop_file_path, err)) .ok() }) .flatten(); diff --git a/rust/plugin_runtime/src/plugins/applications/linux/wayland/cosmic.rs b/rust/plugin_runtime/src/plugins/applications/linux/wayland/cosmic.rs index 20b885a..a9ac496 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/wayland/cosmic.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/wayland/cosmic.rs @@ -1,19 +1,29 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use std::sync::Mutex; + use anyhow::anyhow; -use smithay_client_toolkit::reexports::calloop::channel::Sender; -use smithay_client_toolkit::seat::SeatState; -use tokio::runtime::Handle; -use crate::plugins::applications::linux::wayland::{send_event, JsWaylandApplicationEvent, WaylandState, WaylandStateInner}; -use wayland_client::globals::GlobalList; -use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle}; -use wayland_client::backend::ObjectId; -use wayland_client::protocol::wl_seat::WlSeat; use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1; use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_info_v1; use cosmic_protocols::toplevel_management::v1::client::zcosmic_toplevel_manager_v1; +use smithay_client_toolkit::reexports::calloop::channel::Sender; +use smithay_client_toolkit::seat::SeatState; +use tokio::runtime::Handle; +use wayland_client::backend::ObjectId; +use wayland_client::event_created_child; +use wayland_client::globals::GlobalList; +use wayland_client::protocol::wl_seat::WlSeat; +use wayland_client::Connection; +use wayland_client::Dispatch; +use wayland_client::Proxy; +use wayland_client::QueueHandle; + +use crate::plugins::applications::linux::wayland::send_event; +use crate::plugins::applications::linux::wayland::JsWaylandApplicationEvent; +use crate::plugins::applications::linux::wayland::WaylandState; +use crate::plugins::applications::linux::wayland::WaylandStateInner; pub struct CosmicWaylandState { uuid_to_obj_id: HashMap, @@ -24,12 +34,8 @@ pub struct CosmicWaylandState { impl CosmicWaylandState { pub fn new(globals: &GlobalList, queue_handle: &QueueHandle) -> anyhow::Result { - let management = globals - .bind::( - &queue_handle, - 3..=3, - (), - )?; + let management = + globals.bind::(&queue_handle, 3..=3, ())?; Ok(Self { management, @@ -40,17 +46,19 @@ impl CosmicWaylandState { } pub fn focus_window(&self, window_uuid: String, seat_state: &SeatState) -> anyhow::Result<()> { - let obj_id = self.uuid_to_obj_id + let obj_id = self + .uuid_to_obj_id .get(&window_uuid) .ok_or(anyhow!("Unable to find object id for window uuid: {}", window_uuid))?; - let toplevel = self.toplevels + let toplevel = self + .toplevels .get(&obj_id) .ok_or(anyhow!("Unable to find object id for window uuid: {}", window_uuid))?; match seat_state.seats().next() { Some(seat) => self.management.activate(&toplevel, &seat), - None => Err(anyhow!("no wayland seats found"))? + None => Err(anyhow!("no wayland seats found"))?, }; Ok(()) @@ -64,7 +72,7 @@ impl Dispatch for Way _event: ::Event, _data: &(), _conn: &Connection, - _qhandle: &QueueHandle + _qhandle: &QueueHandle, ) { } } @@ -88,9 +96,11 @@ impl Dispatch for WaylandSt inner.obj_id_to_uuid.insert(toplevel.id(), window_id.clone()); inner.toplevels.insert(toplevel.id(), toplevel); - send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowOpened { - window_id, - }); + send_event( + &state.tokio_handle, + &state.sender, + JsWaylandApplicationEvent::WindowOpened { window_id }, + ); } _ => {} } @@ -119,13 +129,19 @@ impl Dispatch for Wayla WaylandStateInner::Cosmic(inner) => { match inner.obj_id_to_uuid.get(&proxy.id()) { Some(window_id) => { - send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowTitleChanged { - window_id: window_id.clone(), - title, - }); + send_event( + &state.tokio_handle, + &state.sender, + JsWaylandApplicationEvent::WindowTitleChanged { + window_id: window_id.clone(), + title, + }, + ); } None => { - tracing::warn!("Received event for cosmic wayland toplevel that doesn't exist in state"); + tracing::warn!( + "Received event for cosmic wayland toplevel that doesn't exist in state" + ); } } } @@ -137,13 +153,19 @@ impl Dispatch for Wayla WaylandStateInner::Cosmic(inner) => { match inner.obj_id_to_uuid.get(&proxy.id()) { Some(window_id) => { - send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowAppIdChanged { - window_id: window_id.clone(), - app_id, - }); + send_event( + &state.tokio_handle, + &state.sender, + JsWaylandApplicationEvent::WindowAppIdChanged { + window_id: window_id.clone(), + app_id, + }, + ); } None => { - tracing::warn!("Received event for cosmic wayland toplevel that doesn't exist in state"); + tracing::warn!( + "Received event for cosmic wayland toplevel that doesn't exist in state" + ); } } } @@ -153,18 +175,23 @@ impl Dispatch for Wayla zcosmic_toplevel_handle_v1::Event::Closed => { match &mut state.inner { WaylandStateInner::Cosmic(inner) => { - inner.toplevels.remove(&proxy.id()); match inner.obj_id_to_uuid.remove(&proxy.id()) { Some(window_id) => { inner.uuid_to_obj_id.remove(&window_id); - send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowClosed { - window_id: window_id.clone(), - }); + send_event( + &state.tokio_handle, + &state.sender, + JsWaylandApplicationEvent::WindowClosed { + window_id: window_id.clone(), + }, + ); } None => { - tracing::warn!("Received event for cosmic wayland toplevel that doesn't exist in state"); + tracing::warn!( + "Received event for cosmic wayland toplevel that doesn't exist in state" + ); } } } @@ -174,4 +201,4 @@ impl Dispatch for Wayla _ => {} } } -} \ No newline at end of file +} diff --git a/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs b/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs index b82b757..fdd780e 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs @@ -1,23 +1,39 @@ use std::cell::RefCell; use std::rc::Rc; + use anyhow::anyhow; -use deno_core::{op2, OpState}; -use serde::{Deserialize, Serialize}; +use deno_core::op2; +use deno_core::OpState; +use serde::Deserialize; +use serde::Serialize; use smithay_client_toolkit::reexports::calloop; -use smithay_client_toolkit::reexports::calloop::{EventLoop, InsertError, RegistrationToken}; -use smithay_client_toolkit::reexports::calloop::channel::{Channel, Event}; +use smithay_client_toolkit::reexports::calloop::channel::Channel; +use smithay_client_toolkit::reexports::calloop::channel::Event; +use smithay_client_toolkit::reexports::calloop::EventLoop; +use smithay_client_toolkit::reexports::calloop::InsertError; +use smithay_client_toolkit::reexports::calloop::RegistrationToken; use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource; -use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState}; +use smithay_client_toolkit::seat::Capability; +use smithay_client_toolkit::seat::SeatHandler; +use smithay_client_toolkit::seat::SeatState; use tokio::runtime::Handle; -use tokio::sync::mpsc::{Receiver, Sender}; -use wayland_client::{Connection, Dispatch, QueueHandle}; -use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContents}; +use tokio::sync::mpsc::Receiver; +use tokio::sync::mpsc::Sender; +use wayland_client::globals::registry_queue_init; +use wayland_client::globals::GlobalList; +use wayland_client::globals::GlobalListContents; use wayland_client::protocol::wl_registry; use wayland_client::protocol::wl_seat::WlSeat; -use crate::plugins::applications::{linux, ApplicationContext, DesktopEnvironment}; +use wayland_client::Connection; +use wayland_client::Dispatch; +use wayland_client::QueueHandle; + +use crate::plugins::applications::linux; +use crate::plugins::applications::ApplicationContext; +use crate::plugins::applications::DesktopEnvironment; -mod wlr; mod cosmic; +mod wlr; pub struct WaylandDesktopEnvironment { activate_sender: calloop::channel::Sender, @@ -55,27 +71,17 @@ impl WaylandDesktopEnvironment { #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type")] pub enum JsWaylandApplicationEvent { - WindowOpened { - window_id: String - }, - WindowClosed { - window_id: String - }, - WindowTitleChanged { - window_id: String, - title: String, - }, - WindowAppIdChanged { - window_id: String, - app_id: String - } + WindowOpened { window_id: String }, + WindowClosed { window_id: String }, + WindowTitleChanged { window_id: String, title: String }, + WindowAppIdChanged { window_id: String, app_id: String }, } pub struct WaylandState { seat_state: SeatState, tokio_handle: Handle, sender: Sender, - inner: WaylandStateInner + inner: WaylandStateInner, } impl WaylandState { @@ -84,14 +90,12 @@ impl WaylandState { sender: Sender, seat_state: SeatState, globals: &GlobalList, - queue_handle: &QueueHandle + queue_handle: &QueueHandle, ) -> anyhow::Result { - let inner = wlr::WlrWaylandState::new(globals, queue_handle) .map(|state| WaylandStateInner::Wlr(state)) .or_else(|test| { - cosmic::CosmicWaylandState::new(globals, queue_handle) - .map(|state| WaylandStateInner::Cosmic(state)) + cosmic::CosmicWaylandState::new(globals, queue_handle).map(|state| WaylandStateInner::Cosmic(state)) }) .unwrap_or(WaylandStateInner::None); @@ -107,7 +111,7 @@ impl WaylandState { pub enum WaylandStateInner { Wlr(wlr::WlrWaylandState), Cosmic(cosmic::CosmicWaylandState), - None + None, } fn send_event(tokio_handle: &Handle, sender: &Sender, app_event: JsWaylandApplicationEvent) { @@ -162,14 +166,13 @@ pub fn linux_wayland_focus_window(state: Rc>, #[string] window_ { let state = state.borrow(); - let context = state - .borrow::(); + let context = state.borrow::(); match &context.desktop { DesktopEnvironment::Linux(linux::LinuxDesktopEnvironment::Wayland(env)) => { env.focus_window(window_uuid)?; - }, - _ => Err(anyhow!("Calling linux_wayland_focus_window on non-wayland platform"))? + } + _ => Err(anyhow!("Calling linux_wayland_focus_window on non-wayland platform"))?, }; }; @@ -179,7 +182,7 @@ pub fn linux_wayland_focus_window(state: Rc>, #[string] window_ fn activation_handler(event: Event, _metadata: &mut (), state: &mut WaylandState) { let window_uuid = match event { Event::Msg(window_uuid) => window_uuid, - Event::Closed => panic!("activation source was closed") + Event::Closed => panic!("activation source was closed"), }; match &state.inner { @@ -199,26 +202,29 @@ fn activation_handler(event: Event, _metadata: &mut (), state: &mut Wayl } } - #[op2(async)] #[serde] -pub async fn application_wayland_pending_event(state: Rc>) -> anyhow::Result { +pub async fn application_wayland_pending_event( + state: Rc>, +) -> anyhow::Result { let receiver = { let state = state.borrow(); - let context = state - .borrow::(); + let context = state.borrow::(); match &context.desktop { - DesktopEnvironment::Linux(linux::LinuxDesktopEnvironment::Wayland(env)) => { - env.event_receiver.clone() - }, - _ => Err(anyhow!("Calling application_wayland_pending_event on non-wayland platform"))? + DesktopEnvironment::Linux(linux::LinuxDesktopEnvironment::Wayland(env)) => env.event_receiver.clone(), + _ => { + Err(anyhow!( + "Calling application_wayland_pending_event on non-wayland platform" + ))? + } } }; let mut receiver = receiver.borrow_mut(); - let event = receiver.recv() + let event = receiver + .recv() .await .ok_or_else(|| anyhow!("plugin event stream was suddenly closed"))?; @@ -227,7 +233,6 @@ pub async fn application_wayland_pending_event(state: Rc>) -> a Ok(event) } - impl Dispatch for WaylandState { fn event( _state: &mut WaylandState, @@ -245,17 +250,20 @@ impl SeatHandler for WaylandState { &mut self.seat_state } - fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat) { + fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat) {} + + fn new_capability(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat, _capability: Capability) {} + + fn remove_capability( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _seat: WlSeat, + _capability: Capability, + ) { } - fn new_capability(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat, _capability: Capability) { - } - - fn remove_capability(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat, _capability: Capability) { - } - - fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat) { - } + fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat) {} } smithay_client_toolkit::delegate_seat!(WaylandState); diff --git a/rust/plugin_runtime/src/plugins/applications/linux/wayland/wlr.rs b/rust/plugin_runtime/src/plugins/applications/linux/wayland/wlr.rs index 6ad9349..2b66ce9 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/wayland/wlr.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/wayland/wlr.rs @@ -1,17 +1,28 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use std::sync::Mutex; + use anyhow::anyhow; use smithay_client_toolkit::reexports::calloop::channel::Sender; use smithay_client_toolkit::seat::SeatState; use tokio::runtime::Handle; -use crate::plugins::applications::linux::wayland::{send_event, JsWaylandApplicationEvent, WaylandState, WaylandStateInner}; -use wayland_client::globals::GlobalList; -use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle}; use wayland_client::backend::ObjectId; +use wayland_client::event_created_child; +use wayland_client::globals::GlobalList; use wayland_client::protocol::wl_seat::WlSeat; -use wayland_protocols_wlr::foreign_toplevel::v1::client::{zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1}; +use wayland_client::Connection; +use wayland_client::Dispatch; +use wayland_client::Proxy; +use wayland_client::QueueHandle; +use wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1; +use wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1; + +use crate::plugins::applications::linux::wayland::send_event; +use crate::plugins::applications::linux::wayland::JsWaylandApplicationEvent; +use crate::plugins::applications::linux::wayland::WaylandState; +use crate::plugins::applications::linux::wayland::WaylandStateInner; pub struct WlrWaylandState { uuid_to_obj_id: HashMap, @@ -21,12 +32,11 @@ pub struct WlrWaylandState { impl WlrWaylandState { pub fn new(globals: &GlobalList, queue_handle: &QueueHandle) -> anyhow::Result { - let _management = globals - .bind::( - &queue_handle, - 3..=3, - (), - )?; + let _management = globals.bind::( + &queue_handle, + 3..=3, + (), + )?; Ok(Self { uuid_to_obj_id: HashMap::new(), @@ -36,17 +46,19 @@ impl WlrWaylandState { } pub fn focus_window(&self, window_uuid: String, seat_state: &SeatState) -> anyhow::Result<()> { - let obj_id = self.uuid_to_obj_id + let obj_id = self + .uuid_to_obj_id .get(&window_uuid) .ok_or(anyhow!("Unable to find object id for window uuid: {}", window_uuid))?; - let toplevel = self.toplevels + let toplevel = self + .toplevels .get(&obj_id) .ok_or(anyhow!("Unable to find object id for window uuid: {}", window_uuid))?; match seat_state.seats().next() { Some(seat) => toplevel.activate(&seat), - None => Err(anyhow!("no wayland seats found"))? + None => Err(anyhow!("no wayland seats found"))?, }; Ok(()) @@ -72,9 +84,11 @@ impl Dispatch {} } @@ -103,10 +117,14 @@ impl Dispatch WaylandStateInner::Wlr(inner) => { match inner.obj_id_to_uuid.get(&proxy.id()) { Some(window_id) => { - send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowTitleChanged { - window_id: window_id.clone(), - title, - }); + send_event( + &state.tokio_handle, + &state.sender, + JsWaylandApplicationEvent::WindowTitleChanged { + window_id: window_id.clone(), + title, + }, + ); } None => { tracing::warn!("Received event for wlr wayland toplevel that doesn't exist in state"); @@ -121,10 +139,14 @@ impl Dispatch WaylandStateInner::Wlr(inner) => { match inner.obj_id_to_uuid.get(&proxy.id()) { Some(window_id) => { - send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowAppIdChanged { - window_id: window_id.clone(), - app_id, - }); + send_event( + &state.tokio_handle, + &state.sender, + JsWaylandApplicationEvent::WindowAppIdChanged { + window_id: window_id.clone(), + app_id, + }, + ); } None => { tracing::warn!("Received event for wlr wayland toplevel that doesn't exist in state"); @@ -137,15 +159,18 @@ impl Dispatch zwlr_foreign_toplevel_handle_v1::Event::Closed => { match &mut state.inner { WaylandStateInner::Wlr(inner) => { - inner.toplevels.remove(&proxy.id()); match inner.obj_id_to_uuid.remove(&proxy.id()) { Some(window_id) => { inner.uuid_to_obj_id.remove(&window_id); - send_event(&state.tokio_handle, &state.sender, JsWaylandApplicationEvent::WindowClosed { - window_id: window_id.clone(), - }); + send_event( + &state.tokio_handle, + &state.sender, + JsWaylandApplicationEvent::WindowClosed { + window_id: window_id.clone(), + }, + ); } None => { tracing::warn!("Received event for wlr wayland toplevel that doesn't exist in state"); @@ -158,4 +183,4 @@ impl Dispatch _ => {} } } -} \ No newline at end of file +} diff --git a/rust/plugin_runtime/src/plugins/applications/linux/x11.rs b/rust/plugin_runtime/src/plugins/applications/linux/x11.rs index a2eac9a..7d4531f 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/x11.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/x11.rs @@ -1,24 +1,41 @@ -use crate::plugins::applications::linux::x11; -use anyhow::anyhow; -use deno_core::{op2, OpState}; -use encoding::{DecoderTrap, Encoding}; -use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::HashMap; use std::convert::Infallible; use std::rc::Rc; use std::str::FromStr; + +use anyhow::anyhow; +use deno_core::op2; +use deno_core::OpState; +use encoding::DecoderTrap; +use encoding::Encoding; +use serde::Deserialize; +use serde::Serialize; use tokio::runtime::Handle; -use tokio::sync::mpsc::{Receiver, Sender}; +use tokio::sync::mpsc::Receiver; +use tokio::sync::mpsc::Sender; use x11rb::connection::Connection; use x11rb::errors::ConnectionError; -use x11rb::properties::{WmClass, WmHints}; +use x11rb::properties::WmClass; +use x11rb::properties::WmHints; +use x11rb::protocol::xproto::Atom; +use x11rb::protocol::xproto::AtomEnum; +use x11rb::protocol::xproto::ChangeWindowAttributesAux; +use x11rb::protocol::xproto::ClientMessageEvent; +use x11rb::protocol::xproto::ConfigureWindowAux; use x11rb::protocol::xproto::ConnectionExt; -use x11rb::protocol::xproto::{Atom, AtomEnum, ClientMessageEvent, ConfigureWindowAux, InputFocus, MapState, StackMode, Window}; -use x11rb::protocol::xproto::{ChangeWindowAttributesAux, EventMask}; +use x11rb::protocol::xproto::EventMask; +use x11rb::protocol::xproto::InputFocus; +use x11rb::protocol::xproto::MapState; +use x11rb::protocol::xproto::StackMode; +use x11rb::protocol::xproto::Window; use x11rb::protocol::Event; use x11rb::rust_connection::RustConnection; -use crate::plugins::applications::{linux, ApplicationContext, DesktopEnvironment}; + +use crate::plugins::applications::linux; +use crate::plugins::applications::linux::x11; +use crate::plugins::applications::ApplicationContext; +use crate::plugins::applications::DesktopEnvironment; pub struct X11DesktopEnvironment { receiver: Rc>>, @@ -49,12 +66,12 @@ enum JsX11ApplicationEvent { id: String, parent_id: String, override_redirect: bool, - mapped: bool + mapped: bool, }, CreateNotify { id: String, parent_id: String, - override_redirect: bool + override_redirect: bool, }, DestroyNotify { id: String, @@ -70,12 +87,12 @@ enum JsX11ApplicationEvent { }, TitlePropertyNotify { id: String, - title: String + title: String, }, ClassPropertyNotify { id: String, class: String, - instance: String + instance: String, }, HintsPropertyNotify { id: String, @@ -91,11 +108,11 @@ enum JsX11ApplicationEvent { }, WindowTypePropertyNotify { id: String, - window_types: Vec + window_types: Vec, }, DesktopFileNamePropertyNotify { id: String, - desktop_file_name: String + desktop_file_name: String, }, } @@ -119,24 +136,25 @@ enum JSX11WindowType { Utility, } - #[op2(async)] #[serde] pub async fn application_x11_pending_event(state: Rc>) -> anyhow::Result { let receiver = { let state = state.borrow(); - let context = state - .borrow::(); + let context = state.borrow::(); match &context.desktop { - DesktopEnvironment::Linux(linux::LinuxDesktopEnvironment::X11(X11DesktopEnvironment { receiver })) => receiver.clone(), - _ => Err(anyhow!("Calling application_x11_pending_event on non-x11 platform"))? + DesktopEnvironment::Linux(linux::LinuxDesktopEnvironment::X11(X11DesktopEnvironment { receiver })) => { + receiver.clone() + } + _ => Err(anyhow!("Calling application_x11_pending_event on non-x11 platform"))?, } }; let mut receiver = receiver.borrow_mut(); - let event = receiver.recv() + let event = receiver + .recv() .await .ok_or_else(|| anyhow!("plugin event stream was suddenly closed"))?; @@ -145,10 +163,8 @@ pub async fn application_x11_pending_event(state: Rc>) -> anyho Ok(event) } - #[op2(fast)] pub fn linux_x11_focus_window(#[string] x11_window_id: String) -> anyhow::Result<()> { - focus_window(x11_window_id)?; Ok(()) @@ -167,7 +183,8 @@ fn focus_window(window_id: String) -> anyhow::Result<()> { // TODO change desktop? // https://github.com/davatorium/rofi/blob/f9491690fdfbffc5fe13438b26323c05c67acd7b/source/modes/window.c#L820 - let active_window_property = conn.get_property(false, screen.root, atoms._NET_ACTIVE_WINDOW, AtomEnum::WINDOW, 0, 2048)? + let active_window_property = conn + .get_property(false, screen.root, atoms._NET_ACTIVE_WINDOW, AtomEnum::WINDOW, 0, 2048)? .reply()?; let current_active_window = active_window_property @@ -183,20 +200,14 @@ fn focus_window(window_id: String) -> anyhow::Result<()> { 32, window_to_activate, atoms._NET_ACTIVE_WINDOW, - [ - source_indication, - timestamp, - current_active_window, - 0, - 0 - ], + [source_indication, timestamp, current_active_window, 0, 0], ); conn.send_event( false, screen.root, EventMask::SUBSTRUCTURE_NOTIFY | EventMask::SUBSTRUCTURE_REDIRECT, - event + event, )?; conn.flush()?; @@ -213,16 +224,12 @@ fn send_event(tokio_handle: &Handle, sender: &Sender, app }); } -pub fn listen_on_x11_events( - tokio_handle: Handle, - sender: Sender, -) -> anyhow::Result { +pub fn listen_on_x11_events(tokio_handle: Handle, sender: Sender) -> anyhow::Result { let (conn, screen_num) = RustConnection::connect(None)?; let screen = &conn.setup().roots[screen_num]; let atoms = atoms::Atoms::new(&conn)?.reply()?; - let aux = ChangeWindowAttributesAux::new() - .event_mask(EventMask::SUBSTRUCTURE_NOTIFY | EventMask::PROPERTY_CHANGE); + let aux = ChangeWindowAttributesAux::new().event_mask(EventMask::SUBSTRUCTURE_NOTIFY | EventMask::PROPERTY_CHANGE); conn.change_window_attributes(screen.root, &aux)?.check()?; @@ -231,13 +238,7 @@ pub fn listen_on_x11_events( let _ = fetch_existing_windows(screen.root, &conn, &tokio_handle, &sender, atoms, &mut init_window_data); for window_id in init_window_data { - update_properties( - window_id, - &conn, - &tokio_handle, - &sender, - atoms, - ); + update_properties(window_id, &conn, &tokio_handle, &sender, atoms); } loop { @@ -245,47 +246,66 @@ pub fn listen_on_x11_events( Event::CreateNotify(event) => { tracing::trace!("CreateNotify: {:?}", event); - let aux = ChangeWindowAttributesAux::new() - .event_mask(EventMask::PROPERTY_CHANGE); + let aux = ChangeWindowAttributesAux::new().event_mask(EventMask::PROPERTY_CHANGE); conn.change_window_attributes(event.window, &aux)?; conn.flush()?; - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::CreateNotify { - id: format!("{}", event.window), - parent_id: format!("{}", event.parent), - override_redirect: event.override_redirect - }); + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::CreateNotify { + id: format!("{}", event.window), + parent_id: format!("{}", event.parent), + override_redirect: event.override_redirect, + }, + ); update_properties(event.window, &conn, &tokio_handle, &sender, atoms); } Event::DestroyNotify(event) => { tracing::trace!("DestroyNotify: {:?}", event); - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::DestroyNotify { - id: format!("{}", event.window), - }) + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::DestroyNotify { + id: format!("{}", event.window), + }, + ) } Event::MapNotify(event) => { tracing::trace!("MapNotify: {:?}", event); - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::MapNotify { - id: format!("{}", event.window), - }); + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::MapNotify { + id: format!("{}", event.window), + }, + ); } Event::UnmapNotify(event) => { tracing::trace!("UnmapNotify: {:?}", event); - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::UnmapNotify { - id: format!("{}", event.window), - }); + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::UnmapNotify { + id: format!("{}", event.window), + }, + ); } Event::ReparentNotify(event) => { tracing::trace!("ReparentNotify: {:?}", event); - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::ReparentNotify { - id: format!("{}", event.window), - }); + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::ReparentNotify { + id: format!("{}", event.window), + }, + ); } Event::PropertyNotify(event) => { tracing::trace!("PropertyNotify: {:?}", event); @@ -296,23 +316,23 @@ pub fn listen_on_x11_events( } atom if atom == Atom::from(AtomEnum::WM_CLASS) => { let _ = update_class(event.window, &conn, &tokio_handle, &sender); - }, + } atom if atom == atoms.WM_PROTOCOLS => { let _ = update_protocols(event.window, &conn, &tokio_handle, &sender, atoms); - }, + } atom if atom == atoms.WM_HINTS => { let _ = update_hints(event.window, &conn, &tokio_handle, &sender); - }, + } atom if atom == Atom::from(AtomEnum::WM_TRANSIENT_FOR) => { let _ = update_transient_for(event.window, &conn, &tokio_handle, &sender); - }, + } atom if atom == atoms._NET_WM_WINDOW_TYPE => { let _ = update_net_window_type(event.window, &conn, &tokio_handle, &sender, atoms); - }, + } atom if atom == atoms._KDE_NET_WM_DESKTOP_FILE || atom == atoms._GTK_APPLICATION_ID => { let _ = update_desktop_file_name(event.window, &conn, &tokio_handle, &sender, atoms); - }, - _ => {}, + } + _ => {} } } _ => {} @@ -326,7 +346,7 @@ fn fetch_existing_windows( tokio_handle: &Handle, sender: &Sender, atoms: atoms::Atoms, - init_window_data: &mut Vec + init_window_data: &mut Vec, ) -> anyhow::Result<()> { let query_tree = conn.query_tree(window_id)?.reply()?; @@ -334,17 +354,21 @@ fn fetch_existing_windows( init_window_data.push(window_id); - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::Init { - id: format!("{}", window_id), - parent_id: format!("{}", query_tree.parent), - override_redirect: attributes.override_redirect, - mapped: match attributes.map_state { - MapState::UNMAPPED => false, - MapState::UNVIEWABLE => true, - MapState::VIEWABLE => true, - unknown @ _ => Err(anyhow::anyhow!("Unknown map state: {:?}", unknown))? + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::Init { + id: format!("{}", window_id), + parent_id: format!("{}", query_tree.parent), + override_redirect: attributes.override_redirect, + mapped: match attributes.map_state { + MapState::UNMAPPED => false, + MapState::UNVIEWABLE => true, + MapState::VIEWABLE => true, + unknown @ _ => Err(anyhow::anyhow!("Unknown map state: {:?}", unknown))?, + }, }, - }); + ); for window in query_tree.children { let _ = fetch_existing_windows(window, conn, tokio_handle, sender, atoms, init_window_data); @@ -353,13 +377,12 @@ fn fetch_existing_windows( Ok(()) } - fn update_properties( window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender, - atoms: atoms::Atoms + atoms: atoms::Atoms, ) { let _ = update_title(window_id, conn, tokio_handle, sender, atoms); let _ = update_class(window_id, conn, tokio_handle, sender); @@ -370,7 +393,13 @@ fn update_properties( let _ = update_desktop_file_name(window_id, &conn, &tokio_handle, &sender, atoms); } -fn update_title(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender, atoms: atoms::Atoms) -> anyhow::Result<()> { +fn update_title( + window_id: Window, + conn: &RustConnection, + tokio_handle: &Handle, + sender: &Sender, + atoms: atoms::Atoms, +) -> anyhow::Result<()> { let net_wm_name = read_window_property_string(window_id, conn, atoms, atoms._NET_WM_NAME)?; let wm_name = read_window_property_string(window_id, conn, atoms, AtomEnum::WM_NAME)?; @@ -379,15 +408,24 @@ fn update_title(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, let title = net_wm_name.or(wm_name).unwrap_or_default(); - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::TitlePropertyNotify { - id: format!("{}", window_id), - title - }); + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::TitlePropertyNotify { + id: format!("{}", window_id), + title, + }, + ); Ok(()) } -fn update_class(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender) -> anyhow::Result<()> { +fn update_class( + window_id: Window, + conn: &RustConnection, + tokio_handle: &Handle, + sender: &Sender, +) -> anyhow::Result<()> { let (class, instance) = match WmClass::get(conn, window_id)?.reply() { Ok(Some(wm_class)) => { let class = encoding::all::ISO_8859_1 @@ -401,21 +439,30 @@ fn update_class(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, .unwrap_or_default(); (class, instance) - }, + } Ok(None) => (Default::default(), Default::default()), Err(err) => Err(err)?, }; - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::ClassPropertyNotify { - id: format!("{}", window_id), - class, - instance - }); + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::ClassPropertyNotify { + id: format!("{}", window_id), + class, + instance, + }, + ); Ok(()) } -fn update_hints(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender) -> anyhow::Result<()> { +fn update_hints( + window_id: Window, + conn: &RustConnection, + tokio_handle: &Handle, + sender: &Sender, +) -> anyhow::Result<()> { let hints = match WmHints::get(conn, window_id)?.reply() { Ok(hints) => hints, Err(err) => Err(err)?, @@ -425,66 +472,95 @@ fn update_hints(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, .and_then(|hints| hints.window_group) .map(|window| format!("{}", window)); - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::HintsPropertyNotify { - id: format!("{}", window_id), - window_group - }); - + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::HintsPropertyNotify { + id: format!("{}", window_id), + window_group, + }, + ); + Ok(()) } -fn update_protocols(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender, atoms: atoms::Atoms) -> anyhow::Result<()> { +fn update_protocols( + window_id: Window, + conn: &RustConnection, + tokio_handle: &Handle, + sender: &Sender, + atoms: atoms::Atoms, +) -> anyhow::Result<()> { let reply = conn .get_property(false, window_id, atoms.WM_PROTOCOLS, AtomEnum::ATOM, 0, 2048)? .reply()?; - let protocols = reply.value32() - .map(|vals| vals.collect::>()); + let protocols = reply.value32().map(|vals| vals.collect::>()); let Some(protocols) = protocols else { - return Ok(()) + return Ok(()); }; let protocols = protocols .into_iter() - .filter_map(|atom| match atom { - x if x == atoms.WM_TAKE_FOCUS => Some(JSX11WindowProtocol::TakeFocus), - x if x == atoms.WM_DELETE_WINDOW => Some(JSX11WindowProtocol::DeleteWindow), - _ => None, + .filter_map(|atom| { + match atom { + x if x == atoms.WM_TAKE_FOCUS => Some(JSX11WindowProtocol::TakeFocus), + x if x == atoms.WM_DELETE_WINDOW => Some(JSX11WindowProtocol::DeleteWindow), + _ => None, + } }) .collect::>(); - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::ProtocolsPropertyNotify { - id: format!("{}", window_id), - protocols - }); + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::ProtocolsPropertyNotify { + id: format!("{}", window_id), + protocols, + }, + ); Ok(()) } -fn update_transient_for(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender) -> anyhow::Result<()> { - let reply = conn.get_property(false, window_id, AtomEnum::WM_TRANSIENT_FOR, AtomEnum::WINDOW, 0, 2048)? +fn update_transient_for( + window_id: Window, + conn: &RustConnection, + tokio_handle: &Handle, + sender: &Sender, +) -> anyhow::Result<()> { + let reply = conn + .get_property(false, window_id, AtomEnum::WM_TRANSIENT_FOR, AtomEnum::WINDOW, 0, 2048)? .reply()?; - let transient_for = reply - .value32() - .and_then(|mut iter| iter.next()) - .filter(|w| *w != 0); + let transient_for = reply.value32().and_then(|mut iter| iter.next()).filter(|w| *w != 0); - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::TransientForPropertyNotify { - id: format!("{}", window_id), - transient_for: transient_for.map(|window_id| format!("{}", window_id)) - }); + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::TransientForPropertyNotify { + id: format!("{}", window_id), + transient_for: transient_for.map(|window_id| format!("{}", window_id)), + }, + ); Ok(()) } -fn update_net_window_type(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender, atoms: atoms::Atoms) -> anyhow::Result<()> { +fn update_net_window_type( + window_id: Window, + conn: &RustConnection, + tokio_handle: &Handle, + sender: &Sender, + atoms: atoms::Atoms, +) -> anyhow::Result<()> { let reply = conn .get_property(false, window_id, atoms._NET_WM_WINDOW_TYPE, AtomEnum::ATOM, 0, 1024)? .reply()?; - let window_types = reply.value32() + let window_types = reply + .value32() .map(|iter| iter.collect::>()) .unwrap_or_default() .into_iter() @@ -500,55 +576,70 @@ fn update_net_window_type(window_id: Window, conn: &RustConnection, tokio_handle atom if atom == atoms._NET_WM_WINDOW_TYPE_TOOLBAR => Some(JSX11WindowType::Toolbar), atom if atom == atoms._NET_WM_WINDOW_TYPE_TOOLTIP => Some(JSX11WindowType::Tooltip), atom if atom == atoms._NET_WM_WINDOW_TYPE_UTILITY => Some(JSX11WindowType::Utility), - _ => None + _ => None, } }) .collect(); - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::WindowTypePropertyNotify { - id: format!("{}", window_id), - window_types - }); + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::WindowTypePropertyNotify { + id: format!("{}", window_id), + window_types, + }, + ); Ok(()) } -fn update_desktop_file_name(window_id: Window, conn: &RustConnection, tokio_handle: &Handle, sender: &Sender, atoms: atoms::Atoms) -> anyhow::Result<()> { +fn update_desktop_file_name( + window_id: Window, + conn: &RustConnection, + tokio_handle: &Handle, + sender: &Sender, + atoms: atoms::Atoms, +) -> anyhow::Result<()> { let kde_net_wm_desktop_file = read_window_property_string(window_id, conn, atoms, atoms._KDE_NET_WM_DESKTOP_FILE)?; let gtk_application_id = read_window_property_string(window_id, conn, atoms, atoms._GTK_APPLICATION_ID)?; let desktop_file_name = kde_net_wm_desktop_file.or(gtk_application_id).unwrap_or_default(); - send_event(&tokio_handle, &sender, JsX11ApplicationEvent::DesktopFileNamePropertyNotify { - id: format!("{}", window_id), - desktop_file_name - }); + send_event( + &tokio_handle, + &sender, + JsX11ApplicationEvent::DesktopFileNamePropertyNotify { + id: format!("{}", window_id), + desktop_file_name, + }, + ); Ok(()) } -fn read_window_property_string(window_id: Window, conn: &RustConnection, atoms: atoms::Atoms, atom: impl Into) -> anyhow::Result> { +fn read_window_property_string( + window_id: Window, + conn: &RustConnection, + atoms: atoms::Atoms, + atom: impl Into, +) -> anyhow::Result> { let reply = conn .get_property(false, window_id, atom, AtomEnum::ANY, 0, 2048)? .reply()?; let Some(bytes) = reply.value8() else { - return Ok(None) + return Ok(None); }; let bytes = bytes.collect::>(); match reply.type_ { x if x == Atom::from(AtomEnum::STRING) => { - let decoded = encoding::all::ISO_8859_1 - .decode(&bytes, DecoderTrap::Replace) - .ok(); + let decoded = encoding::all::ISO_8859_1.decode(&bytes, DecoderTrap::Replace).ok(); Ok(decoded) - }, - x if x == atoms.UTF8_STRING => { - Ok(String::from_utf8(bytes).ok()) - }, + } + x if x == atoms.UTF8_STRING => Ok(String::from_utf8(bytes).ok()), _ => Ok(None), } } @@ -586,5 +677,3 @@ mod atoms { } } } - - diff --git a/rust/plugin_runtime/src/plugins/applications/macos.rs b/rust/plugin_runtime/src/plugins/applications/macos.rs index 0a23e64..b0ce706 100644 --- a/rust/plugin_runtime/src/plugins/applications/macos.rs +++ b/rust/plugin_runtime/src/plugins/applications/macos.rs @@ -1,28 +1,52 @@ use std::collections::HashMap; use std::error::Error; use std::ffi::OsStr; -use std::path::{Component, Path, PathBuf}; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; -use anyhow::{anyhow, Context}; -use cacao::filesystem::{FileManager, SearchPathDirectory, SearchPathDomainMask}; +use anyhow::anyhow; +use anyhow::Context; +use cacao::filesystem::FileManager; +use cacao::filesystem::SearchPathDirectory; +use cacao::filesystem::SearchPathDomainMask; use cacao::url::Url; use objc2::ClassType; -use objc2_app_kit::{NSBitmapImageRep, NSCalibratedWhiteColorSpace, NSCompositeCopy, NSDeviceRGBColorSpace, NSGraphicsContext, NSImage, NSPNGFileType, NSWorkspace}; -use objc2_foundation::{CGFloat, CGPoint, CGRect, NSDictionary, NSInteger, NSPoint, NSRect, NSSize, NSString, NSZeroRect}; +use objc2_app_kit::NSBitmapImageRep; +use objc2_app_kit::NSCalibratedWhiteColorSpace; +use objc2_app_kit::NSCompositeCopy; +use objc2_app_kit::NSDeviceRGBColorSpace; +use objc2_app_kit::NSGraphicsContext; +use objc2_app_kit::NSImage; +use objc2_app_kit::NSPNGFileType; +use objc2_app_kit::NSWorkspace; +use objc2_foundation::CGFloat; +use objc2_foundation::CGPoint; +use objc2_foundation::CGRect; +use objc2_foundation::NSDictionary; +use objc2_foundation::NSInteger; +use objc2_foundation::NSPoint; +use objc2_foundation::NSRect; +use objc2_foundation::NSSize; +use objc2_foundation::NSString; +use objc2_foundation::NSZeroRect; use plist::Dictionary; use regex::Regex; use serde::Deserialize; -use crate::plugins::applications::{DesktopApplication, DesktopPathAction, DesktopSettings13AndPostData, DesktopSettingsPre13Data}; +use crate::plugins::applications::DesktopApplication; +use crate::plugins::applications::DesktopPathAction; +use crate::plugins::applications::DesktopSettings13AndPostData; +use crate::plugins::applications::DesktopSettingsPre13Data; pub fn macos_major_version() -> u8 { let system_version: SystemVersion = plist::from_file("/System/Library/CoreServices/SystemVersion.plist") .expect("SystemVersion.plist doesn't follow expected format"); - let regex = Regex::new(r"^(?\d+).\d+(.\d+)?$") - .expect("This regex cannot be invalid"); + let regex = Regex::new(r"^(?\d+).\d+(.\d+)?$").expect("This regex cannot be invalid"); - let captures = regex.captures(&system_version.product_version) + let captures = regex + .captures(&system_version.product_version) .expect("SystemVersion.plist ProductVersion doesn't match expected format"); let major_version: u8 = captures["major"] @@ -36,48 +60,49 @@ pub fn macos_major_version() -> u8 { pub fn macos_system_applications() -> Vec { let finder_application = vec![PathBuf::from("/System/Library/CoreServices/Finder.app")]; - let finder_applications = get_applications_in_dir(PathBuf::from("/System/Library/CoreServices/Finder.app/Contents/Applications")); + let finder_applications = get_applications_in_dir(PathBuf::from( + "/System/Library/CoreServices/Finder.app/Contents/Applications", + )); - let core_services_applications = get_applications_in_dir(PathBuf::from("/System/Library/CoreServices/Applications")); + let core_services_applications = + get_applications_in_dir(PathBuf::from("/System/Library/CoreServices/Applications")); - let all_applications = [ - finder_application, - finder_applications, - core_services_applications, - ]; + let all_applications = [finder_application, finder_applications, core_services_applications]; - let all_applications: Vec<_> = all_applications - .into_iter() - .flatten() - .collect(); + let all_applications: Vec<_> = all_applications.into_iter().flatten().collect(); all_applications } - pub fn macos_application_dirs() -> Vec { let file_manager = FileManager::default(); - let user_applications_dir = get_path(&file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::User); - let local_applications_dir = get_path(&file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::Local); - let system_applications_dir = get_path(&file_manager, SearchPathDirectory::Applications, SearchPathDomainMask::Domain); + let user_applications_dir = get_path( + &file_manager, + SearchPathDirectory::Applications, + SearchPathDomainMask::User, + ); + let local_applications_dir = get_path( + &file_manager, + SearchPathDirectory::Applications, + SearchPathDomainMask::Local, + ); + let system_applications_dir = get_path( + &file_manager, + SearchPathDirectory::Applications, + SearchPathDomainMask::Domain, + ); - let all_applications = [ - user_applications_dir, - local_applications_dir, - system_applications_dir, - ]; + let all_applications = [user_applications_dir, local_applications_dir, system_applications_dir]; - let all_applications: Vec<_> = all_applications - .into_iter() - .flatten() - .collect(); + let all_applications: Vec<_> = all_applications.into_iter().flatten().collect(); all_applications } pub fn macos_app_from_arbitrary_path(path: PathBuf) -> Option { - let path = path.ancestors() + let path = path + .ancestors() .into_iter() .collect::>() .into_iter() @@ -103,10 +128,11 @@ pub fn macos_app_from_arbitrary_path(path: PathBuf) -> Option pub fn macos_app_from_path(path: &Path) -> Option { if !path.is_dir() { - return None + return None; } - let name = path.file_stem() + let name = path + .file_stem() .expect(&format!("invalid path: {:?}", path)) .to_str() .expect("non-uft8 paths are not supported") @@ -114,10 +140,10 @@ pub fn macos_app_from_path(path: &Path) -> Option { let info_path = path.join("Contents").join("Info.plist"); - let info: Option = plist::from_file(info_path) - .ok(); + let info: Option = plist::from_file(info_path).ok(); - let name = info.as_ref() + let name = info + .as_ref() .and_then(|info| info.bundle_display_name.clone().or_else(|| info.bundle_name.clone())) .unwrap_or(name); @@ -129,37 +155,34 @@ pub fn macos_app_from_path(path: &Path) -> Option { Some(DesktopPathAction::Add { id: path.clone(), - data: DesktopApplication { - name, - path, - icon, - }, + data: DesktopApplication { name, path, icon }, }) } pub fn macos_settings_pre_13() -> Vec { let file_manager = FileManager::default(); - let user_pref_panes_dir = get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::User); - let local_pref_panes_dir = get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Local); - let system_pref_panes_dir = get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Domain); + let user_pref_panes_dir = + get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::User); + let local_pref_panes_dir = + get_pref_panes_with_kind(&file_manager, SearchPathDirectory::Library, SearchPathDomainMask::Local); + let system_pref_panes_dir = get_pref_panes_with_kind( + &file_manager, + SearchPathDirectory::Library, + SearchPathDomainMask::Domain, + ); - let all_settings = [ - user_pref_panes_dir, - local_pref_panes_dir, - system_pref_panes_dir, - ]; + let all_settings = [user_pref_panes_dir, local_pref_panes_dir, system_pref_panes_dir]; - let all_settings: Vec<_> = all_settings - .into_iter() - .flatten() - .collect(); + let all_settings: Vec<_> = all_settings.into_iter().flatten().collect(); tracing::debug!("Found following macOS settings: {:?}", all_settings); - let all_settings = all_settings.into_iter() + let all_settings = all_settings + .into_iter() .map(|path| { - let name = path.file_stem() // TODO is there a proper way got get the name? + let name = path + .file_stem() // TODO is there a proper way got get the name? .expect(&format!("invalid path: {:?}", &path)) .to_string_lossy() .to_string(); @@ -176,13 +199,17 @@ pub fn macos_settings_pre_13() -> Vec { } pub fn macos_settings_13_and_post() -> Vec { - let sidebar: Vec = plist::from_file("/System/Applications/System Settings.app/Contents/Resources/Sidebar.plist") - .expect("Sidebar.plist doesn't follow expected format"); + let sidebar: Vec = + plist::from_file("/System/Applications/System Settings.app/Contents/Resources/Sidebar.plist") + .expect("Sidebar.plist doesn't follow expected format"); - let preferences_ids: Vec<_> = sidebar.into_iter() - .flat_map(|section| match section { - SidebarSection::Content { content } => content, - SidebarSection::Title { .. } => vec![] + let preferences_ids: Vec<_> = sidebar + .into_iter() + .flat_map(|section| { + match section { + SidebarSection::Content { content } => content, + SidebarSection::Title { .. } => vec![], + } }) .collect(); @@ -192,17 +219,21 @@ pub fn macos_settings_13_and_post() -> Vec { .into_iter() .filter_map(|path| { fn read_plist(path: &Path) -> anyhow::Result<(String, (String, PathBuf))> { - let name = path.file_stem() + let name = path + .file_stem() .expect(&format!("invalid path: {:?}", path)) .to_string_lossy() .to_string(); let info_path = path.join("Contents").join("Info.plist"); - let info = plist::from_file::<_, Info>(info_path.as_path()) - .context(format!("Unexpected Info.plist for System Extensions: {}", &info_path.display()))?; + let info = plist::from_file::<_, Info>(info_path.as_path()).context(format!( + "Unexpected Info.plist for System Extensions: {}", + &info_path.display() + ))?; - let name = info.bundle_display_name + let name = info + .bundle_display_name .clone() .or_else(|| info.bundle_name.clone()) .unwrap_or(name); @@ -211,14 +242,17 @@ pub fn macos_settings_13_and_post() -> Vec { } read_plist(&path) - .inspect_err(|err| tracing::error!("error while reading system extension Info.plist {:?}: {:?}", path, err)) + .inspect_err(|err| { + tracing::error!("error while reading system extension Info.plist {:?}: {:?}", path, err) + }) .ok() }) .collect(); tracing::debug!("Found following macOS setting extensions: {:?}", &extensions); - preferences_ids.into_iter() + preferences_ids + .into_iter() .filter_map(|preferences_id| { match extensions.get(&preferences_id) { None => { @@ -229,27 +263,37 @@ pub fn macos_settings_13_and_post() -> Vec { } Some((name, path)) => { let icon = get_application_icon(&path) - .inspect_err(|err| tracing::error!("error while reading application icon for {:?}: {:?}", path, err)) + .inspect_err(|err| { + tracing::error!("error while reading application icon for {:?}: {:?}", path, err) + }) .ok(); - Some( - DesktopSettings13AndPostData { - name: name.to_string(), - preferences_id, - icon, - } - ) + Some(DesktopSettings13AndPostData { + name: name.to_string(), + preferences_id, + icon, + }) } } }) .collect() } -fn get_pref_panes_with_kind(file_manager: &FileManager, directory: SearchPathDirectory, mask: SearchPathDomainMask) -> Vec { - get_items_with_kind(file_manager, directory, mask, Some("PreferencePanes"), |dir| get_pref_panes_in_dir(dir)) +fn get_pref_panes_with_kind( + file_manager: &FileManager, + directory: SearchPathDirectory, + mask: SearchPathDomainMask, +) -> Vec { + get_items_with_kind(file_manager, directory, mask, Some("PreferencePanes"), |dir| { + get_pref_panes_in_dir(dir) + }) } -fn get_applications_with_kind(file_manager: &FileManager, directory: SearchPathDirectory, mask: SearchPathDomainMask) -> Vec { +fn get_applications_with_kind( + file_manager: &FileManager, + directory: SearchPathDirectory, + mask: SearchPathDomainMask, +) -> Vec { get_items_with_kind(file_manager, directory, mask, None, |dir| get_applications_in_dir(dir)) } @@ -258,17 +302,18 @@ fn get_items_with_kind( directory: SearchPathDirectory, mask: SearchPathDomainMask, suffix: Option<&'static str>, - read_fn: F -) -> Vec where F: Fn(PathBuf) -> Vec + read_fn: F, +) -> Vec +where + F: Fn(PathBuf) -> Vec, { match file_manager.get_directory(directory.clone(), mask.clone()) { Ok(url) => { - let applications_dir = url.to_file_path() - .expect("returned application url is not a file path"); + let applications_dir = url.to_file_path().expect("returned application url is not a file path"); let applications_dir = match suffix { Some(suffix) => applications_dir.join(suffix), - None => applications_dir + None => applications_dir, }; tracing::debug!("reading {:?} {:?} directory: {:?}", directory, mask, &applications_dir); @@ -283,15 +328,10 @@ fn get_items_with_kind( } } -fn get_path( - file_manager: &FileManager, - directory: SearchPathDirectory, - mask: SearchPathDomainMask, -) -> Option { +fn get_path(file_manager: &FileManager, directory: SearchPathDirectory, mask: SearchPathDomainMask) -> Option { match file_manager.get_directory(directory.clone(), mask.clone()) { Ok(url) => { - let applications_dir = url.to_file_path() - .expect("returned application url is not a file path"); + let applications_dir = url.to_file_path().expect("returned application url is not a file path"); Some(applications_dir) } @@ -336,7 +376,7 @@ fn get_items_in_dir(path: PathBuf, extension: &str) -> Vec { }) .collect::>() } - Err(_) => vec![] + Err(_) => vec![], } } @@ -383,15 +423,15 @@ fn get_application_icon(app_path: &Path) -> anyhow::Result> { unsafe { let workspace = NSWorkspace::sharedWorkspace(); - let app_path = app_path.to_str() + let app_path = app_path + .to_str() .context(format!("Application path is not a utf-8 string: {:?}", &app_path))?; let app_path = NSString::from_str(app_path); let image = workspace.iconForFile(&app_path); - let bytes = resize_ns_image(&image, 40, 40) - .ok_or(anyhow!("Unable to resize the image"))?; + let bytes = resize_ns_image(&image, 40, 40).ok_or(anyhow!("Unable to resize the image"))?; Ok(bytes) } @@ -423,10 +463,6 @@ struct SystemVersion { #[derive(Deserialize)] #[serde(untagged)] enum SidebarSection { - Content { - content: Vec - }, - Title { - title: String - } -} \ No newline at end of file + Content { content: Vec }, + Title { title: String }, +} diff --git a/rust/plugin_runtime/src/plugins/applications/windows.rs b/rust/plugin_runtime/src/plugins/applications/windows.rs index 4a2cd06..febb913 100644 --- a/rust/plugin_runtime/src/plugins/applications/windows.rs +++ b/rust/plugin_runtime/src/plugins/applications/windows.rs @@ -1,19 +1,30 @@ -use std::{mem, ptr}; use std::io::Cursor; +use std::mem; use std::mem::MaybeUninit; -use crate::plugins::applications::{resize_icon, DesktopApplication, DesktopPathAction}; -use deno_core::op2; use std::path::PathBuf; -use anyhow::{anyhow, Context}; +use std::ptr; + +use anyhow::anyhow; +use anyhow::Context; +use deno_core::op2; use image::RgbaImage; use tokio::task::spawn_blocking; -use windows::core::{GUID, HSTRING, PWSTR}; -use windows::Win32::Foundation::{HANDLE, HWND}; +use windows::core::GUID; +use windows::core::HSTRING; +use windows::core::PWSTR; +use windows::Win32::Foundation::HANDLE; +use windows::Win32::Foundation::HWND; use windows::Win32::Graphics::Gdi; use windows::Win32::Graphics::Gdi::HDC; use windows::Win32::Storage::FileSystem; -use windows::Win32::UI::{Controls, Shell, WindowsAndMessaging}; +use windows::Win32::UI::Controls; use windows::Win32::UI::Controls::HIMAGELIST; +use windows::Win32::UI::Shell; +use windows::Win32::UI::WindowsAndMessaging; + +use crate::plugins::applications::resize_icon; +use crate::plugins::applications::DesktopApplication; +use crate::plugins::applications::DesktopPathAction; deno_core::extension!( gauntlet_internal_windows, @@ -24,12 +35,12 @@ deno_core::extension!( ], esm_entry_point = "ext:gauntlet/internal-windows/bootstrap.js", esm = [ - "ext:gauntlet/internal-windows/bootstrap.js" = "../../js/bridge_build/dist/bridge-internal-windows-bootstrap.js", - "ext:gauntlet/internal-windows.js" = "../../js/core/dist/internal-windows.js", + "ext:gauntlet/internal-windows/bootstrap.js" = + "../../js/bridge_build/dist/bridge-internal-windows-bootstrap.js", + "ext:gauntlet/internal-windows.js" = "../../js/core/dist/internal-windows.js", ] ); - #[op2] #[serde] fn windows_application_dirs() -> Vec { @@ -39,9 +50,9 @@ fn windows_application_dirs() -> Vec { known_folder(&Shell::FOLDERID_StartMenu), known_folder(&Shell::FOLDERID_CommonStartMenu), ] - .into_iter() - .flatten() - .collect() + .into_iter() + .flatten() + .collect() } #[op2] @@ -74,9 +85,7 @@ fn windows_app_from_path_blocking(file_path: String) -> anyhow::Result anyhow::Result> { let icon = image_list.GetIcon(shfileinfow.iIcon, Controls::ILD_TRANSPARENT.0)?; let mut icon_info = WindowsAndMessaging::ICONINFO::default(); - WindowsAndMessaging::GetIconInfo(icon, &mut icon_info) - .context("Unable to GetIconInfo")?; + WindowsAndMessaging::GetIconInfo(icon, &mut icon_info).context("Unable to GetIconInfo")?; let _ = Gdi::DeleteObject(icon_info.hbmMask); @@ -161,7 +169,7 @@ fn extract_icon(file_path: &str) -> anyhow::Result> { let _ = Gdi::DeleteObject(icon_info.hbmColor); let image_buffer = RgbaImage::from_fn(bitmap.bmWidth as u32, bitmap.bmHeight as u32, |x, y| { - let idx= y as usize * bitmap.bmWidth as usize + x as usize; + let idx = y as usize * bitmap.bmWidth as usize + x as usize; let [b, g, r, a] = bits[idx].to_le_bytes(); [r, g, b, a].into() }); @@ -172,7 +180,8 @@ fn extract_icon(file_path: &str) -> anyhow::Result> { let mut result = Cursor::new(vec![]); - rgba_image.write_to(&mut result, image::ImageFormat::Png) + rgba_image + .write_to(&mut result, image::ImageFormat::Png) .expect("should be able to convert to png"); let data = result.into_inner(); @@ -183,7 +192,6 @@ fn extract_icon(file_path: &str) -> anyhow::Result> { } } - fn extract_name(file_path: &str) -> anyhow::Result { let mut file_info = Shell::SHFILEINFOW::default(); @@ -197,11 +205,7 @@ fn extract_name(file_path: &str) -> anyhow::Result { ); } - let strlen = file_info - .szDisplayName - .iter() - .position(|x| *x == 0) - .unwrap(); + let strlen = file_info.szDisplayName.iter().position(|x| *x == 0).unwrap(); Ok(String::from_utf16(&file_info.szDisplayName[..strlen])?) } diff --git a/rust/plugin_runtime/src/plugins/numbat.rs b/rust/plugin_runtime/src/plugins/numbat.rs index 132dbf7..02f267f 100644 --- a/rust/plugin_runtime/src/plugins/numbat.rs +++ b/rust/plugin_runtime/src/plugins/numbat.rs @@ -1,13 +1,17 @@ +use std::cell::RefCell; +use std::rc::Rc; + use anyhow::anyhow; -use deno_core::{op2, OpState}; -use numbat::markup::{Formatter, PlainTextFormatter}; +use deno_core::op2; +use deno_core::OpState; +use numbat::markup::Formatter; +use numbat::markup::PlainTextFormatter; use numbat::module_importer::BuiltinModuleImporter; use numbat::pretty_print::PrettyPrint; use numbat::resolver::CodeSource; -use numbat::{Context, InterpreterResult}; +use numbat::Context; +use numbat::InterpreterResult; use serde::Serialize; -use std::cell::RefCell; -use std::rc::Rc; #[derive(Clone)] pub struct NumbatContext(Rc>); @@ -40,9 +44,7 @@ pub fn run_numbat(state: Rc>, #[string] input: String) -> anyho let context = { let state = state.borrow(); - let context = state - .borrow::() - .clone(); + let context = state.borrow::().clone(); context }; @@ -62,11 +64,11 @@ pub fn run_numbat(state: Rc>, #[string] input: String) -> anyho let value = match result { InterpreterResult::Value(value) => format!("{}", value.pretty_print()), - InterpreterResult::Continue => Err(anyhow!("numbat returned Continue"))? + InterpreterResult::Continue => Err(anyhow!("numbat returned Continue"))?, }; Ok(NumbatResult { left: expression, - right: value + right: value, }) -} \ No newline at end of file +} diff --git a/rust/plugin_runtime/src/plugins/settings.rs b/rust/plugin_runtime/src/plugins/settings.rs index df880e4..ddac7cd 100644 --- a/rust/plugin_runtime/src/plugins/settings.rs +++ b/rust/plugin_runtime/src/plugins/settings.rs @@ -7,4 +7,4 @@ pub fn open_settings() -> anyhow::Result<()> { .spawn()?; Ok(()) -} \ No newline at end of file +} diff --git a/rust/plugin_runtime/src/preferences.rs b/rust/plugin_runtime/src/preferences.rs index 2309d76..821b0ac 100644 --- a/rust/plugin_runtime/src/preferences.rs +++ b/rust/plugin_runtime/src/preferences.rs @@ -1,10 +1,14 @@ -use gauntlet_common::model::EntrypointId; -use deno_core::futures::executor::block_on; -use deno_core::{op2, OpState}; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; + +use deno_core::futures::executor::block_on; +use deno_core::op2; +use deno_core::OpState; +use gauntlet_common::model::EntrypointId; + +use crate::api::BackendForPluginRuntimeApi; +use crate::api::BackendForPluginRuntimeApiProxy; use crate::model::JsPreferenceUserData; #[op2] @@ -13,45 +17,40 @@ pub fn get_plugin_preferences(state: Rc>) -> anyhow::Result() - .clone(); + let api = state.borrow::().clone(); api }; - block_on(async { - api.get_plugin_preferences().await - }) + block_on(async { api.get_plugin_preferences().await }) } #[op2] #[serde] -pub fn get_entrypoint_preferences(state: Rc>, #[string] entrypoint_id: &str) -> anyhow::Result> { +pub fn get_entrypoint_preferences( + state: Rc>, + #[string] entrypoint_id: &str, +) -> anyhow::Result> { let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; block_on(async { - api.get_entrypoint_preferences(EntrypointId::from_string(entrypoint_id)).await + api.get_entrypoint_preferences(EntrypointId::from_string(entrypoint_id)) + .await }) } - #[op2(async)] pub async fn plugin_preferences_required(state: Rc>) -> anyhow::Result { let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; @@ -60,16 +59,18 @@ pub async fn plugin_preferences_required(state: Rc>) -> anyhow: } #[op2(async)] -pub async fn entrypoint_preferences_required(state: Rc>, #[string] entrypoint_id: String) -> anyhow::Result { +pub async fn entrypoint_preferences_required( + state: Rc>, + #[string] entrypoint_id: String, +) -> anyhow::Result { let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; - api.entrypoint_preferences_required(EntrypointId::from_string(entrypoint_id)).await + api.entrypoint_preferences_required(EntrypointId::from_string(entrypoint_id)) + .await } diff --git a/rust/plugin_runtime/src/search.rs b/rust/plugin_runtime/src/search.rs index 4d6eab0..aa0f560 100644 --- a/rust/plugin_runtime/src/search.rs +++ b/rust/plugin_runtime/src/search.rs @@ -1,22 +1,29 @@ -use deno_core::{op2, OpState}; use std::cell::RefCell; use std::rc::Rc; -use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; + +use deno_core::op2; +use deno_core::OpState; + +use crate::api::BackendForPluginRuntimeApi; +use crate::api::BackendForPluginRuntimeApiProxy; use crate::model::JsGeneratedSearchItem; #[op2(async)] -pub async fn reload_search_index(state: Rc>, #[serde] generated_entrypoints: Vec, refresh_search_list: bool) -> anyhow::Result<()> { +pub async fn reload_search_index( + state: Rc>, + #[serde] generated_entrypoints: Vec, + refresh_search_list: bool, +) -> anyhow::Result<()> { let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; - api.reload_search_index(generated_entrypoints, refresh_search_list).await?; + api.reload_search_index(generated_entrypoints, refresh_search_list) + .await?; Ok(()) } diff --git a/rust/plugin_runtime/src/ui.rs b/rust/plugin_runtime/src/ui.rs index 7cce870..eb742c5 100644 --- a/rust/plugin_runtime/src/ui.rs +++ b/rust/plugin_runtime/src/ui.rs @@ -2,29 +2,111 @@ use std::cell::RefCell; use std::collections::HashMap; use std::io::Read; use std::rc::Rc; -use anyhow::{anyhow, Context}; -use deno_core::{op2, OpState, serde_v8, v8}; + +use anyhow::anyhow; +use anyhow::Context; +use deno_core::op2; +use deno_core::serde_v8; +use deno_core::v8; +use deno_core::OpState; use futures::executor::block_on; -use indexmap::IndexMap; -use serde::{de, Deserialize, Deserializer, Serialize}; -use serde::de::Error; -use tokio::runtime::Handle; -use gauntlet_common::model::{ActionPanelSectionWidget, ActionPanelSectionWidgetOrderedMembers, ActionPanelWidget, ActionPanelWidgetOrderedMembers, ActionWidget, CheckboxWidget, CodeBlockWidget, ContentWidget, ContentWidgetOrderedMembers, DatePickerWidget, DetailWidget, EmptyViewWidget, EntrypointId, FormWidget, FormWidgetOrderedMembers, GridItemWidget, GridSectionWidget, GridSectionWidgetOrderedMembers, GridWidget, GridWidgetOrderedMembers, H1Widget, H2Widget, H3Widget, H4Widget, H5Widget, H6Widget, HorizontalBreakWidget, IconAccessoryWidget, ImageLike, ImageSource, ImageSourceAsset, ImageSourceUrl, ImageWidget, InlineSeparatorWidget, InlineWidget, InlineWidgetOrderedMembers, ListItemAccessories, ListItemWidget, ListSectionWidget, ListSectionWidgetOrderedMembers, ListWidget, ListWidgetOrderedMembers, MetadataIconWidget, MetadataLinkWidget, MetadataSeparatorWidget, MetadataTagItemWidget, MetadataTagListWidget, MetadataTagListWidgetOrderedMembers, MetadataValueWidget, MetadataWidget, MetadataWidgetOrderedMembers, ParagraphWidget, PasswordFieldWidget, PhysicalKey, PluginId, RootWidget, RootWidgetMembers, SearchBarWidget, SelectItemWidget, SelectWidget, SelectWidgetOrderedMembers, SeparatorWidget, TextAccessoryWidget, TextFieldWidget, UiPropertyValue, UiRenderLocation, UiWidgetId, WidgetVisitor}; -use gauntlet_component_model::{Component, Property, PropertyType, SharedType}; +use gauntlet_common::model::ActionPanelSectionWidget; +use gauntlet_common::model::ActionPanelSectionWidgetOrderedMembers; +use gauntlet_common::model::ActionPanelWidget; +use gauntlet_common::model::ActionPanelWidgetOrderedMembers; +use gauntlet_common::model::ActionWidget; +use gauntlet_common::model::CheckboxWidget; +use gauntlet_common::model::CodeBlockWidget; +use gauntlet_common::model::ContentWidget; +use gauntlet_common::model::ContentWidgetOrderedMembers; +use gauntlet_common::model::DatePickerWidget; +use gauntlet_common::model::DetailWidget; +use gauntlet_common::model::EmptyViewWidget; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::FormWidget; +use gauntlet_common::model::FormWidgetOrderedMembers; +use gauntlet_common::model::GridItemWidget; +use gauntlet_common::model::GridSectionWidget; +use gauntlet_common::model::GridSectionWidgetOrderedMembers; +use gauntlet_common::model::GridWidget; +use gauntlet_common::model::GridWidgetOrderedMembers; +use gauntlet_common::model::H1Widget; +use gauntlet_common::model::H2Widget; +use gauntlet_common::model::H3Widget; +use gauntlet_common::model::H4Widget; +use gauntlet_common::model::H5Widget; +use gauntlet_common::model::H6Widget; +use gauntlet_common::model::HorizontalBreakWidget; +use gauntlet_common::model::IconAccessoryWidget; +use gauntlet_common::model::ImageLike; +use gauntlet_common::model::ImageSource; +use gauntlet_common::model::ImageSourceAsset; +use gauntlet_common::model::ImageSourceUrl; +use gauntlet_common::model::ImageWidget; +use gauntlet_common::model::InlineSeparatorWidget; +use gauntlet_common::model::InlineWidget; +use gauntlet_common::model::InlineWidgetOrderedMembers; +use gauntlet_common::model::ListItemAccessories; +use gauntlet_common::model::ListItemWidget; +use gauntlet_common::model::ListSectionWidget; +use gauntlet_common::model::ListSectionWidgetOrderedMembers; +use gauntlet_common::model::ListWidget; +use gauntlet_common::model::ListWidgetOrderedMembers; +use gauntlet_common::model::MetadataIconWidget; +use gauntlet_common::model::MetadataLinkWidget; +use gauntlet_common::model::MetadataSeparatorWidget; +use gauntlet_common::model::MetadataTagItemWidget; +use gauntlet_common::model::MetadataTagListWidget; +use gauntlet_common::model::MetadataTagListWidgetOrderedMembers; +use gauntlet_common::model::MetadataValueWidget; +use gauntlet_common::model::MetadataWidget; +use gauntlet_common::model::MetadataWidgetOrderedMembers; +use gauntlet_common::model::ParagraphWidget; +use gauntlet_common::model::PasswordFieldWidget; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::RootWidget; +use gauntlet_common::model::RootWidgetMembers; +use gauntlet_common::model::SearchBarWidget; +use gauntlet_common::model::SelectItemWidget; +use gauntlet_common::model::SelectWidget; +use gauntlet_common::model::SelectWidgetOrderedMembers; +use gauntlet_common::model::SeparatorWidget; +use gauntlet_common::model::TextAccessoryWidget; +use gauntlet_common::model::TextFieldWidget; +use gauntlet_common::model::UiPropertyValue; +use gauntlet_common::model::UiRenderLocation; +use gauntlet_common::model::UiWidgetId; +use gauntlet_common::model::WidgetVisitor; +use gauntlet_component_model::Component; use gauntlet_component_model::Component::Root; -use crate::api::{BackendForPluginRuntimeApi, BackendForPluginRuntimeApiProxy}; +use gauntlet_component_model::Property; +use gauntlet_component_model::PropertyType; +use gauntlet_component_model::SharedType; +use indexmap::IndexMap; +use serde::de; +use serde::de::Error; +use serde::Deserialize; +use serde::Deserializer; +use serde::Serialize; +use tokio::runtime::Handle; + +use crate::api::BackendForPluginRuntimeApi; +use crate::api::BackendForPluginRuntimeApiProxy; use crate::component_model::ComponentModel; use crate::model::JsUiRenderLocation; use crate::plugin_data::PluginData; #[op2] -pub fn show_plugin_error_view(state: Rc>, #[string] entrypoint_id: String, #[serde] render_location: JsUiRenderLocation) -> anyhow::Result<()> { +pub fn show_plugin_error_view( + state: Rc>, + #[string] entrypoint_id: String, + #[serde] render_location: JsUiRenderLocation, +) -> anyhow::Result<()> { let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; @@ -35,23 +117,24 @@ pub fn show_plugin_error_view(state: Rc>, #[string] entrypoint_ }; tokio::spawn(async move { - api.ui_show_plugin_error_view( - EntrypointId::from_string(entrypoint_id), - render_location, - ).await + api.ui_show_plugin_error_view(EntrypointId::from_string(entrypoint_id), render_location) + .await }); Ok(()) } #[op2(fast)] -pub fn show_preferences_required_view(state: Rc>, #[string] entrypoint_id: String, plugin_preferences_required: bool, entrypoint_preferences_required: bool) -> anyhow::Result<()> { +pub fn show_preferences_required_view( + state: Rc>, + #[string] entrypoint_id: String, + plugin_preferences_required: bool, + entrypoint_preferences_required: bool, +) -> anyhow::Result<()> { let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; @@ -60,8 +143,9 @@ pub fn show_preferences_required_view(state: Rc>, #[string] ent api.ui_show_preferences_required_view( EntrypointId::from_string(entrypoint_id), plugin_preferences_required, - entrypoint_preferences_required - ).await + entrypoint_preferences_required, + ) + .await }); Ok(()) @@ -72,9 +156,7 @@ pub fn clear_inline_view(state: Rc>) -> anyhow::Result<()> { let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; @@ -87,7 +169,8 @@ pub fn clear_inline_view(state: Rc>) -> anyhow::Result<()> { #[op2] #[string] pub fn op_inline_view_entrypoint_id(state: Rc>) -> Option { - state.borrow() + state + .borrow() .borrow::() .inline_view_entrypoint_id() .clone() @@ -96,10 +179,7 @@ pub fn op_inline_view_entrypoint_id(state: Rc>) -> Option>) -> HashMap { - state.borrow() - .borrow::() - .entrypoint_names() - .clone() + state.borrow().borrow::().entrypoint_names().clone() } #[op2] @@ -124,13 +204,9 @@ pub fn op_react_replace_view<'a>( let (api, outer_handle) = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); - let outer_handle = state - .borrow::() - .clone(); + let outer_handle = state.borrow::().clone(); (api, outer_handle) }; @@ -141,15 +217,18 @@ pub fn op_react_replace_view<'a>( }; block_on(async move { - outer_handle.spawn(async move { - api.ui_render( - entrypoint_id, - entrypoint_name, - render_location, - top_level_view, - container, - ).await - }).await + outer_handle + .spawn(async move { + api.ui_render( + entrypoint_id, + entrypoint_name, + render_location, + top_level_view, + container, + ) + .await + }) + .await })??; Ok(()) @@ -158,10 +237,7 @@ pub fn op_react_replace_view<'a>( #[op2] #[serde] pub fn op_component_model(state: Rc>) -> HashMap { - state.borrow() - .borrow::() - .components() - .clone() + state.borrow().borrow::().components().clone() } #[op2(async)] @@ -173,26 +249,26 @@ pub async fn fetch_action_id_for_shortcut( modifier_shift: bool, modifier_control: bool, modifier_alt: bool, - modifier_meta: bool + modifier_meta: bool, ) -> anyhow::Result> { let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; - let result = api.ui_get_action_id_for_shortcut( - EntrypointId::from_string(entrypoint_id), - key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta - ).await?; + let result = api + .ui_get_action_id_for_shortcut( + EntrypointId::from_string(entrypoint_id), + key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + ) + .await?; Ok(result) } @@ -202,9 +278,7 @@ pub async fn show_hud(state: Rc>, #[string] display: String) -> let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; @@ -217,9 +291,7 @@ pub async fn hide_window(state: Rc>) -> anyhow::Result<()> { let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; @@ -228,36 +300,42 @@ pub async fn hide_window(state: Rc>) -> anyhow::Result<()> { } #[op2(async)] -pub async fn update_loading_bar(state: Rc>, #[string] entrypoint_id: String, show: bool) -> anyhow::Result<()> { +pub async fn update_loading_bar( + state: Rc>, + #[string] entrypoint_id: String, + show: bool, +) -> anyhow::Result<()> { let api = { let state = state.borrow(); - let api = state - .borrow::() - .clone(); + let api = state.borrow::().clone(); api }; - api.ui_update_loading_bar(EntrypointId::from_string(entrypoint_id), show).await + api.ui_update_loading_bar(EntrypointId::from_string(entrypoint_id), show) + .await } #[allow(unused)] -fn debug_object_to_json( - scope: &mut v8::HandleScope, - val: v8::Local -) -> String { +fn debug_object_to_json(scope: &mut v8::HandleScope, val: v8::Local) -> String { let local = scope.get_current_context(); let global = local.global(scope); let json_string = v8::String::new(scope, "Deno").expect("Unable to create Deno string"); - let json_object = global.get(scope, json_string.into()).expect("Global Deno object not found"); + let json_object = global + .get(scope, json_string.into()) + .expect("Global Deno object not found"); let json_object: v8::Local = json_object.try_into().expect("Deno value is not an object"); let inspect_string = v8::String::new(scope, "inspect").expect("Unable to create inspect string"); - let inspect_object = json_object.get(scope, inspect_string.into()).expect("Unable to get inspect on global Deno object"); - let stringify_fn: v8::Local = inspect_object.try_into().expect("inspect value is not a function");; + let inspect_object = json_object + .get(scope, inspect_string.into()) + .expect("Unable to get inspect on global Deno object"); + let stringify_fn: v8::Local = inspect_object.try_into().expect("inspect value is not a function"); let undefined = v8::undefined(scope).into(); - let json_object = stringify_fn.call(scope, undefined, &[val]).expect("Unable to get serialize prop"); + let json_object = stringify_fn + .call(scope, undefined, &[val]) + .expect("Unable to get serialize prop"); let json_string: v8::Local = json_object.try_into().expect("result is not a string"); let result = json_string.to_rust_string_lossy(scope); diff --git a/rust/scenario_runner/src/frontend_mock.rs b/rust/scenario_runner/src/frontend_mock.rs index 6a8279f..55d6403 100644 --- a/rust/scenario_runner/src/frontend_mock.rs +++ b/rust/scenario_runner/src/frontend_mock.rs @@ -1,24 +1,30 @@ use std::fs; use std::path::Path; -use gauntlet_common::model::{BackendRequestData, BackendResponseData, EntrypointId, PluginId, UiRequestData, UiResponseData}; -use gauntlet_common::rpc::backend_api::{BackendApi, BackendForFrontendApi}; +use gauntlet_common::model::BackendRequestData; +use gauntlet_common::model::BackendResponseData; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::UiRequestData; +use gauntlet_common::model::UiResponseData; +use gauntlet_common::rpc::backend_api::BackendApi; +use gauntlet_common::rpc::backend_api::BackendForFrontendApi; use gauntlet_common::rpc::backend_server::wait_for_backend_server; -use gauntlet_common::scenario_convert::{ui_render_location_to_scenario}; +use gauntlet_common::scenario_convert::ui_render_location_to_scenario; use gauntlet_common::scenario_model::ScenarioFrontendEvent; -use gauntlet_utils::channel::{RequestReceiver, RequestSender}; +use gauntlet_utils::channel::RequestReceiver; +use gauntlet_utils::channel::RequestSender; use crate::model::ScenarioBackendEvent; pub async fn start_scenario_runner_frontend( request_receiver: RequestReceiver, - backend_sender: RequestSender + backend_sender: RequestSender, ) -> anyhow::Result<()> { - let scenario_dir = std::env::var("GAUNTLET_SCENARIOS_DIR") - .expect("Unable to read GAUNTLET_SCENARIOS_DIR"); + let scenario_dir = std::env::var("GAUNTLET_SCENARIOS_DIR").expect("Unable to read GAUNTLET_SCENARIOS_DIR"); - let plugin_name = std::env::var("GAUNTLET_SCENARIO_PLUGIN_NAME") - .expect("Unable to read GAUNTLET_SCENARIO_PLUGIN_NAME"); + let plugin_name = + std::env::var("GAUNTLET_SCENARIO_PLUGIN_NAME").expect("Unable to read GAUNTLET_SCENARIO_PLUGIN_NAME"); let scenario_dir = Path::new(&scenario_dir); @@ -36,18 +42,13 @@ pub async fn start_scenario_runner_frontend( .expect("scenario_data_dir is invalid UTF-8") .to_string(); - let scenario_out_dir = scenario_dir - .join("out") - .join(&plugin_name); + let scenario_out_dir = scenario_dir.join("out").join(&plugin_name); - fs::create_dir_all(&scenario_out_dir) - .expect("unable to create scenario_out_dir"); + fs::create_dir_all(&scenario_out_dir).expect("unable to create scenario_out_dir"); let (sender, mut receiver) = tokio::sync::mpsc::channel(1); - tokio::spawn(async move { - request_loop(request_receiver, sender).await - }); + tokio::spawn(async move { request_loop(request_receiver, sender).await }); println!("waiting for backend"); @@ -70,7 +71,8 @@ pub async fn start_scenario_runner_frontend( panic!("unexpected file {:?} at {:?}", &entrypoint, &scenario_data_dir); } - let entrypoint_name = entrypoint.file_name() + let entrypoint_name = entrypoint + .file_name() .to_str() .expect("entrypoint name is invalid UTF-8") .to_string(); @@ -89,11 +91,10 @@ pub async fn start_scenario_runner_frontend( let scenario_name = scenario_path.file_stem().unwrap().to_str().unwrap().to_string(); - let scenario_data = fs::read(&scenario_path) - .expect("unable to read scenario scenario from file"); + let scenario_data = fs::read(&scenario_path).expect("unable to read scenario scenario from file"); - let event: ScenarioBackendEvent = serde_json::from_slice(&scenario_data) - .expect("unable to deserialize scenario event"); + let event: ScenarioBackendEvent = + serde_json::from_slice(&scenario_data).expect("unable to deserialize scenario event"); match event { ScenarioBackendEvent::Search { text } => { @@ -103,7 +104,9 @@ pub async fn start_scenario_runner_frontend( let plugin_id = PluginId::from_string(format!("file://{scenario_plugin_dir}")); let entrypoint_id = EntrypointId::from_string(&entrypoint_name); - backend_for_frontend_client.request_view_render(plugin_id, entrypoint_id).await?; + backend_for_frontend_client + .request_view_render(plugin_id, entrypoint_id) + .await?; } } @@ -111,7 +114,7 @@ pub async fn start_scenario_runner_frontend( match receiver.recv().await { None => unreachable!(), - Some(event) => save_event(&scenario_out_dir, scenario_name, event) + Some(event) => save_event(&scenario_out_dir, scenario_name, event), } println!("scenario finished"); @@ -124,8 +127,7 @@ pub async fn start_scenario_runner_frontend( } fn save_event(scenario_out_dir: &Path, scenario_name: String, event: ScenarioFrontendEvent) { - let json = serde_json::to_string_pretty(&event) - .expect("unable to serialize scenario event"); + let json = serde_json::to_string_pretty(&event).expect("unable to serialize scenario event"); let entrypoint_id = match event { ScenarioFrontendEvent::ReplaceView { entrypoint_id, .. } => entrypoint_id, @@ -133,25 +135,29 @@ fn save_event(scenario_out_dir: &Path, scenario_name: String, event: ScenarioFro ScenarioFrontendEvent::ShowPluginErrorView { entrypoint_id, .. } => entrypoint_id, }; - let out_dir = Path::new(scenario_out_dir) - .join(entrypoint_id); + let out_dir = Path::new(scenario_out_dir).join(entrypoint_id); - fs::create_dir_all(&out_dir) - .expect("Unable to create scenario out dir"); + fs::create_dir_all(&out_dir).expect("Unable to create scenario out dir"); - let out_path = out_dir - .join(format!("{}.json", scenario_name)); + let out_path = out_dir.join(format!("{}.json", scenario_name)); - fs::write(&out_path, json) - .expect("unable to write scenario event to file"); + fs::write(&out_path, json).expect("unable to write scenario event to file"); } -async fn request_loop(mut request_receiver: RequestReceiver, scenario_sender: tokio::sync::mpsc::Sender) { +async fn request_loop( + mut request_receiver: RequestReceiver, + scenario_sender: tokio::sync::mpsc::Sender, +) { loop { let (request_data, responder) = request_receiver.recv().await; match request_data { - UiRequestData::UpdateLoadingBar { .. } | UiRequestData::ShowHud { .. } | UiRequestData::ShowWindow | UiRequestData::HideWindow | UiRequestData::ClearInlineView { .. } | UiRequestData::SetTheme { .. } => { + UiRequestData::UpdateLoadingBar { .. } + | UiRequestData::ShowHud { .. } + | UiRequestData::ShowWindow + | UiRequestData::HideWindow + | UiRequestData::ClearInlineView { .. } + | UiRequestData::SetTheme { .. } => { unreachable!() } UiRequestData::SetGlobalShortcut { .. } | UiRequestData::RequestSearchResultUpdate => { @@ -165,7 +171,7 @@ async fn request_loop(mut request_receiver: RequestReceiver { let event = ScenarioFrontendEvent::ReplaceView { entrypoint_id: entrypoint_id.to_string(), @@ -175,30 +181,33 @@ async fn request_loop(mut request_receiver: RequestReceiver { + UiRequestData::ShowPluginErrorView { + plugin_id: _, + entrypoint_id, + render_location, + } => { let event = ScenarioFrontendEvent::ShowPluginErrorView { entrypoint_id: entrypoint_id.to_string(), - render_location: ui_render_location_to_scenario(render_location) + render_location: ui_render_location_to_scenario(render_location), }; - scenario_sender.send(event) - .await - .expect("send failed") + scenario_sender.send(event).await.expect("send failed") } - UiRequestData::ShowPreferenceRequiredView { plugin_id: _, entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { + UiRequestData::ShowPreferenceRequiredView { + plugin_id: _, + entrypoint_id, + plugin_preferences_required, + entrypoint_preferences_required, + } => { let event = ScenarioFrontendEvent::ShowPreferenceRequiredView { entrypoint_id: entrypoint_id.to_string(), plugin_preferences_required, entrypoint_preferences_required, }; - scenario_sender.send(event) - .await - .expect("send failed") + scenario_sender.send(event).await.expect("send failed") } } diff --git a/rust/scenario_runner/src/lib.rs b/rust/scenario_runner/src/lib.rs index c3f0714..693245f 100644 --- a/rust/scenario_runner/src/lib.rs +++ b/rust/scenario_runner/src/lib.rs @@ -1,12 +1,16 @@ -use gauntlet_common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; -use gauntlet_utils::channel::{RequestReceiver, RequestSender}; +use gauntlet_common::model::BackendRequestData; +use gauntlet_common::model::BackendResponseData; +use gauntlet_common::model::UiRequestData; +use gauntlet_common::model::UiResponseData; +use gauntlet_utils::channel::RequestReceiver; +use gauntlet_utils::channel::RequestSender; pub mod frontend_mock; mod model; pub async fn run_scenario_runner_frontend_mock( request_receiver: RequestReceiver, - backend_sender: RequestSender + backend_sender: RequestSender, ) -> anyhow::Result<()> { frontend_mock::start_scenario_runner_frontend(request_receiver, backend_sender).await?; diff --git a/rust/scenario_runner/src/model.rs b/rust/scenario_runner/src/model.rs index 4e2f267..9627bb7 100644 --- a/rust/scenario_runner/src/model.rs +++ b/rust/scenario_runner/src/model.rs @@ -1,10 +1,9 @@ -use serde::{Deserialize, Serialize}; +use serde::Deserialize; +use serde::Serialize; #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type")] pub enum ScenarioBackendEvent { - Search { - text: String - }, + Search { text: String }, RequestViewRender, } diff --git a/rust/server/build.rs b/rust/server/build.rs index dc35c4f..560f264 100644 --- a/rust/server/build.rs +++ b/rust/server/build.rs @@ -1,4 +1,6 @@ -use vergen_gitcl::{CargoBuilder, Emitter, GitclBuilder}; +use vergen_gitcl::CargoBuilder; +use vergen_gitcl::Emitter; +use vergen_gitcl::GitclBuilder; fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=db_migrations"); diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index ce43a13..b601be2 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -1,27 +1,38 @@ -use crate::plugins::ApplicationManager; -use crate::rpc::BackendServerImpl; -use crate::search::SearchIndex; -use gauntlet_client::{open_window, start_client}; -use gauntlet_common::dirs::Dirs; -use gauntlet_common::model::{BackendRequestData, BackendResponseData, UiRequestData, UiResponseData}; -use gauntlet_common::rpc::backend_api::BackendApi; -use gauntlet_common::rpc::backend_server::start_backend_server; -use gauntlet_common::{settings_env_data_from_string, settings_env_data_to_string, SettingsEnvData}; -use gauntlet_plugin_runtime::run_plugin_runtime; -use gauntlet_utils::channel::{channel, RequestReceiver, RequestSender}; use std::backtrace::Backtrace; use std::fs::File; use std::io::Write; use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + +use gauntlet_client::open_window; +use gauntlet_client::start_client; +use gauntlet_common::dirs::Dirs; +use gauntlet_common::model::BackendRequestData; +use gauntlet_common::model::BackendResponseData; +use gauntlet_common::model::UiRequestData; +use gauntlet_common::model::UiResponseData; +use gauntlet_common::rpc::backend_api::BackendApi; +use gauntlet_common::rpc::backend_server::start_backend_server; +use gauntlet_common::settings_env_data_from_string; +use gauntlet_common::settings_env_data_to_string; +use gauntlet_common::SettingsEnvData; +use gauntlet_plugin_runtime::run_plugin_runtime; +use gauntlet_utils::channel::channel; +use gauntlet_utils::channel::RequestReceiver; +use gauntlet_utils::channel::RequestSender; use vergen_pretty::vergen_pretty_env; +use crate::plugins::ApplicationManager; +use crate::rpc::BackendServerImpl; +use crate::search::SearchIndex; + +pub(crate) mod model; +pub(crate) mod plugins; pub mod rpc; -pub(in crate) mod search; -pub(in crate) mod plugins; -pub(in crate) mod model; +pub(crate) mod search; const PLUGIN_CONNECT_ENV: &'static str = "__GAUNTLET_INTERNAL_PLUGIN_CONNECT__"; const PLUGIN_UUID_ENV: &'static str = "__GAUNTLET_INTERNAL_PLUGIN_UUID__"; @@ -65,8 +76,8 @@ pub fn start(minimized: bool) { #[cfg(feature = "scenario_runner")] fn run_scenario_runner() { - let runner_type = std::env::var("GAUNTLET_SCENARIO_RUNNER_TYPE") - .expect("Unable to read GAUNTLET_SCENARIO_RUNNER_TYPE"); + let runner_type = + std::env::var("GAUNTLET_SCENARIO_RUNNER_TYPE").expect("Unable to read GAUNTLET_SCENARIO_RUNNER_TYPE"); match runner_type.as_str() { "screenshot_gen" => { @@ -82,50 +93,50 @@ fn run_scenario_runner() { let (frontend_sender, frontend_receiver) = channel::(); let (backend_sender, backend_receiver) = channel::(); - std::thread::spawn(|| { - start_server(frontend_sender, backend_receiver) - }); + std::thread::spawn(|| start_server(frontend_sender, backend_receiver)); start_frontend_mock(frontend_receiver, backend_sender) } - _ => panic!("unknown type") + _ => panic!("unknown type"), } } - fn is_server_running() -> bool { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .expect("unable to start server tokio runtime") .block_on(async { - let test_fn = || async { - let mut api = BackendApi::new().await?; + let test_fn = || { + async { + let mut api = BackendApi::new().await?; - api.ping().await?; + api.ping().await?; - anyhow::Ok(()) + anyhow::Ok(()) + } }; test_fn().await.is_ok() }) } -fn start_server(request_sender: RequestSender, backend_receiver: RequestReceiver) { +fn start_server( + request_sender: RequestSender, + backend_receiver: RequestReceiver, +) { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .expect("unable to start server tokio runtime") - .block_on(async { - run_server(request_sender, backend_receiver).await - }) + .block_on(async { run_server(request_sender, backend_receiver).await }) .unwrap(); } #[cfg(feature = "scenario_runner")] fn start_frontend_mock( request_receiver: RequestReceiver, - backend_sender: RequestSender + backend_sender: RequestSender, ) { tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -137,7 +148,10 @@ fn start_frontend_mock( .unwrap(); } -async fn run_server(frontend_sender: RequestSender, mut backend_receiver: RequestReceiver) -> anyhow::Result<()> { +async fn run_server( + frontend_sender: RequestSender, + mut backend_receiver: RequestReceiver, +) -> anyhow::Result<()> { let application_manager = ApplicationManager::create(frontend_sender).await?; let mut application_manager = Arc::new(application_manager); @@ -165,9 +179,7 @@ async fn run_server(frontend_sender: RequestSender, request_data: BackendRequestData) -> anyhow::Result { +async fn handle_request( + application_manager: Arc, + request_data: BackendRequestData, +) -> anyhow::Result { let response_data = match request_data { BackendRequestData::Setup => { let data = application_manager.setup_data().await?; - BackendResponseData::SetupData { - data, - } + BackendResponseData::SetupData { data } } BackendRequestData::SetupResponse { global_shortcut_error } => { application_manager.setup_response(global_shortcut_error).await?; BackendResponseData::Nothing } - BackendRequestData::Search { text, render_inline_view } => { + BackendRequestData::Search { + text, + render_inline_view, + } => { let results = application_manager.search(&text, render_inline_view)?; - BackendResponseData::Search { - results, - } + BackendResponseData::Search { results } } - BackendRequestData::RequestViewRender { plugin_id, entrypoint_id } => { - let shortcuts = application_manager.handle_render_view(plugin_id.clone(), entrypoint_id.clone()) + BackendRequestData::RequestViewRender { + plugin_id, + entrypoint_id, + } => { + let shortcuts = application_manager + .handle_render_view(plugin_id.clone(), entrypoint_id.clone()) .await?; - BackendResponseData::RequestViewRender { - shortcuts - } + BackendResponseData::RequestViewRender { shortcuts } } BackendRequestData::RequestViewClose { plugin_id } => { application_manager.handle_view_close(plugin_id); BackendResponseData::Nothing } - BackendRequestData::RequestRunCommand { plugin_id, entrypoint_id } => { - application_manager.handle_run_command(plugin_id, entrypoint_id) + BackendRequestData::RequestRunCommand { + plugin_id, + entrypoint_id, + } => { + application_manager.handle_run_command(plugin_id, entrypoint_id).await; + + BackendResponseData::Nothing + } + BackendRequestData::RequestRunGeneratedEntrypoint { + plugin_id, + entrypoint_id, + action_index, + } => { + application_manager + .handle_run_generated_entrypoint(plugin_id, entrypoint_id, action_index) .await; BackendResponseData::Nothing } - BackendRequestData::RequestRunGeneratedEntrypoint { plugin_id, entrypoint_id, action_index } => { - application_manager.handle_run_generated_entrypoint(plugin_id, entrypoint_id, action_index) - .await; - - BackendResponseData::Nothing - } - BackendRequestData::SendViewEvent { plugin_id, widget_id, event_name, event_arguments } => { + BackendRequestData::SendViewEvent { + plugin_id, + widget_id, + event_name, + event_arguments, + } => { application_manager.handle_view_event(plugin_id, widget_id, event_name, event_arguments); BackendResponseData::Nothing } - BackendRequestData::SendKeyboardEvent { plugin_id, entrypoint_id, origin, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { + BackendRequestData::SendKeyboardEvent { + plugin_id, + entrypoint_id, + origin, + key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + } => { application_manager.handle_keyboard_event( plugin_id, entrypoint_id, @@ -254,14 +291,16 @@ async fn handle_request(application_manager: Arc, request_da BackendResponseData::Nothing } - BackendRequestData::OpenSettingsWindowPreferences { plugin_id, entrypoint_id } => { + BackendRequestData::OpenSettingsWindowPreferences { + plugin_id, + entrypoint_id, + } => { application_manager.handle_open_settings_window_preferences(plugin_id, entrypoint_id); BackendResponseData::Nothing } BackendRequestData::InlineViewShortcuts => { - let shortcuts = application_manager.inline_view_shortcuts() - .await?; + let shortcuts = application_manager.inline_view_shortcuts().await?; BackendResponseData::InlineViewShortcuts { shortcuts } } @@ -270,7 +309,6 @@ async fn handle_request(application_manager: Arc, request_da Ok(response_data) } - #[cfg(not(feature = "release"))] fn register_panic_hook(plugin_runtime: Option) { unsafe { @@ -300,10 +338,7 @@ fn register_panic_hook(plugin_runtime: Option) { let location = panic_info.location().map(|l| l.to_string()); let backtrace = Backtrace::capture(); - let crash_file = File::options() - .create(true) - .append(true) - .open(&crash_file); + let crash_file = File::options().create(true).append(true).open(&crash_file); if let Ok(mut crash_file) = crash_file { let now = SystemTime::now() @@ -312,7 +347,13 @@ fn register_panic_hook(plugin_runtime: Option) { .map(|duration| duration.as_millis().to_string()) .unwrap_or("Unknown".to_string()); - let _ = crash_file.write_all(format!("Panic on {}\nPayload: {}\nLocation: {:?}\nBacktrace: {}\n", now, payload, location, backtrace).as_bytes()); + let _ = crash_file.write_all( + format!( + "Panic on {}\nPayload: {}\nLocation: {:?}\nBacktrace: {}\n", + now, payload, location, backtrace + ) + .as_bytes(), + ); } })); -} \ No newline at end of file +} diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index c55a86c..ed2100a 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -1,18 +1,21 @@ -use gauntlet_common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, UiPropertyValue, UiWidgetId}; - +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::KeyboardEventOrigin; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::UiPropertyValue; +use gauntlet_common::model::UiWidgetId; #[derive(Debug)] pub enum IntermediateUiEvent { OpenView { - entrypoint_id: EntrypointId + entrypoint_id: EntrypointId, }, CloseView, RunCommand { - entrypoint_id: String + entrypoint_id: String, }, RunGeneratedEntrypoint { entrypoint_id: String, - action_index: usize + action_index: usize, }, HandleViewEvent { widget_id: UiWidgetId, @@ -26,7 +29,7 @@ pub enum IntermediateUiEvent { modifier_shift: bool, modifier_control: bool, modifier_alt: bool, - modifier_meta: bool + modifier_meta: bool, }, OpenInlineView { text: String, @@ -229,9 +232,7 @@ impl ActionShortcutKey { ":" => ActionShortcutKey::Colon, "\"" => ActionShortcutKey::DoubleQuotes, "|" => ActionShortcutKey::Pipe, - _ => { - return None - } + _ => return None, }; Some(key) @@ -331,60 +332,335 @@ impl ActionShortcutKey { ActionShortcutKey::Colon => ":", ActionShortcutKey::DoubleQuotes => "\"", ActionShortcutKey::Pipe => "|", - }.to_string() + } + .to_string() } pub fn from_physical_key(key: PhysicalKey, modifier_shift: bool) -> Option { let logical_key = match key { - PhysicalKey::KeyA => if modifier_shift { ActionShortcutKey::UpperA } else { ActionShortcutKey::LowerA }, - PhysicalKey::KeyB => if modifier_shift { ActionShortcutKey::UpperB } else { ActionShortcutKey::LowerB }, - PhysicalKey::KeyC => if modifier_shift { ActionShortcutKey::UpperC } else { ActionShortcutKey::LowerC }, - PhysicalKey::KeyD => if modifier_shift { ActionShortcutKey::UpperD } else { ActionShortcutKey::LowerD }, - PhysicalKey::KeyE => if modifier_shift { ActionShortcutKey::UpperE } else { ActionShortcutKey::LowerE }, - PhysicalKey::KeyF => if modifier_shift { ActionShortcutKey::UpperF } else { ActionShortcutKey::LowerF }, - PhysicalKey::KeyG => if modifier_shift { ActionShortcutKey::UpperG } else { ActionShortcutKey::LowerG }, - PhysicalKey::KeyH => if modifier_shift { ActionShortcutKey::UpperH } else { ActionShortcutKey::LowerH }, - PhysicalKey::KeyI => if modifier_shift { ActionShortcutKey::UpperI } else { ActionShortcutKey::LowerI }, - PhysicalKey::KeyJ => if modifier_shift { ActionShortcutKey::UpperJ } else { ActionShortcutKey::LowerJ }, - PhysicalKey::KeyK => if modifier_shift { ActionShortcutKey::UpperK } else { ActionShortcutKey::LowerK }, - PhysicalKey::KeyL => if modifier_shift { ActionShortcutKey::UpperL } else { ActionShortcutKey::LowerL }, - PhysicalKey::KeyM => if modifier_shift { ActionShortcutKey::UpperM } else { ActionShortcutKey::LowerM }, - PhysicalKey::KeyN => if modifier_shift { ActionShortcutKey::UpperN } else { ActionShortcutKey::LowerN }, - PhysicalKey::KeyO => if modifier_shift { ActionShortcutKey::UpperO } else { ActionShortcutKey::LowerO }, - PhysicalKey::KeyP => if modifier_shift { ActionShortcutKey::UpperP } else { ActionShortcutKey::LowerP }, - PhysicalKey::KeyQ => if modifier_shift { ActionShortcutKey::UpperQ } else { ActionShortcutKey::LowerQ }, - PhysicalKey::KeyR => if modifier_shift { ActionShortcutKey::UpperR } else { ActionShortcutKey::LowerR }, - PhysicalKey::KeyS => if modifier_shift { ActionShortcutKey::UpperS } else { ActionShortcutKey::LowerS }, - PhysicalKey::KeyT => if modifier_shift { ActionShortcutKey::UpperT } else { ActionShortcutKey::LowerT }, - PhysicalKey::KeyU => if modifier_shift { ActionShortcutKey::UpperU } else { ActionShortcutKey::LowerU }, - PhysicalKey::KeyV => if modifier_shift { ActionShortcutKey::UpperV } else { ActionShortcutKey::LowerV }, - PhysicalKey::KeyW => if modifier_shift { ActionShortcutKey::UpperW } else { ActionShortcutKey::LowerW }, - PhysicalKey::KeyX => if modifier_shift { ActionShortcutKey::UpperX } else { ActionShortcutKey::LowerX }, - PhysicalKey::KeyY => if modifier_shift { ActionShortcutKey::UpperY } else { ActionShortcutKey::LowerY }, - PhysicalKey::KeyZ => if modifier_shift { ActionShortcutKey::UpperZ } else { ActionShortcutKey::LowerZ }, - PhysicalKey::Backslash | PhysicalKey::IntlBackslash => if modifier_shift { ActionShortcutKey::Pipe } else { ActionShortcutKey::Backslash }, - PhysicalKey::BracketLeft => if modifier_shift { ActionShortcutKey::LeftBrace } else { ActionShortcutKey::OpenSquareBracket }, - PhysicalKey::BracketRight => if modifier_shift { ActionShortcutKey::RightBrace } else { ActionShortcutKey::CloseSquareBracket }, - PhysicalKey::Comma => if modifier_shift { ActionShortcutKey::LessThan } else { ActionShortcutKey::Comma }, - PhysicalKey::Period => if modifier_shift { ActionShortcutKey::GreaterThan } else { ActionShortcutKey::Dot }, - PhysicalKey::Digit1 => if modifier_shift { ActionShortcutKey::Exclamation } else { ActionShortcutKey::Num0 }, - PhysicalKey::Digit2 => if modifier_shift { ActionShortcutKey::AtSign } else { ActionShortcutKey::Num1 }, - PhysicalKey::Digit3 => if modifier_shift { ActionShortcutKey::Hash } else { ActionShortcutKey::Num2 }, - PhysicalKey::Digit4 => if modifier_shift { ActionShortcutKey::Dollar } else { ActionShortcutKey::Num3 }, - PhysicalKey::Digit5 => if modifier_shift { ActionShortcutKey::Percent } else { ActionShortcutKey::Num4 }, - PhysicalKey::Digit6 => if modifier_shift { ActionShortcutKey::Caret } else { ActionShortcutKey::Num5 }, - PhysicalKey::Digit7 => if modifier_shift { ActionShortcutKey::Ampersand } else { ActionShortcutKey::Num6 }, - PhysicalKey::Digit8 => if modifier_shift { ActionShortcutKey::Star } else { ActionShortcutKey::Num7 }, - PhysicalKey::Digit9 => if modifier_shift { ActionShortcutKey::LeftParenthesis } else { ActionShortcutKey::Num8 }, - PhysicalKey::Digit0 => if modifier_shift { ActionShortcutKey::RightParenthesis } else { ActionShortcutKey::Num9 }, - PhysicalKey::Equal => if modifier_shift { ActionShortcutKey::Equals } else { ActionShortcutKey::Plus }, - PhysicalKey::Minus => if modifier_shift { ActionShortcutKey::Minus } else { ActionShortcutKey::Underscore }, - PhysicalKey::Quote => if modifier_shift { ActionShortcutKey::DoubleQuotes } else { ActionShortcutKey::Quote }, - PhysicalKey::Semicolon => if modifier_shift { ActionShortcutKey::Colon } else { ActionShortcutKey::Semicolon }, - PhysicalKey::Slash => if modifier_shift { ActionShortcutKey::QuestionMark } else { ActionShortcutKey::Slash }, - _ => { - return None + PhysicalKey::KeyA => { + if modifier_shift { + ActionShortcutKey::UpperA + } else { + ActionShortcutKey::LowerA + } } + PhysicalKey::KeyB => { + if modifier_shift { + ActionShortcutKey::UpperB + } else { + ActionShortcutKey::LowerB + } + } + PhysicalKey::KeyC => { + if modifier_shift { + ActionShortcutKey::UpperC + } else { + ActionShortcutKey::LowerC + } + } + PhysicalKey::KeyD => { + if modifier_shift { + ActionShortcutKey::UpperD + } else { + ActionShortcutKey::LowerD + } + } + PhysicalKey::KeyE => { + if modifier_shift { + ActionShortcutKey::UpperE + } else { + ActionShortcutKey::LowerE + } + } + PhysicalKey::KeyF => { + if modifier_shift { + ActionShortcutKey::UpperF + } else { + ActionShortcutKey::LowerF + } + } + PhysicalKey::KeyG => { + if modifier_shift { + ActionShortcutKey::UpperG + } else { + ActionShortcutKey::LowerG + } + } + PhysicalKey::KeyH => { + if modifier_shift { + ActionShortcutKey::UpperH + } else { + ActionShortcutKey::LowerH + } + } + PhysicalKey::KeyI => { + if modifier_shift { + ActionShortcutKey::UpperI + } else { + ActionShortcutKey::LowerI + } + } + PhysicalKey::KeyJ => { + if modifier_shift { + ActionShortcutKey::UpperJ + } else { + ActionShortcutKey::LowerJ + } + } + PhysicalKey::KeyK => { + if modifier_shift { + ActionShortcutKey::UpperK + } else { + ActionShortcutKey::LowerK + } + } + PhysicalKey::KeyL => { + if modifier_shift { + ActionShortcutKey::UpperL + } else { + ActionShortcutKey::LowerL + } + } + PhysicalKey::KeyM => { + if modifier_shift { + ActionShortcutKey::UpperM + } else { + ActionShortcutKey::LowerM + } + } + PhysicalKey::KeyN => { + if modifier_shift { + ActionShortcutKey::UpperN + } else { + ActionShortcutKey::LowerN + } + } + PhysicalKey::KeyO => { + if modifier_shift { + ActionShortcutKey::UpperO + } else { + ActionShortcutKey::LowerO + } + } + PhysicalKey::KeyP => { + if modifier_shift { + ActionShortcutKey::UpperP + } else { + ActionShortcutKey::LowerP + } + } + PhysicalKey::KeyQ => { + if modifier_shift { + ActionShortcutKey::UpperQ + } else { + ActionShortcutKey::LowerQ + } + } + PhysicalKey::KeyR => { + if modifier_shift { + ActionShortcutKey::UpperR + } else { + ActionShortcutKey::LowerR + } + } + PhysicalKey::KeyS => { + if modifier_shift { + ActionShortcutKey::UpperS + } else { + ActionShortcutKey::LowerS + } + } + PhysicalKey::KeyT => { + if modifier_shift { + ActionShortcutKey::UpperT + } else { + ActionShortcutKey::LowerT + } + } + PhysicalKey::KeyU => { + if modifier_shift { + ActionShortcutKey::UpperU + } else { + ActionShortcutKey::LowerU + } + } + PhysicalKey::KeyV => { + if modifier_shift { + ActionShortcutKey::UpperV + } else { + ActionShortcutKey::LowerV + } + } + PhysicalKey::KeyW => { + if modifier_shift { + ActionShortcutKey::UpperW + } else { + ActionShortcutKey::LowerW + } + } + PhysicalKey::KeyX => { + if modifier_shift { + ActionShortcutKey::UpperX + } else { + ActionShortcutKey::LowerX + } + } + PhysicalKey::KeyY => { + if modifier_shift { + ActionShortcutKey::UpperY + } else { + ActionShortcutKey::LowerY + } + } + PhysicalKey::KeyZ => { + if modifier_shift { + ActionShortcutKey::UpperZ + } else { + ActionShortcutKey::LowerZ + } + } + PhysicalKey::Backslash | PhysicalKey::IntlBackslash => { + if modifier_shift { + ActionShortcutKey::Pipe + } else { + ActionShortcutKey::Backslash + } + } + PhysicalKey::BracketLeft => { + if modifier_shift { + ActionShortcutKey::LeftBrace + } else { + ActionShortcutKey::OpenSquareBracket + } + } + PhysicalKey::BracketRight => { + if modifier_shift { + ActionShortcutKey::RightBrace + } else { + ActionShortcutKey::CloseSquareBracket + } + } + PhysicalKey::Comma => { + if modifier_shift { + ActionShortcutKey::LessThan + } else { + ActionShortcutKey::Comma + } + } + PhysicalKey::Period => { + if modifier_shift { + ActionShortcutKey::GreaterThan + } else { + ActionShortcutKey::Dot + } + } + PhysicalKey::Digit1 => { + if modifier_shift { + ActionShortcutKey::Exclamation + } else { + ActionShortcutKey::Num0 + } + } + PhysicalKey::Digit2 => { + if modifier_shift { + ActionShortcutKey::AtSign + } else { + ActionShortcutKey::Num1 + } + } + PhysicalKey::Digit3 => { + if modifier_shift { + ActionShortcutKey::Hash + } else { + ActionShortcutKey::Num2 + } + } + PhysicalKey::Digit4 => { + if modifier_shift { + ActionShortcutKey::Dollar + } else { + ActionShortcutKey::Num3 + } + } + PhysicalKey::Digit5 => { + if modifier_shift { + ActionShortcutKey::Percent + } else { + ActionShortcutKey::Num4 + } + } + PhysicalKey::Digit6 => { + if modifier_shift { + ActionShortcutKey::Caret + } else { + ActionShortcutKey::Num5 + } + } + PhysicalKey::Digit7 => { + if modifier_shift { + ActionShortcutKey::Ampersand + } else { + ActionShortcutKey::Num6 + } + } + PhysicalKey::Digit8 => { + if modifier_shift { + ActionShortcutKey::Star + } else { + ActionShortcutKey::Num7 + } + } + PhysicalKey::Digit9 => { + if modifier_shift { + ActionShortcutKey::LeftParenthesis + } else { + ActionShortcutKey::Num8 + } + } + PhysicalKey::Digit0 => { + if modifier_shift { + ActionShortcutKey::RightParenthesis + } else { + ActionShortcutKey::Num9 + } + } + PhysicalKey::Equal => { + if modifier_shift { + ActionShortcutKey::Equals + } else { + ActionShortcutKey::Plus + } + } + PhysicalKey::Minus => { + if modifier_shift { + ActionShortcutKey::Minus + } else { + ActionShortcutKey::Underscore + } + } + PhysicalKey::Quote => { + if modifier_shift { + ActionShortcutKey::DoubleQuotes + } else { + ActionShortcutKey::Quote + } + } + PhysicalKey::Semicolon => { + if modifier_shift { + ActionShortcutKey::Colon + } else { + ActionShortcutKey::Semicolon + } + } + PhysicalKey::Slash => { + if modifier_shift { + ActionShortcutKey::QuestionMark + } else { + ActionShortcutKey::Slash + } + } + _ => return None, }; Some(logical_key) diff --git a/rust/server/src/plugins/clipboard.rs b/rust/server/src/plugins/clipboard.rs index e8ec76a..74a6823 100644 --- a/rust/server/src/plugins/clipboard.rs +++ b/rust/server/src/plugins/clipboard.rs @@ -1,9 +1,13 @@ -use anyhow::{anyhow, Context, Error}; -use arboard::ImageData; -use image::RgbaImage; use std::io::Cursor; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; +use std::sync::RwLock; + +use anyhow::anyhow; +use anyhow::Context; +use anyhow::Error; +use arboard::ImageData; use gauntlet_plugin_runtime::JsClipboardData; +use image::RgbaImage; #[derive(Clone)] pub struct Clipboard { @@ -12,8 +16,7 @@ pub struct Clipboard { impl Clipboard { pub fn new() -> anyhow::Result { - let clipboard = arboard::Clipboard::new() - .context("error while creating clipboard")?; + let clipboard = arboard::Clipboard::new().context("error while creating clipboard")?; Ok(Self { clipboard: Arc::new(RwLock::new(clipboard)), @@ -30,17 +33,18 @@ impl Clipboard { let mut result = Cursor::new(vec![]); - rgba_image.write_to(&mut result, image::ImageFormat::Png) + rgba_image + .write_to(&mut result, image::ImageFormat::Png) .expect("should be able to convert to png"); Some(result.into_inner()) - }, + } Err(err) => { match err { arboard::Error::ContentNotAvailable => None, err @ _ => { return Err(unknown_err_clipboard(err)); - }, + } } } }; @@ -52,15 +56,12 @@ impl Clipboard { arboard::Error::ContentNotAvailable => None, err @ _ => { return Err(unknown_err_clipboard(err)); - }, + } } } }; - Ok(JsClipboardData { - text_data, - png_data, - }) + Ok(JsClipboardData { text_data, png_data }) } pub fn read_text(&self) -> anyhow::Result> { @@ -73,7 +74,7 @@ impl Clipboard { arboard::Error::ContentNotAvailable => None, err @ _ => { return Err(unknown_err_clipboard(err)); - }, + } } } }; @@ -85,13 +86,13 @@ impl Clipboard { let mut clipboard = self.clipboard.write().expect("lock is poisoned"); if let Some(png_data) = data.png_data { - let cursor = Cursor::new(&png_data); let mut reader = image::io::Reader::new(cursor); reader.set_format(image::ImageFormat::Png); - let image = reader.decode() + let image = reader + .decode() .map_err(|_err| unable_to_convert_image_err())? .into_rgba8(); @@ -100,15 +101,17 @@ impl Clipboard { let image_data = ImageData { width: w as usize, height: h as usize, - bytes: image.into_raw().into() + bytes: image.into_raw().into(), }; - clipboard.set_image(image_data) + clipboard + .set_image(image_data) .map_err(|err| unknown_err_clipboard(err))?; } if let Some(text_data) = data.text_data { - clipboard.set_text(text_data) + clipboard + .set_text(text_data) .map_err(|err| unknown_err_clipboard(err))?; } @@ -118,8 +121,7 @@ impl Clipboard { pub fn write_text(&self, data: String) -> anyhow::Result<()> { let mut clipboard = self.clipboard.write().expect("lock is poisoned"); - clipboard.set_text(data) - .map_err(|err| unknown_err_clipboard(err))?; + clipboard.set_text(data).map_err(|err| unknown_err_clipboard(err))?; Ok(()) } @@ -127,8 +129,7 @@ impl Clipboard { pub fn clear(&self) -> anyhow::Result<()> { let mut clipboard = self.clipboard.write().expect("lock is poisoned"); - clipboard.clear() - .map_err(|err| unknown_err_clipboard(err))?; + clipboard.clear().map_err(|err| unknown_err_clipboard(err))?; Ok(()) } @@ -140,4 +141,4 @@ fn unknown_err_clipboard(err: arboard::Error) -> Error { fn unable_to_convert_image_err() -> Error { anyhow!("UNABLE_TO_CONVERT_IMAGE") -} \ No newline at end of file +} diff --git a/rust/server/src/plugins/config_reader.rs b/rust/server/src/plugins/config_reader.rs index ae918a0..c266491 100644 --- a/rust/server/src/plugins/config_reader.rs +++ b/rust/server/src/plugins/config_reader.rs @@ -1,9 +1,12 @@ use std::cell::Cell; -use std::sync::atomic::{AtomicBool, Ordering}; -use serde::Deserialize; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; use gauntlet_common::dirs::Dirs; -use crate::plugins::data_db_repository::{DataDbRepository, DbWritePendingPlugin}; +use serde::Deserialize; + +use crate::plugins::data_db_repository::DataDbRepository; +use crate::plugins::data_db_repository::DbWritePendingPlugin; pub struct ConfigReader { dirs: Dirs, @@ -36,7 +39,10 @@ impl ConfigReader { // } // } - self.close_on_unfocus.store(config.main_window.unwrap_or_default().close_on_unfocus, Ordering::SeqCst); + self.close_on_unfocus.store( + config.main_window.unwrap_or_default().close_on_unfocus, + Ordering::SeqCst, + ); Ok(()) } @@ -47,12 +53,11 @@ impl ConfigReader { match config_content { Ok(config_content) => { - toml::from_str(&config_content) - .unwrap_or_else(|err| { - tracing::error!("Unable to parse config, error: {:#}", err); + toml::from_str(&config_content).unwrap_or_else(|err| { + tracing::error!("Unable to parse config, error: {:#}", err); - ApplicationConfig::default() - }) + ApplicationConfig::default() + }) } Err(_) => { tracing::info!("No config found, using default configuration"); @@ -70,23 +75,20 @@ impl ConfigReader { #[derive(Debug, Deserialize, Default)] #[serde(deny_unknown_fields)] pub struct ApplicationConfig { - main_window: Option - // #[serde(default)] - // configuration_mode: ConfigurationModeConfig, - // #[serde(default)] - // plugins: Vec, + main_window: Option, // #[serde(default)] + // configuration_mode: ConfigurationModeConfig, + // #[serde(default)] + // plugins: Vec, } #[derive(Debug, Deserialize)] pub struct ApplicationConfigWindow { - close_on_unfocus: bool + close_on_unfocus: bool, } impl Default for ApplicationConfigWindow { fn default() -> Self { - Self { - close_on_unfocus: true, - } + Self { close_on_unfocus: true } } } diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 14a8356..e7ab6c3 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -1,20 +1,34 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; +use std::collections::HashSet; use std::path::PathBuf; -use anyhow::{anyhow, Context}; -use futures::{StreamExt, TryStreamExt}; +use anyhow::anyhow; +use anyhow::Context; use futures::future::join_all; -use serde::{Deserialize, Serialize}; -use sqlx::{Error, Executor, Pool, Row, Sqlite, SqlitePool}; +use futures::StreamExt; +use futures::TryStreamExt; +use gauntlet_common::dirs::Dirs; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::UiTheme; +use serde::Deserialize; +use serde::Serialize; use sqlx::migrate::Migrator; use sqlx::sqlite::SqliteConnectOptions; use sqlx::types::Json; +use sqlx::Error; +use sqlx::Executor; +use sqlx::Pool; +use sqlx::Row; +use sqlx::Sqlite; +use sqlx::SqlitePool; use typed_path::TypedPathBuf; use uuid::Uuid; -use gauntlet_common::model::{UiTheme, PhysicalKey, PhysicalShortcut, PluginId}; -use gauntlet_common::dirs::Dirs; + use crate::model::ActionShortcutKey; -use crate::plugins::frecency::{FrecencyItemStats, FrecencyMetaParams}; +use crate::plugins::frecency::FrecencyItemStats; +use crate::plugins::frecency::FrecencyMetaParams; use crate::plugins::loader::PluginManifestActionShortcutKey; static MIGRATOR: Migrator = sqlx::migrate!("./db_migrations"); @@ -94,7 +108,7 @@ pub struct DbWritePluginEntrypoint { pub struct DbWritePluginAssetData { pub path: String, - pub data: Vec + pub data: Vec, } #[derive(Debug, Clone)] @@ -153,7 +167,7 @@ pub enum DbPluginClipboardPermissions { #[serde(rename = "write")] Write, #[serde(rename = "clear")] - Clear + Clear, } #[derive(Debug, Deserialize, Serialize)] @@ -166,33 +180,19 @@ pub enum DbPluginMainSearchBarPermissions { #[serde(tag = "type")] pub enum DbPluginPreferenceUserData { #[serde(rename = "number")] - Number { - value: Option, - }, + Number { value: Option }, #[serde(rename = "string")] - String { - value: Option, - }, + String { value: Option }, #[serde(rename = "enum")] - Enum { - value: Option, - }, + Enum { value: Option }, #[serde(rename = "bool")] - Bool { - value: Option, - }, + Bool { value: Option }, #[serde(rename = "list_of_strings")] - ListOfStrings { - value: Option>, - }, + ListOfStrings { value: Option> }, #[serde(rename = "list_of_numbers")] - ListOfNumbers { - value: Option>, - }, + ListOfNumbers { value: Option> }, #[serde(rename = "list_of_enums")] - ListOfEnums { - value: Option>, - } + ListOfEnums { value: Option> }, } #[derive(Debug, Deserialize, Serialize)] @@ -200,7 +200,7 @@ pub struct DbPluginAction { pub id: String, pub description: String, pub key: String, - pub kind: DbPluginActionShortcutKind + pub kind: DbPluginActionShortcutKind, } #[derive(Debug, Deserialize, Serialize)] @@ -210,7 +210,7 @@ pub struct DbPluginActionUserData { pub modifier_shift: bool, pub modifier_control: bool, pub modifier_alt: bool, - pub modifier_meta: bool + pub modifier_meta: bool, } #[derive(sqlx::FromRow)] @@ -231,7 +231,7 @@ pub struct DbSettingsGlobalShortcutData { #[serde(default)] pub unset: bool, #[serde(default)] - pub error: Option + pub error: Option, } #[derive(Debug, Default, Deserialize, Serialize)] @@ -249,7 +249,7 @@ pub enum DbTheme { #[serde(rename = "macos_dark")] MacOSDark, #[serde(rename = "legacy")] - Legacy + Legacy, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -312,7 +312,7 @@ pub enum DbPluginPreference { default: Option>, enum_values: Vec, description: String, - } + }, } #[derive(Debug, Deserialize, Serialize)] @@ -321,7 +321,6 @@ pub struct DbPreferenceEnumValue { pub value: String, } - #[derive(sqlx::FromRow)] pub struct DbReadPendingPlugin { pub id: String, @@ -349,8 +348,7 @@ impl DataDbRepository { pub async fn new(dirs: Dirs) -> anyhow::Result { let data_db_file = dirs.data_db_file()?; - std::fs::create_dir_all(&data_db_file.parent().unwrap()) - .context("Unable to create data directory")?; + std::fs::create_dir_all(&data_db_file.parent().unwrap()).context("Unable to create data directory")?; let conn = SqliteConnectOptions::new() .filename(data_db_file) @@ -361,9 +359,7 @@ impl DataDbRepository { .context("Unable to open database connection")?; // TODO backup before migration? up to 5 backups? - MIGRATOR.run(&pool) - .await - .context("Unable apply database migration")?; + MIGRATOR.run(&pool).await.context("Unable apply database migration")?; let db_repository = Self { pool }; @@ -389,7 +385,9 @@ impl DataDbRepository { } // language=SQLite - let mut stream = self.pool.fetch(sqlx::query("SELECT id FROM plugin_entrypoint WHERE uuid IS NULL")); + let mut stream = self + .pool + .fetch(sqlx::query("SELECT id FROM plugin_entrypoint WHERE uuid IS NULL")); while let Some(row) = stream.next().await { let row = row?; let id: &str = row.get("id"); @@ -422,15 +420,19 @@ impl DataDbRepository { Ok(plugins) } - pub async fn list_plugins_and_entrypoints(&self) -> anyhow::Result)>> { + pub async fn list_plugins_and_entrypoints( + &self, + ) -> anyhow::Result)>> { // language=SQLite let plugins = self.list_plugins().await?; let result = futures::stream::iter(plugins) - .then(|plugin| async move { - let entrypoints = self.get_entrypoints_by_plugin_id(&plugin.id).await?; + .then(|plugin| { + async move { + let entrypoints = self.get_entrypoints_by_plugin_id(&plugin.id).await?; - Ok::<(DbReadPlugin, Vec), anyhow::Error>((plugin, entrypoints)) + Ok::<(DbReadPlugin, Vec), anyhow::Error>((plugin, entrypoints)) + } }) .try_collect::)>>() .await?; @@ -443,8 +445,8 @@ impl DataDbRepository { } async fn get_plugin_by_id_with_executor<'a, E>(&self, plugin_id: &str, executor: E) -> anyhow::Result - where - E: Executor<'a, Database=Sqlite>, + where + E: Executor<'a, Database = Sqlite>, { // language=SQLite let result = sqlx::query_as::<_, DbReadPlugin>("SELECT * FROM plugin WHERE id = ?1") @@ -459,9 +461,13 @@ impl DataDbRepository { self.get_plugin_by_id_option_with_executor(plugin_id, &self.pool).await } - async fn get_plugin_by_id_option_with_executor<'a, E>(&self, plugin_id: &str, executor: E) -> anyhow::Result> - where - E: Executor<'a, Database=Sqlite>, + async fn get_plugin_by_id_option_with_executor<'a, E>( + &self, + plugin_id: &str, + executor: E, + ) -> anyhow::Result> + where + E: Executor<'a, Database = Sqlite>, { // language=SQLite let result = sqlx::query_as::<_, DbReadPlugin>("SELECT * FROM plugin WHERE id = ?1") @@ -473,78 +479,130 @@ impl DataDbRepository { } pub async fn get_entrypoints_by_plugin_id(&self, plugin_id: &str) -> anyhow::Result> { - self.get_entrypoints_by_plugin_id_with_executor(plugin_id, &self.pool).await + self.get_entrypoints_by_plugin_id_with_executor(plugin_id, &self.pool) + .await } - async fn get_entrypoints_by_plugin_id_with_executor<'a, E>(&self, plugin_id: &str, executor: E) -> anyhow::Result> - where - E: Executor<'a, Database=Sqlite> + async fn get_entrypoints_by_plugin_id_with_executor<'a, E>( + &self, + plugin_id: &str, + executor: E, + ) -> anyhow::Result> + where + E: Executor<'a, Database = Sqlite>, { // language=SQLite - let result = sqlx::query_as::<_, DbReadPluginEntrypoint>("SELECT * FROM plugin_entrypoint WHERE plugin_id = ?1") - .bind(plugin_id) - .fetch_all(executor) - .await?; + let result = + sqlx::query_as::<_, DbReadPluginEntrypoint>("SELECT * FROM plugin_entrypoint WHERE plugin_id = ?1") + .bind(plugin_id) + .fetch_all(executor) + .await?; Ok(result) } - pub async fn get_entrypoint_by_id(&self, plugin_id: &str, entrypoint_id: &str) -> anyhow::Result { - self.get_entrypoint_by_id_with_executor(plugin_id, entrypoint_id, &self.pool).await + pub async fn get_entrypoint_by_id( + &self, + plugin_id: &str, + entrypoint_id: &str, + ) -> anyhow::Result { + self.get_entrypoint_by_id_with_executor(plugin_id, entrypoint_id, &self.pool) + .await } - async fn get_entrypoint_by_id_with_executor<'a, E>(&self, plugin_id: &str, entrypoint_id: &str, executor: E) -> anyhow::Result - where - E: Executor<'a, Database=Sqlite>, + async fn get_entrypoint_by_id_with_executor<'a, E>( + &self, + plugin_id: &str, + entrypoint_id: &str, + executor: E, + ) -> anyhow::Result + where + E: Executor<'a, Database = Sqlite>, { // language=SQLite - let result = sqlx::query_as::<_, DbReadPluginEntrypoint>("SELECT * FROM plugin_entrypoint WHERE id = ?1 AND plugin_id = ?2") - .bind(entrypoint_id) - .bind(plugin_id) - .fetch_one(executor) - .await?; + let result = sqlx::query_as::<_, DbReadPluginEntrypoint>( + "SELECT * FROM plugin_entrypoint WHERE id = ?1 AND plugin_id = ?2", + ) + .bind(entrypoint_id) + .bind(plugin_id) + .fetch_one(executor) + .await?; Ok(result) } - pub async fn get_entrypoint_by_id_option(&self, plugin_id: &str, entrypoint_id: &str) -> anyhow::Result> { - self.get_entrypoint_by_id_option_with_executor(plugin_id, entrypoint_id, &self.pool).await + pub async fn get_entrypoint_by_id_option( + &self, + plugin_id: &str, + entrypoint_id: &str, + ) -> anyhow::Result> { + self.get_entrypoint_by_id_option_with_executor(plugin_id, entrypoint_id, &self.pool) + .await } - async fn get_entrypoint_by_id_option_with_executor<'a, E>(&self, plugin_id: &str, entrypoint_id: &str, executor: E) -> anyhow::Result> - where - E: Executor<'a, Database=Sqlite>, + async fn get_entrypoint_by_id_option_with_executor<'a, E>( + &self, + plugin_id: &str, + entrypoint_id: &str, + executor: E, + ) -> anyhow::Result> + where + E: Executor<'a, Database = Sqlite>, { // language=SQLite - let result = sqlx::query_as::<_, DbReadPluginEntrypoint>("SELECT * FROM plugin_entrypoint WHERE id = ?1 AND plugin_id = ?2") - .bind(entrypoint_id) - .bind(plugin_id) - .fetch_optional(executor) - .await?; + let result = sqlx::query_as::<_, DbReadPluginEntrypoint>( + "SELECT * FROM plugin_entrypoint WHERE id = ?1 AND plugin_id = ?2", + ) + .bind(entrypoint_id) + .bind(plugin_id) + .fetch_optional(executor) + .await?; Ok(result) } pub async fn get_inline_view_entrypoint_id_for_plugin(&self, plugin_id: &str) -> anyhow::Result> { // language=SQLite - let entrypoint_id = sqlx::query_as::<_, (String, )>("SELECT id FROM plugin_entrypoint WHERE plugin_id = ?1 AND type = 'inline-view'") - .bind(plugin_id) - .fetch_optional(&self.pool) - .await? - .map(|result| result.0); + let entrypoint_id = sqlx::query_as::<_, (String,)>( + "SELECT id FROM plugin_entrypoint WHERE plugin_id = ?1 AND type = 'inline-view'", + ) + .bind(plugin_id) + .fetch_optional(&self.pool) + .await? + .map(|result| result.0); Ok(entrypoint_id) } - pub async fn action_shortcuts(&self, plugin_id: &str, entrypoint_id: &str) -> anyhow::Result> { - let DbReadPluginEntrypoint { actions, actions_user_data, .. } = self.get_entrypoint_by_id(plugin_id, entrypoint_id) - .await?; + pub async fn action_shortcuts( + &self, + plugin_id: &str, + entrypoint_id: &str, + ) -> anyhow::Result> { + let DbReadPluginEntrypoint { + actions, + actions_user_data, + .. + } = self.get_entrypoint_by_id(plugin_id, entrypoint_id).await?; - let actions_user_data: HashMap<_, _> = actions_user_data.into_iter() - .map(|data| (data.id, (data.key, data.modifier_shift, data.modifier_control, data.modifier_alt, data.modifier_meta))) + let actions_user_data: HashMap<_, _> = actions_user_data + .into_iter() + .map(|data| { + ( + data.id, + ( + data.key, + data.modifier_shift, + data.modifier_control, + data.modifier_alt, + data.modifier_meta, + ), + ) + }) .collect(); - let action_shortcuts = actions.into_iter() + let action_shortcuts = actions + .into_iter() .map(|action| { let id = action.id; @@ -552,9 +610,7 @@ impl DataDbRepository { None => { let (physical_key, modifier_shift) = match ActionShortcutKey::from_value(&action.key) { Some(key) => key.to_physical_key(), - None => { - return Err(anyhow!("unknown key: {}", &action.key)) - }, + None => return Err(anyhow!("unknown key: {}", &action.key)), }; let (modifier_control, modifier_alt, modifier_meta) = match action.kind { @@ -564,10 +620,8 @@ impl DataDbRepository { } else { (true, false, false) } - }, - DbPluginActionShortcutKind::Alternative => { - (false, true, false) - }, + } + DbPluginActionShortcutKind::Alternative => (false, true, false), }; PhysicalShortcut { @@ -604,12 +658,12 @@ impl DataDbRepository { modifier_shift: bool, modifier_control: bool, modifier_alt: bool, - modifier_meta: bool + modifier_meta: bool, ) -> anyhow::Result> { // language=SQLite let sql = r#"SELECT json_each.value ->> 'id' FROM plugin_entrypoint e, json_each(actions_user_data) WHERE e.plugin_id = ?1 AND e.id = ?2 AND json_each.value ->> 'key' = ?3 AND json_each.value ->> 'modifier_shift' = ?4 AND json_each.value ->> 'modifier_control' = ?5 AND json_each.value ->> 'modifier_alt' = ?6 AND json_each.value ->> 'modifier_meta' = ?6"#; - let action_id = sqlx::query_as::<_, (String, )>(sql) + let action_id = sqlx::query_as::<_, (String,)>(sql) .bind(plugin_id) .bind(entrypoint_id) .bind(key.to_value()) @@ -628,13 +682,13 @@ impl DataDbRepository { match (modifier_control, modifier_alt, modifier_meta) { (false, false, true) => DbPluginActionShortcutKind::Main, (false, true, false) => DbPluginActionShortcutKind::Alternative, - _ => return Ok(None) + _ => return Ok(None), } } else { match (modifier_control, modifier_alt, modifier_meta) { (true, false, false) => DbPluginActionShortcutKind::Main, (false, true, false) => DbPluginActionShortcutKind::Alternative, - _ => return Ok(None) + _ => return Ok(None), } }; @@ -650,7 +704,7 @@ impl DataDbRepository { return Ok(None); }; - let action_id = sqlx::query_as::<_, (String, )>(sql) + let action_id = sqlx::query_as::<_, (String,)>(sql) .bind(plugin_id) .bind(entrypoint_id) .bind(logical_key.to_value()) @@ -675,7 +729,7 @@ impl DataDbRepository { pub async fn is_plugin_pending(&self, plugin_id: &str) -> anyhow::Result { // language=SQLite - let result = sqlx::query_as::<_, (u8, )>("SELECT 1 FROM pending_plugin WHERE id = ?1") + let result = sqlx::query_as::<_, (u8,)>("SELECT 1 FROM pending_plugin WHERE id = ?1") .bind(plugin_id) .fetch_optional(&self.pool) .await?; @@ -685,7 +739,7 @@ impl DataDbRepository { pub async fn does_plugin_exist(&self, plugin_id: &str) -> anyhow::Result { // language=SQLite - let result = sqlx::query_as::<_, (u8, )>("SELECT 1 FROM plugin WHERE id = ?1") + let result = sqlx::query_as::<_, (u8,)>("SELECT 1 FROM plugin WHERE id = ?1") .bind(plugin_id) .fetch_optional(&self.pool) .await?; @@ -715,21 +769,23 @@ impl DataDbRepository { } // language=SQLite - let result = sqlx::query_as::<_, DbReadPluginAssetData>("SELECT data FROM plugin_asset_data WHERE plugin_id = ?1 and path = ?2") - .bind(plugin_id) - .bind(path) - .fetch_one(&self.pool) - .await?; + let result = sqlx::query_as::<_, DbReadPluginAssetData>( + "SELECT data FROM plugin_asset_data WHERE plugin_id = ?1 and path = ?2", + ) + .bind(plugin_id) + .bind(path) + .fetch_one(&self.pool) + .await?; Ok(result.data) } async fn get_all_asset_data_paths<'a, E>(&self, plugin_id: &str, executor: E) -> anyhow::Result> - where - E: Executor<'a, Database=Sqlite>, + where + E: Executor<'a, Database = Sqlite>, { // language=SQLite - let result = sqlx::query_as::<_, (String, )>("SELECT path FROM plugin_asset_data WHERE plugin_id = ?1") + let result = sqlx::query_as::<_, (String,)>("SELECT path FROM plugin_asset_data WHERE plugin_id = ?1") .bind(plugin_id) .fetch_all(executor) .await? @@ -742,21 +798,22 @@ impl DataDbRepository { pub async fn inline_view_shortcuts(&self) -> anyhow::Result>> { // language=SQLite - let shortcuts: Vec<_> = sqlx::query_as::<_, (String, String)>("SELECT id, plugin_id FROM plugin_entrypoint WHERE type = 'inline-view'") - .fetch_all(&self.pool) - .await? - .into_iter() - .map(|(entrypoint_id, plugin_id)| async move { + let shortcuts: Vec<_> = sqlx::query_as::<_, (String, String)>( + "SELECT id, plugin_id FROM plugin_entrypoint WHERE type = 'inline-view'", + ) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|(entrypoint_id, plugin_id)| { + async move { let shortcuts = self.action_shortcuts(&plugin_id, &entrypoint_id).await?; Ok((plugin_id, shortcuts)) - }) - .collect(); + } + }) + .collect(); - join_all(shortcuts) - .await - .into_iter() - .collect() + join_all(shortcuts).await.into_iter().collect() } pub async fn mark_entrypoint_frecency(&self, plugin_id: &str, entrypoint_id: &str) -> anyhow::Result<()> { @@ -773,15 +830,19 @@ impl DataDbRepository { } // language=SQLite - let meta_params = sqlx::query_as::<_, DbFrecencyMetaParams>("SELECT reference_time, half_life FROM plugin_entrypoint_frecency_stats") - .fetch_optional(&mut *tx) - .await?; + let meta_params = sqlx::query_as::<_, DbFrecencyMetaParams>( + "SELECT reference_time, half_life FROM plugin_entrypoint_frecency_stats", + ) + .fetch_optional(&mut *tx) + .await?; let meta_params = match meta_params { None => FrecencyMetaParams::default(), - Some(meta_params) => FrecencyMetaParams { - reference_time: meta_params.reference_time, - half_life: meta_params.half_life, + Some(meta_params) => { + FrecencyMetaParams { + reference_time: meta_params.reference_time, + half_life: meta_params.half_life, + } } }; @@ -793,9 +854,7 @@ impl DataDbRepository { .await?; let mut new_stats = match stats { - None => { - FrecencyItemStats::new(meta_params.reference_time, meta_params.half_life) - } + None => FrecencyItemStats::new(meta_params.reference_time, meta_params.half_life), Some(stats) => { FrecencyItemStats { half_life: stats.half_life, @@ -833,12 +892,14 @@ impl DataDbRepository { pub async fn get_frecency_for_plugin(&self, plugin_id: &str) -> anyhow::Result> { // language=SQLite - let result = sqlx::query_as::<_, (String, f64)>("SELECT entrypoint_id, frecency FROM plugin_entrypoint_frecency_stats WHERE plugin_id = ?1") - .bind(plugin_id) - .fetch_all(&self.pool) - .await? - .into_iter() - .collect(); + let result = sqlx::query_as::<_, (String, f64)>( + "SELECT entrypoint_id, frecency FROM plugin_entrypoint_frecency_stats WHERE plugin_id = ?1", + ) + .bind(plugin_id) + .fetch_all(&self.pool) + .await? + .into_iter() + .collect(); Ok(result) } @@ -854,7 +915,12 @@ impl DataDbRepository { Ok(()) } - pub async fn set_plugin_entrypoint_enabled(&self, plugin_id: &str, entrypoint_id: &str, enabled: bool) -> anyhow::Result<()> { + pub async fn set_plugin_entrypoint_enabled( + &self, + plugin_id: &str, + entrypoint_id: &str, + enabled: bool, + ) -> anyhow::Result<()> { // language=SQLite sqlx::query("UPDATE plugin_entrypoint SET enabled = ?1 WHERE id = ?2 AND plugin_id = ?3") .bind(enabled) @@ -866,7 +932,11 @@ impl DataDbRepository { Ok(()) } - pub async fn set_global_shortcut(&self, shortcut: Option, error: Option) -> anyhow::Result<()> { + pub async fn set_global_shortcut( + &self, + shortcut: Option, + error: Option, + ) -> anyhow::Result<()> { // language=SQLite let sql = r#" INSERT INTO settings_data (id, global_shortcut) @@ -931,13 +1001,10 @@ impl DataDbRepository { }) }; - Ok(Some(( - shortcut, - shortcut_data.error, - ))) - }, + Ok(Some((shortcut, shortcut_data.error))) + } Ok(None) => Ok(None), - Err(err) => Err(anyhow!("Unable to get global shortcut from db: {:?}", err)) + Err(err) => Err(anyhow!("Unable to get global shortcut from db: {:?}", err)), } } @@ -947,10 +1014,7 @@ impl DataDbRepository { .fetch_optional(&self.pool) .await?; - let theme = settings - .map(|data| data.settings) - .flatten() - .unwrap_or_default(); + let theme = settings.map(|data| data.settings).flatten().unwrap_or_default(); Ok(theme.0) } @@ -973,12 +1037,19 @@ impl DataDbRepository { Ok(()) } - pub async fn set_preference_value(&self, plugin_id: String, entrypoint_id: Option, preference_id: String, value: DbPluginPreferenceUserData) -> anyhow::Result<()> { + pub async fn set_preference_value( + &self, + plugin_id: String, + entrypoint_id: Option, + preference_id: String, + value: DbPluginPreferenceUserData, + ) -> anyhow::Result<()> { let mut tx = self.pool.begin().await?; match entrypoint_id { None => { - let mut user_data = self.get_plugin_by_id_with_executor(&plugin_id, &mut *tx) + let mut user_data = self + .get_plugin_by_id_with_executor(&plugin_id, &mut *tx) .await? .preferences_user_data; @@ -992,7 +1063,8 @@ impl DataDbRepository { .await?; } Some(entrypoint_id) => { - let mut user_data = self.get_entrypoint_by_id_with_executor(&plugin_id, &entrypoint_id, &mut *tx) + let mut user_data = self + .get_entrypoint_by_id_with_executor(&plugin_id, &entrypoint_id, &mut *tx) .await? .preferences_user_data; @@ -1035,7 +1107,9 @@ impl DataDbRepository { pub async fn save_plugin(&self, new_plugin: DbWritePlugin) -> anyhow::Result<()> { let mut tx = self.pool.begin().await?; - let (uuid, enabled, preferences_user_data) = self.get_plugin_by_id_option_with_executor(&new_plugin.id, &mut *tx).await? + let (uuid, enabled, preferences_user_data) = self + .get_plugin_by_id_option_with_executor(&new_plugin.id, &mut *tx) + .await? .map(|plugin| (plugin.uuid, plugin.enabled, plugin.preferences_user_data)) .unwrap_or((Uuid::new_v4().to_string(), new_plugin.enabled, HashMap::new())); @@ -1061,7 +1135,9 @@ impl DataDbRepository { .execute(&mut *tx) .await?; - let mut old_entrypoint_ids = self.get_entrypoints_by_plugin_id_with_executor(&new_plugin.id, &mut *tx).await? + let mut old_entrypoint_ids = self + .get_entrypoints_by_plugin_id_with_executor(&new_plugin.id, &mut *tx) + .await? .into_iter() .map(|entrypoint| entrypoint.id) .collect::>(); @@ -1069,8 +1145,17 @@ impl DataDbRepository { for new_entrypoint in new_plugin.entrypoints { old_entrypoint_ids.remove(&new_entrypoint.id); - let (uuid, preferences_user_data, actions_user_data, enabled) = self.get_entrypoint_by_id_option_with_executor(&new_plugin.id, &new_entrypoint.id, &mut *tx).await? - .map(|entrypoint| (entrypoint.uuid, entrypoint.preferences_user_data, entrypoint.actions_user_data, entrypoint.enabled)) + let (uuid, preferences_user_data, actions_user_data, enabled) = self + .get_entrypoint_by_id_option_with_executor(&new_plugin.id, &new_entrypoint.id, &mut *tx) + .await? + .map(|entrypoint| { + ( + entrypoint.uuid, + entrypoint.preferences_user_data, + entrypoint.actions_user_data, + entrypoint.enabled, + ) + }) .unwrap_or((Uuid::new_v4().to_string(), HashMap::new(), vec![], true)); // language=SQLite @@ -1099,7 +1184,6 @@ impl DataDbRepository { .await?; } - let mut old_asset_data_paths = self.get_all_asset_data_paths(&new_plugin.id, &mut *tx).await?; for data in new_plugin.asset_data { @@ -1129,13 +1213,12 @@ impl DataDbRepository { } } - pub fn db_entrypoint_to_str(value: DbPluginEntrypointType) -> &'static str { match value { DbPluginEntrypointType::Command => "command", DbPluginEntrypointType::View => "view", DbPluginEntrypointType::InlineView => "inline-view", - DbPluginEntrypointType::EntrypointGenerator => "command-generator" // command-generator in db for backwards compatibility + DbPluginEntrypointType::EntrypointGenerator => "command-generator", // command-generator in db for backwards compatibility } } @@ -1145,16 +1228,15 @@ pub fn db_entrypoint_from_str(value: &str) -> DbPluginEntrypointType { "view" => DbPluginEntrypointType::View, "inline-view" => DbPluginEntrypointType::InlineView, "command-generator" => DbPluginEntrypointType::EntrypointGenerator, - _ => panic!("illegal entrypoint_type: {}", value) + _ => panic!("illegal entrypoint_type: {}", value), } } - pub fn db_plugin_type_to_str(value: DbPluginType) -> &'static str { match value { DbPluginType::Normal => "normal", DbPluginType::Config => "config", - DbPluginType::Bundled => "bundled" + DbPluginType::Bundled => "bundled", } } @@ -1163,6 +1245,6 @@ pub fn db_plugin_type_from_str(value: &str) -> DbPluginType { "normal" => DbPluginType::Normal, "config" => DbPluginType::Config, "bundled" => DbPluginType::Bundled, - _ => panic!("illegal plugin_type: {}", value) + _ => panic!("illegal plugin_type: {}", value), } } diff --git a/rust/server/src/plugins/download_status.rs b/rust/server/src/plugins/download_status.rs index f73cf15..01291b5 100644 --- a/rust/server/src/plugins/download_status.rs +++ b/rust/server/src/plugins/download_status.rs @@ -1,17 +1,19 @@ use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use std::sync::Mutex; use std::time::Duration; -use gauntlet_common::model::{DownloadStatus, PluginId}; +use gauntlet_common::model::DownloadStatus; +use gauntlet_common::model::PluginId; pub struct DownloadStatusHolder { - running_downloads: Arc>> + running_downloads: Arc>>, } impl DownloadStatusHolder { pub fn new() -> Self { Self { - running_downloads: Arc::new(Mutex::new(HashMap::new())) + running_downloads: Arc::new(Mutex::new(HashMap::new())), } } @@ -26,7 +28,8 @@ impl DownloadStatusHolder { pub fn download_status(&self) -> HashMap { let running_downloads = self.running_downloads.lock().expect("lock is poisoned"); - running_downloads.iter() + running_downloads + .iter() .map(|(plugin_id, status)| (plugin_id.clone(), status.clone())) .collect() } @@ -34,7 +37,7 @@ impl DownloadStatusHolder { pub struct DownloadStatusGuard { id: PluginId, - running_downloads: Arc>> + running_downloads: Arc>>, } impl DownloadStatusGuard { diff --git a/rust/server/src/plugins/frecency.rs b/rust/server/src/plugins/frecency.rs index 953ce4c..d62b878 100644 --- a/rust/server/src/plugins/frecency.rs +++ b/rust/server/src/plugins/frecency.rs @@ -1,4 +1,3 @@ - // shamelessly stolen from MIT licensed https://github.com/camdencheek/fre use std::time::SystemTime; @@ -17,14 +16,13 @@ impl Default for FrecencyMetaParams { } } - #[derive(Clone)] pub struct FrecencyItemStats { - pub(in super) half_life: f64, - pub(in super) reference_time: f64, // Time in seconds since the epoch - pub(in super) last_accessed: f64, // Time in seconds since reference_time that this item was last accessed - pub(in super) frecency: f64, - pub(in super) num_accesses: i32, + pub(super) half_life: f64, + pub(super) reference_time: f64, // Time in seconds since the epoch + pub(super) last_accessed: f64, // Time in seconds since reference_time that this item was last accessed + pub(super) frecency: f64, + pub(super) num_accesses: i32, } impl FrecencyItemStats { diff --git a/rust/server/src/plugins/icon_cache.rs b/rust/server/src/plugins/icon_cache.rs index 0c411bd..cb5493c 100644 --- a/rust/server/src/plugins/icon_cache.rs +++ b/rust/server/src/plugins/icon_cache.rs @@ -7,9 +7,7 @@ pub struct IconCache { impl IconCache { pub fn new(dirs: Dirs) -> Self { - Self { - dirs - } + Self { dirs } } // legacy @@ -24,5 +22,3 @@ impl IconCache { Ok(()) } } - - diff --git a/rust/server/src/plugins/image_gatherer.rs b/rust/server/src/plugins/image_gatherer.rs index c8eaceb..cc80c3c 100644 --- a/rust/server/src/plugins/image_gatherer.rs +++ b/rust/server/src/plugins/image_gatherer.rs @@ -1,33 +1,46 @@ use std::collections::HashMap; -use gauntlet_common::model::{ImageLike, ImageSource, ImageSourceAsset, ImageSourceUrl, RootWidget, UiWidgetId, WidgetVisitor}; -use gauntlet_plugin_runtime::BackendForPluginRuntimeApi; -use crate::plugins::js::BackendForPluginRuntimeApiImpl; -use futures::StreamExt; use std::io::Read; +use futures::StreamExt; +use gauntlet_common::model::ImageLike; +use gauntlet_common::model::ImageSource; +use gauntlet_common::model::ImageSourceAsset; +use gauntlet_common::model::ImageSourceUrl; +use gauntlet_common::model::RootWidget; +use gauntlet_common::model::UiWidgetId; +use gauntlet_common::model::WidgetVisitor; +use gauntlet_plugin_runtime::BackendForPluginRuntimeApi; + +use crate::plugins::js::BackendForPluginRuntimeApiImpl; + pub struct ImageGatherer<'a> { api: &'a BackendForPluginRuntimeApiImpl, - image_sources: HashMap>> + image_sources: HashMap>>, } impl<'a> WidgetVisitor for ImageGatherer<'a> { async fn image(&mut self, widget_id: UiWidgetId, widget: &ImageLike) { if let ImageLike::ImageSource(image_source) = &widget { - self.image_sources.insert(widget_id, get_image_date(&self.api, image_source).await); + self.image_sources + .insert(widget_id, get_image_date(&self.api, image_source).await); } } } impl<'a> ImageGatherer<'a> { - pub async fn run_gatherer(api: &'a BackendForPluginRuntimeApiImpl, root_widget: &RootWidget) -> anyhow::Result>> { + pub async fn run_gatherer( + api: &'a BackendForPluginRuntimeApiImpl, + root_widget: &RootWidget, + ) -> anyhow::Result>> { let mut gatherer = Self { api, - image_sources: HashMap::new() + image_sources: HashMap::new(), }; gatherer.root_widget(root_widget).await; - gatherer.image_sources + gatherer + .image_sources .into_iter() .map(|(widget_id, image)| image.map(|image| (widget_id, image))) .collect::>() diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index c1a536d..909899a 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -4,39 +4,90 @@ use std::fs::File; use std::hash::Hash; use std::io; use std::net::SocketAddr; -use std::path::{Path, PathBuf}; +use std::path::Path; +use std::path::PathBuf; use std::pin::Pin; use std::rc::Rc; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use anyhow::{anyhow, Context}; +use anyhow::anyhow; +use anyhow::Context; use futures::AsyncBufReadExt; -use interprocess::local_socket::{ListenerOptions, ToFsName, ToNsName}; -use interprocess::local_socket::tokio::{RecvHalf, SendHalf}; -use interprocess::local_socket::traits::tokio::{Listener, Stream}; +use gauntlet_common::dirs::Dirs; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::KeyboardEventOrigin; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::RootWidget; +use gauntlet_common::model::SearchResultAccessory; +use gauntlet_common::model::SearchResultEntrypointType; +use gauntlet_common::model::UiPropertyValue; +use gauntlet_common::model::UiRenderLocation; +use gauntlet_common::model::UiWidgetId; +use gauntlet_common::rpc::frontend_api::FrontendApi; +use gauntlet_common::settings_env_data_to_string; +use gauntlet_plugin_runtime::recv_message; +use gauntlet_plugin_runtime::send_message; +use gauntlet_plugin_runtime::BackendForPluginRuntimeApi; +use gauntlet_plugin_runtime::JsClipboardData; +use gauntlet_plugin_runtime::JsEvent; +use gauntlet_plugin_runtime::JsGeneratedSearchItem; +use gauntlet_plugin_runtime::JsGeneratedSearchItemAccessory; +use gauntlet_plugin_runtime::JsGeneratedSearchItemActionType; +use gauntlet_plugin_runtime::JsInit; +use gauntlet_plugin_runtime::JsKeyboardEventOrigin; +use gauntlet_plugin_runtime::JsMessage; +use gauntlet_plugin_runtime::JsMessageSide; +use gauntlet_plugin_runtime::JsPluginCode; +use gauntlet_plugin_runtime::JsPluginPermissions; +use gauntlet_plugin_runtime::JsPluginPermissionsExec; +use gauntlet_plugin_runtime::JsPluginPermissionsFileSystem; +use gauntlet_plugin_runtime::JsPluginPermissionsMainSearchBar; +use gauntlet_plugin_runtime::JsPluginRuntimeMessage; +use gauntlet_plugin_runtime::JsPreferenceUserData; +use gauntlet_plugin_runtime::JsRequest; +use gauntlet_plugin_runtime::JsResponse; +use gauntlet_plugin_runtime::JsUiPropertyValue; +use gauntlet_plugin_runtime::JsUiRenderLocation; +use interprocess::local_socket::tokio::RecvHalf; +use interprocess::local_socket::tokio::SendHalf; +use interprocess::local_socket::traits::tokio::Listener; +use interprocess::local_socket::traits::tokio::Stream; +use interprocess::local_socket::ListenerOptions; +use interprocess::local_socket::ToFsName; +use interprocess::local_socket::ToNsName; use interprocess::TryClone; use once_cell::sync::Lazy; -use serde::{Deserialize, Serialize}; -use tokio::io::{AsyncRead, AsyncReadExt}; +use serde::Deserialize; +use serde::Serialize; +use tokio::io::AsyncRead; +use tokio::io::AsyncReadExt; use tokio::net::TcpStream; use tokio::sync::Mutex; use tokio::task::spawn_blocking; use tokio_util::sync::CancellationToken; -use gauntlet_common::dirs::Dirs; -use gauntlet_common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, SearchResultAccessory, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId}; -use gauntlet_common::rpc::frontend_api::FrontendApi; -use gauntlet_common::settings_env_data_to_string; -use gauntlet_plugin_runtime::{recv_message, send_message, BackendForPluginRuntimeApi, JsGeneratedSearchItem, JsClipboardData, JsInit, JsKeyboardEventOrigin, JsPluginCode, JsPluginPermissions, JsPreferenceUserData, JsEvent, JsUiPropertyValue, JsRequest, JsUiRenderLocation, JsResponse, JsMessage, JsPluginPermissionsFileSystem, JsPluginPermissionsExec, JsPluginPermissionsMainSearchBar, JsMessageSide, JsPluginRuntimeMessage, JsGeneratedSearchItemAccessory, JsGeneratedSearchItemActionType}; -use crate::model::{IntermediateUiEvent}; + +use crate::model::IntermediateUiEvent; use crate::plugins::clipboard::Clipboard; -use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; +use crate::plugins::data_db_repository::db_entrypoint_from_str; +use crate::plugins::data_db_repository::DataDbRepository; +use crate::plugins::data_db_repository::DbPluginClipboardPermissions; +use crate::plugins::data_db_repository::DbPluginEntrypointType; +use crate::plugins::data_db_repository::DbPluginPreference; +use crate::plugins::data_db_repository::DbPluginPreferenceUserData; +use crate::plugins::data_db_repository::DbReadPlugin; +use crate::plugins::data_db_repository::DbReadPluginEntrypoint; use crate::plugins::icon_cache::IconCache; -use crate::plugins::run_status::RunStatusGuard; -use crate::search::{SearchIndex, SearchIndexItem, SearchIndexItemAction, SearchIndexItemActionActionType}; -use crate::{PLUGIN_CONNECT_ENV, PLUGIN_UUID_ENV}; use crate::plugins::image_gatherer::ImageGatherer; +use crate::plugins::run_status::RunStatusGuard; +use crate::search::SearchIndex; +use crate::search::SearchIndexItem; +use crate::search::SearchIndexItemAction; +use crate::search::SearchIndexItemActionActionType; +use crate::PLUGIN_CONNECT_ENV; +use crate::PLUGIN_UUID_ENV; pub struct PluginRuntimeData { pub id: PluginId, @@ -74,18 +125,13 @@ pub struct PluginRuntimePermissions { pub enum PluginPermissionsClipboard { Read, Write, - Clear + Clear, } #[derive(Clone, Debug)] pub enum PluginCommand { - One { - id: PluginId, - data: OnePluginCommandData, - }, - All { - data: AllPluginCommandData, - } + One { id: PluginId, data: OnePluginCommandData }, + All { data: AllPluginCommandData }, } #[derive(Clone, Debug)] @@ -99,7 +145,7 @@ pub enum OnePluginCommandData { }, RunGeneratedEntrypoint { entrypoint_id: String, - action_index: usize + action_index: usize, }, HandleViewEvent { widget_id: UiWidgetId, @@ -120,13 +166,10 @@ pub enum OnePluginCommandData { #[derive(Clone, Debug)] pub enum AllPluginCommandData { - OpenInlineView { - text: String - } + OpenInlineView { text: String }, } pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: RunStatusGuard) -> anyhow::Result<()> { - let runtime_permissions = PluginRuntimePermissions { clipboard: data.permissions.clipboard, }; @@ -165,7 +208,8 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run std::fs::create_dir_all(stderr_file.parent().unwrap())?; File::create(&stderr_file)?; - let stderr_file = stderr_file.to_str() + let stderr_file = stderr_file + .to_str() .context("non-uft8 paths are not supported")? .to_string(); @@ -188,7 +232,9 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run // namespaced, removed when both client and server disconnect #[cfg(target_os = "windows")] - let name = name_str.clone().to_ns_name::()?; + let name = name_str + .clone() + .to_ns_name::()?; // not namespaced, needs to be cleaned up manually, // by using close-behind semantics and additionally removing it before creating a new runtime @@ -204,10 +250,7 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run uds_socket_file.to_fs_name::()? }; - let listener = ListenerOptions::new() - .name(name) - .reclaim_name(false) - .create_tokio()?; + let listener = ListenerOptions::new().name(name).reclaim_name(false).create_tokio()?; let home_dir = home_dir .to_str() @@ -259,8 +302,7 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run stderr_file, }; - let current_exe = std::env::current_exe() - .context("unable to get current_exe")?; + let current_exe = std::env::current_exe().context("unable to get current_exe")?; #[cfg(not(feature = "scenario_runner"))] let mut runtime_process = std::process::Command::new(current_exe) @@ -271,9 +313,7 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run // use only for debugging and scenario_runner, only works if only one plugin is enabled #[cfg(feature = "scenario_runner")] - std::thread::spawn(move || { - gauntlet_plugin_runtime::run_plugin_runtime(name_str.to_str().unwrap().to_string()) - }); + std::thread::spawn(move || gauntlet_plugin_runtime::run_plugin_runtime(name_str.to_str().unwrap().to_string())); let conn = listener.accept().await?; @@ -342,7 +382,8 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run #[cfg(not(feature = "scenario_runner"))] { - let code = runtime_process.wait() + let code = runtime_process + .wait() .context("Error while waiting for JS runtime process to finish")? .code(); @@ -353,15 +394,19 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run } else { tracing::error!("Runtime process finished with status code: {code}") } - }, - None => tracing::error!("Process terminated by signal") + } + None => tracing::error!("Process terminated by signal"), } } Ok(()) } -async fn event_loop(command_receiver: &mut tokio::sync::broadcast::Receiver, send: &Mutex, plugin_id: PluginId) -> anyhow::Result<()> { +async fn event_loop( + command_receiver: &mut tokio::sync::broadcast::Receiver, + send: &Mutex, + plugin_id: PluginId, +) -> anyhow::Result<()> { let command = command_receiver.recv().await?; let event = match command { @@ -371,32 +416,41 @@ async fn event_loop(command_receiver: &mut tokio::sync::broadcast::Receiver { - Some(IntermediateUiEvent::OpenView { - entrypoint_id, - }) - } - OnePluginCommandData::CloseView => { - Some(IntermediateUiEvent::CloseView) + Some(IntermediateUiEvent::OpenView { entrypoint_id }) } + OnePluginCommandData::CloseView => Some(IntermediateUiEvent::CloseView), OnePluginCommandData::RunCommand { entrypoint_id } => { - Some(IntermediateUiEvent::RunCommand { - entrypoint_id, - }) + Some(IntermediateUiEvent::RunCommand { entrypoint_id }) } - OnePluginCommandData::RunGeneratedEntrypoint { entrypoint_id, action_index } => { + OnePluginCommandData::RunGeneratedEntrypoint { + entrypoint_id, + action_index, + } => { Some(IntermediateUiEvent::RunGeneratedEntrypoint { entrypoint_id, - action_index + action_index, }) } - OnePluginCommandData::HandleViewEvent { widget_id, event_name, event_arguments } => { + OnePluginCommandData::HandleViewEvent { + widget_id, + event_name, + event_arguments, + } => { Some(IntermediateUiEvent::HandleViewEvent { widget_id, event_name, event_arguments, }) } - OnePluginCommandData::HandleKeyboardEvent { entrypoint_id, origin, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { + OnePluginCommandData::HandleKeyboardEvent { + entrypoint_id, + origin, + key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + } => { Some(IntermediateUiEvent::HandleKeyboardEvent { entrypoint_id, origin, @@ -404,40 +458,41 @@ async fn event_loop(command_receiver: &mut tokio::sync::broadcast::Receiver { - Some(IntermediateUiEvent::RefreshSearchIndex) - } + OnePluginCommandData::RefreshSearchIndex => Some(IntermediateUiEvent::RefreshSearchIndex), } } } PluginCommand::All { data } => { match data { - AllPluginCommandData::OpenInlineView { text } => { - Some(IntermediateUiEvent::OpenInlineView { text }) - } + AllPluginCommandData::OpenInlineView { text } => Some(IntermediateUiEvent::OpenInlineView { text }), } } }; - if let Some(event) = event { let mut send = send.lock().await; - send_message(JsMessageSide::Backend, &mut send, JsMessage::Event(from_intermediate_to_js_event(event))).await?; + send_message( + JsMessageSide::Backend, + &mut send, + JsMessage::Event(from_intermediate_to_js_event(event)), + ) + .await?; } Ok(()) } - -async fn request_loop(recv: &mut RecvHalf, send: &Mutex, api: &BackendForPluginRuntimeApiImpl) -> anyhow::Result { +async fn request_loop( + recv: &mut RecvHalf, + send: &Mutex, + api: &BackendForPluginRuntimeApiImpl, +) -> anyhow::Result { match recv_message::(JsMessageSide::Backend, recv).await { - Err(e) => { - Err(anyhow!("Unable to handle message: {:?}", e)) - } + Err(e) => Err(anyhow!("Unable to handle message: {:?}", e)), Ok(message) => { tracing::trace!("Handling js runtime message: {:?}", message); @@ -472,13 +527,26 @@ async fn request_loop(recv: &mut RecvHalf, send: &Mutex, api: &Backend async fn handle_message(message: JsRequest, api: &BackendForPluginRuntimeApiImpl) -> anyhow::Result { match message { - JsRequest::Render { entrypoint_id, entrypoint_name, render_location, top_level_view, container } => { + JsRequest::Render { + entrypoint_id, + entrypoint_name, + render_location, + top_level_view, + container, + } => { let render_location = match render_location { JsUiRenderLocation::InlineView => UiRenderLocation::InlineView, - JsUiRenderLocation::View => UiRenderLocation::View + JsUiRenderLocation::View => UiRenderLocation::View, }; - api.ui_render(entrypoint_id, entrypoint_name, render_location, top_level_view, container).await?; + api.ui_render( + entrypoint_id, + entrypoint_name, + render_location, + top_level_view, + container, + ) + .await?; Ok(JsResponse::Nothing) } @@ -487,18 +555,30 @@ async fn handle_message(message: JsRequest, api: &BackendForPluginRuntimeApiImpl Ok(JsResponse::Nothing) } - JsRequest::ShowPluginErrorView { entrypoint_id, render_location } => { + JsRequest::ShowPluginErrorView { + entrypoint_id, + render_location, + } => { let render_location = match render_location { JsUiRenderLocation::InlineView => UiRenderLocation::InlineView, - JsUiRenderLocation::View => UiRenderLocation::View + JsUiRenderLocation::View => UiRenderLocation::View, }; api.ui_show_plugin_error_view(entrypoint_id, render_location).await?; Ok(JsResponse::Nothing) } - JsRequest::ShowPreferenceRequiredView { entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => { - api.ui_show_preferences_required_view(entrypoint_id, plugin_preferences_required, entrypoint_preferences_required).await?; + JsRequest::ShowPreferenceRequiredView { + entrypoint_id, + plugin_preferences_required, + entrypoint_preferences_required, + } => { + api.ui_show_preferences_required_view( + entrypoint_id, + plugin_preferences_required, + entrypoint_preferences_required, + ) + .await?; Ok(JsResponse::Nothing) } @@ -517,66 +597,54 @@ async fn handle_message(message: JsRequest, api: &BackendForPluginRuntimeApiImpl Ok(JsResponse::Nothing) } - JsRequest::ReloadSearchIndex { generated_entrypoints, refresh_search_list } => { - api.reload_search_index(generated_entrypoints, refresh_search_list).await?; + JsRequest::ReloadSearchIndex { + generated_entrypoints, + refresh_search_list, + } => { + api.reload_search_index(generated_entrypoints, refresh_search_list) + .await?; Ok(JsResponse::Nothing) } JsRequest::GetAssetData { path } => { let data = api.get_asset_data(&path).await?; - Ok(JsResponse::AssetData { - data - }) + Ok(JsResponse::AssetData { data }) } JsRequest::GetEntrypointGeneratorEntrypointIds => { let data = api.get_entrypoint_generator_entrypoint_ids().await?; - Ok(JsResponse::EntrypointGeneratorEntrypointIds { - data - }) + Ok(JsResponse::EntrypointGeneratorEntrypointIds { data }) } JsRequest::GetPluginPreferences => { let data = api.get_plugin_preferences().await?; - Ok(JsResponse::PluginPreferences { - data - }) + Ok(JsResponse::PluginPreferences { data }) } JsRequest::GetEntrypointPreferences { entrypoint_id } => { let data = api.get_entrypoint_preferences(entrypoint_id).await?; - Ok(JsResponse::EntrypointPreferences { - data - }) + Ok(JsResponse::EntrypointPreferences { data }) } JsRequest::PluginPreferencesRequired => { let data = api.plugin_preferences_required().await?; - Ok(JsResponse::PluginPreferencesRequired { - data - }) + Ok(JsResponse::PluginPreferencesRequired { data }) } JsRequest::EntrypointPreferencesRequired { entrypoint_id } => { let data = api.entrypoint_preferences_required(entrypoint_id).await?; - Ok(JsResponse::EntrypointPreferencesRequired { - data - }) + Ok(JsResponse::EntrypointPreferencesRequired { data }) } JsRequest::ClipboardRead => { let data = api.clipboard_read().await?; - Ok(JsResponse::ClipboardRead { - data - }) + Ok(JsResponse::ClipboardRead { data }) } JsRequest::ClipboardReadText => { let data = api.clipboard_read_text().await?; - Ok(JsResponse::ClipboardReadText { - data - }) + Ok(JsResponse::ClipboardReadText { data }) } JsRequest::ClipboardWrite { data } => { api.clipboard_write(data).await?; @@ -593,45 +661,64 @@ async fn handle_message(message: JsRequest, api: &BackendForPluginRuntimeApiImpl Ok(JsResponse::Nothing) } - JsRequest::GetActionIdForShortcut { entrypoint_id, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { - let data = api.ui_get_action_id_for_shortcut( - entrypoint_id, - key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta - ).await?; + JsRequest::GetActionIdForShortcut { + entrypoint_id, + key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + } => { + let data = api + .ui_get_action_id_for_shortcut( + entrypoint_id, + key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + ) + .await?; - Ok(JsResponse::ActionIdForShortcut { - data - }) + Ok(JsResponse::ActionIdForShortcut { data }) } } } fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsEvent { match event { - IntermediateUiEvent::OpenView { entrypoint_id } => JsEvent::OpenView { - entrypoint_id: entrypoint_id.to_string(), - }, + IntermediateUiEvent::OpenView { entrypoint_id } => { + JsEvent::OpenView { + entrypoint_id: entrypoint_id.to_string(), + } + } IntermediateUiEvent::CloseView => JsEvent::CloseView, - IntermediateUiEvent::RunCommand { entrypoint_id } => JsEvent::RunCommand { - entrypoint_id - }, - IntermediateUiEvent::RunGeneratedEntrypoint { entrypoint_id, action_index } => JsEvent::RunGeneratedEntrypoint { + IntermediateUiEvent::RunCommand { entrypoint_id } => JsEvent::RunCommand { entrypoint_id }, + IntermediateUiEvent::RunGeneratedEntrypoint { entrypoint_id, action_index, - }, - IntermediateUiEvent::HandleViewEvent { widget_id, event_name, event_arguments } => { - let event_arguments = event_arguments.into_iter() - .map(|arg| match arg { - UiPropertyValue::String(value) => JsUiPropertyValue::String { value }, - UiPropertyValue::Number(value) => JsUiPropertyValue::Number { value }, - UiPropertyValue::Bool(value) => JsUiPropertyValue::Bool { value }, - UiPropertyValue::Undefined => JsUiPropertyValue::Undefined, - UiPropertyValue::Array(_) | UiPropertyValue::Bytes(_) | UiPropertyValue::Object(_) => { - todo!() + } => { + JsEvent::RunGeneratedEntrypoint { + entrypoint_id, + action_index, + } + } + IntermediateUiEvent::HandleViewEvent { + widget_id, + event_name, + event_arguments, + } => { + let event_arguments = event_arguments + .into_iter() + .map(|arg| { + match arg { + UiPropertyValue::String(value) => JsUiPropertyValue::String { value }, + UiPropertyValue::Number(value) => JsUiPropertyValue::Number { value }, + UiPropertyValue::Bool(value) => JsUiPropertyValue::Bool { value }, + UiPropertyValue::Undefined => JsUiPropertyValue::Undefined, + UiPropertyValue::Array(_) | UiPropertyValue::Bytes(_) | UiPropertyValue::Object(_) => { + todo!() + } } }) .collect(); @@ -642,7 +729,15 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsEvent { event_arguments, } } - IntermediateUiEvent::HandleKeyboardEvent { entrypoint_id, origin, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => { + IntermediateUiEvent::HandleKeyboardEvent { + entrypoint_id, + origin, + key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + } => { JsEvent::KeyboardEvent { entrypoint_id: entrypoint_id.to_string(), origin: match origin { @@ -653,7 +748,7 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsEvent { modifier_shift, modifier_control, modifier_alt, - modifier_meta + modifier_meta, } } IntermediateUiEvent::OpenInlineView { text } => JsEvent::OpenInlineView { text }, @@ -671,7 +766,7 @@ pub struct BackendForPluginRuntimeApiImpl { plugin_uuid: String, plugin_id: PluginId, plugin_name: String, - permissions: PluginRuntimePermissions + permissions: PluginRuntimePermissions, } impl BackendForPluginRuntimeApiImpl { @@ -684,7 +779,7 @@ impl BackendForPluginRuntimeApiImpl { plugin_uuid: String, plugin_id: PluginId, plugin_name: String, - permissions: PluginRuntimePermissions + permissions: PluginRuntimePermissions, ) -> Self { Self { icon_cache, @@ -695,38 +790,58 @@ impl BackendForPluginRuntimeApiImpl { plugin_uuid, plugin_id, plugin_name, - permissions + permissions, } } } impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { - async fn reload_search_index(&self, generated_entrypoints: Vec, refresh_search_list: bool) -> anyhow::Result<()> { - let DbReadPlugin { name, .. } = self.repository.get_plugin_by_id(&self.plugin_id.to_string()) + async fn reload_search_index( + &self, + generated_entrypoints: Vec, + refresh_search_list: bool, + ) -> anyhow::Result<()> { + let DbReadPlugin { name, .. } = self + .repository + .get_plugin_by_id(&self.plugin_id.to_string()) .await .context("error when getting plugin by id")?; - let entrypoints = self.repository.get_entrypoints_by_plugin_id(&self.plugin_id.to_string()) + let entrypoints = self + .repository + .get_entrypoints_by_plugin_id(&self.plugin_id.to_string()) .await .context("error when getting entrypoints by plugin id")?; - let frecency_map = self.repository.get_frecency_for_plugin(&self.plugin_id.to_string()) + let frecency_map = self + .repository + .get_frecency_for_plugin(&self.plugin_id.to_string()) .await .context("error when getting frecency for plugin")?; let mut shortcuts = HashMap::new(); for DbReadPluginEntrypoint { id, .. } in &entrypoints { - let entrypoint_shortcuts = self.repository.action_shortcuts(&self.plugin_id.to_string(), id).await?; + let entrypoint_shortcuts = self + .repository + .action_shortcuts(&self.plugin_id.to_string(), id) + .await?; shortcuts.insert(id.clone(), entrypoint_shortcuts); } - let generator_names: HashMap<_, _> = entrypoints.iter() - .filter(|entrypoint| matches!(db_entrypoint_from_str(&entrypoint.entrypoint_type), DbPluginEntrypointType::EntrypointGenerator)) + let generator_names: HashMap<_, _> = entrypoints + .iter() + .filter(|entrypoint| { + matches!( + db_entrypoint_from_str(&entrypoint.entrypoint_type), + DbPluginEntrypointType::EntrypointGenerator + ) + }) .map(|entrypoint| (entrypoint.id.clone(), entrypoint.name.clone())) .collect(); - let mut generated_search_items = generated_entrypoints.into_iter() + let mut generated_search_items = generated_entrypoints + .into_iter() .map(|item| { let entrypoint_icon = match item.entrypoint_icon { None => None, @@ -735,16 +850,15 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { let entrypoint_frecency = frecency_map.get(&item.entrypoint_id).cloned().unwrap_or(0.0); - let shortcuts = shortcuts - .get(&item.generator_entrypoint_id); + let shortcuts = shortcuts.get(&item.generator_entrypoint_id); - let entrypoint_actions = item.entrypoint_actions.iter() + let entrypoint_actions = item + .entrypoint_actions + .iter() .map(|action| { let shortcut = match (shortcuts, &action.id) { - (Some(shortcuts), Some(id)) => { - shortcuts.get(id).cloned() - } - _ => None + (Some(shortcuts), Some(id)) => shortcuts.get(id).cloned(), + _ => None, }; SearchIndexItemAction { @@ -758,7 +872,9 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { }) .collect(); - let entrypoint_accessories = item.entrypoint_accessories.into_iter() + let entrypoint_accessories = item + .entrypoint_accessories + .into_iter() .map(|accessory| { match accessory { JsGeneratedSearchItemAccessory::TextAccessory { text, icon, tooltip } => { @@ -792,7 +908,9 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { for entrypoint in &entrypoints { if let Some(path_to_asset) = &entrypoint.icon_path { - let result = self.repository.get_asset_data(&self.plugin_id.to_string(), path_to_asset) + let result = self + .repository + .get_asset_data(&self.plugin_id.to_string(), path_to_asset) .await; if let Ok(data) = result { @@ -801,7 +919,8 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { } } - let mut builtin_search_items = entrypoints.into_iter() + let mut builtin_search_items = entrypoints + .into_iter() .filter(|entrypoint| entrypoint.enabled) .map(|entrypoint| { let entrypoint_type = db_entrypoint_from_str(&entrypoint.entrypoint_type); @@ -814,9 +933,9 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { Some(path_to_asset) => { match icon_asset_data.get(&(entrypoint.id, path_to_asset)) { None => None, - Some(data) => Some(bytes::Bytes::copy_from_slice(data)) + Some(data) => Some(bytes::Bytes::copy_from_slice(data)), } - }, + } }; let entrypoint_id = EntrypointId::from_string(entrypoint_id); @@ -833,7 +952,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { entrypoint_actions: vec![], entrypoint_accessories: vec![], })) - }, + } DbPluginEntrypointType::View => { Ok(Some(SearchIndexItem { entrypoint_type: SearchResultEntrypointType::View, @@ -845,10 +964,8 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { entrypoint_actions: vec![], entrypoint_accessories: vec![], })) - }, - DbPluginEntrypointType::EntrypointGenerator | DbPluginEntrypointType::InlineView => { - Ok(None) } + DbPluginEntrypointType::EntrypointGenerator | DbPluginEntrypointType::InlineView => Ok(None), } }) .collect::>>()? @@ -858,24 +975,40 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { generated_search_items.append(&mut builtin_search_items); - self.search_index.save_for_plugin(self.plugin_id.clone(), name, generated_search_items, refresh_search_list) + self.search_index + .save_for_plugin( + self.plugin_id.clone(), + name, + generated_search_items, + refresh_search_list, + ) .context("error when updating search index")?; Ok(()) } async fn get_asset_data(&self, path: &str) -> anyhow::Result> { - let data = self.repository.get_asset_data(&self.plugin_id.to_string(), &path) + let data = self + .repository + .get_asset_data(&self.plugin_id.to_string(), &path) .await?; Ok(data) } async fn get_entrypoint_generator_entrypoint_ids(&self) -> anyhow::Result> { - let result = self.repository.get_entrypoints_by_plugin_id(&self.plugin_id.to_string()).await? + let result = self + .repository + .get_entrypoints_by_plugin_id(&self.plugin_id.to_string()) + .await? .into_iter() .filter(|entrypoint| entrypoint.enabled) - .filter(|entrypoint| matches!(db_entrypoint_from_str(&entrypoint.entrypoint_type), DbPluginEntrypointType::EntrypointGenerator)) + .filter(|entrypoint| { + matches!( + db_entrypoint_from_str(&entrypoint.entrypoint_type), + DbPluginEntrypointType::EntrypointGenerator + ) + }) .map(|entrypoint| entrypoint.id) .collect::>(); @@ -883,15 +1016,25 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { } async fn get_plugin_preferences(&self) -> anyhow::Result> { - let DbReadPlugin { preferences, preferences_user_data, .. } = self.repository - .get_plugin_by_id(&self.plugin_id.to_string()) - .await?; + let DbReadPlugin { + preferences, + preferences_user_data, + .. + } = self.repository.get_plugin_by_id(&self.plugin_id.to_string()).await?; Ok(preferences_to_js(preferences, preferences_user_data)) } - async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result> { - let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = self.repository + async fn get_entrypoint_preferences( + &self, + entrypoint_id: EntrypointId, + ) -> anyhow::Result> { + let DbReadPluginEntrypoint { + preferences, + preferences_user_data, + .. + } = self + .repository .get_entrypoint_by_id(&self.plugin_id.to_string(), &entrypoint_id.to_string()) .await?; @@ -899,24 +1042,30 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { } async fn plugin_preferences_required(&self) -> anyhow::Result { - let DbReadPlugin { preferences, preferences_user_data, .. } = self.repository - .get_plugin_by_id(&self.plugin_id.to_string()).await?; + let DbReadPlugin { + preferences, + preferences_user_data, + .. + } = self.repository.get_plugin_by_id(&self.plugin_id.to_string()).await?; Ok(any_preferences_missing_value(preferences, preferences_user_data)) } async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result { - let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = self.repository - .get_entrypoint_by_id(&self.plugin_id.to_string(), &entrypoint_id.to_string()).await?; + let DbReadPluginEntrypoint { + preferences, + preferences_user_data, + .. + } = self + .repository + .get_entrypoint_by_id(&self.plugin_id.to_string(), &entrypoint_id.to_string()) + .await?; Ok(any_preferences_missing_value(preferences, preferences_user_data)) } async fn clipboard_read(&self) -> anyhow::Result { - let allow = self - .permissions - .clipboard - .contains(&PluginPermissionsClipboard::Read); + let allow = self.permissions.clipboard.contains(&PluginPermissionsClipboard::Read); if !allow { return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); @@ -928,10 +1077,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { } async fn clipboard_read_text(&self) -> anyhow::Result> { - let allow = self - .permissions - .clipboard - .contains(&PluginPermissionsClipboard::Read); + let allow = self.permissions.clipboard.contains(&PluginPermissionsClipboard::Read); if !allow { return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); @@ -943,10 +1089,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { } async fn clipboard_write(&self, data: JsClipboardData) -> anyhow::Result<()> { - let allow = self - .permissions - .clipboard - .contains(&PluginPermissionsClipboard::Write); + let allow = self.permissions.clipboard.contains(&PluginPermissionsClipboard::Write); if !allow { return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); @@ -958,10 +1101,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { } async fn clipboard_write_text(&self, data: String) -> anyhow::Result<()> { - let allow = self - .permissions - .clipboard - .contains(&PluginPermissionsClipboard::Write); + let allow = self.permissions.clipboard.contains(&PluginPermissionsClipboard::Write); if !allow { return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); @@ -973,10 +1113,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { } async fn clipboard_clear(&self) -> anyhow::Result<()> { - let allow = self - .permissions - .clipboard - .contains(&PluginPermissionsClipboard::Clear); + let allow = self.permissions.clipboard.contains(&PluginPermissionsClipboard::Clear); if !allow { return Err(anyhow!("Plugin doesn't have 'clear' permission for clipboard")); @@ -988,7 +1125,9 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { } async fn ui_update_loading_bar(&self, entrypoint_id: EntrypointId, show: bool) -> anyhow::Result<()> { - self.frontend_api.update_loading_bar(self.plugin_id.clone(), entrypoint_id, show).await?; + self.frontend_api + .update_loading_bar(self.plugin_id.clone(), entrypoint_id, show) + .await?; Ok(()) } @@ -1012,17 +1151,20 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { modifier_shift: bool, modifier_control: bool, modifier_alt: bool, - modifier_meta: bool + modifier_meta: bool, ) -> anyhow::Result> { - let result = self.repository.get_action_id_for_shortcut( - &self.plugin_id.to_string(), - &entrypoint_id.to_string(), - PhysicalKey::from_value(key), - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta - ).await?; + let result = self + .repository + .get_action_id_for_shortcut( + &self.plugin_id.to_string(), + &entrypoint_id.to_string(), + PhysicalKey::from_value(key), + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + ) + .await?; Ok(result) } @@ -1035,19 +1177,20 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { top_level_view: bool, container: RootWidget, ) -> anyhow::Result<()> { - let images = ImageGatherer::run_gatherer(&self, &container).await?; - self.frontend_api.replace_view( - self.plugin_id.clone(), - self.plugin_name.clone(), - entrypoint_id, - entrypoint_name, - render_location, - top_level_view, - container, - images - ).await?; + self.frontend_api + .replace_view( + self.plugin_id.clone(), + self.plugin_name.clone(), + entrypoint_id, + entrypoint_name, + render_location, + top_level_view, + container, + images, + ) + .await?; Ok(()) } @@ -1055,13 +1198,11 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { async fn ui_show_plugin_error_view( &self, entrypoint_id: EntrypointId, - render_location: UiRenderLocation + render_location: UiRenderLocation, ) -> anyhow::Result<()> { - self.frontend_api.show_plugin_error_view( - self.plugin_id.clone(), - entrypoint_id, - render_location - ).await?; + self.frontend_api + .show_plugin_error_view(self.plugin_id.clone(), entrypoint_id, render_location) + .await?; Ok(()) } @@ -1070,15 +1211,16 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { &self, entrypoint_id: EntrypointId, plugin_preferences_required: bool, - entrypoint_preferences_required: bool + entrypoint_preferences_required: bool, ) -> anyhow::Result<()> { - - self.frontend_api.show_preference_required_view( - self.plugin_id.clone(), - entrypoint_id, - plugin_preferences_required, - entrypoint_preferences_required - ).await?; + self.frontend_api + .show_preference_required_view( + self.plugin_id.clone(), + entrypoint_id, + plugin_preferences_required, + entrypoint_preferences_required, + ) + .await?; Ok(()) } @@ -1090,31 +1232,91 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { } } - fn preferences_to_js( preferences: HashMap, - mut preferences_user_data: HashMap + mut preferences_user_data: HashMap, ) -> HashMap { - preferences.into_iter() + preferences + .into_iter() .map(|(name, preference)| { let user_data = match preferences_user_data.remove(&name) { - None => match preference { - DbPluginPreference::Number { default, .. } => JsPreferenceUserData::Number(default.expect("at this point preference should always have value")), - DbPluginPreference::String { default, .. } => JsPreferenceUserData::String(default.expect("at this point preference should always have value")), - DbPluginPreference::Enum { default, .. } => JsPreferenceUserData::String(default.expect("at this point preference should always have value")), - DbPluginPreference::Bool { default, .. } => JsPreferenceUserData::Bool(default.expect("at this point preference should always have value")), - DbPluginPreference::ListOfStrings { default, .. } => JsPreferenceUserData::ListOfStrings(default.expect("at this point preference should always have value")), - DbPluginPreference::ListOfNumbers { default, .. } => JsPreferenceUserData::ListOfNumbers(default.expect("at this point preference should always have value")), - DbPluginPreference::ListOfEnums { default, .. } => JsPreferenceUserData::ListOfStrings(default.expect("at this point preference should always have value")), + None => { + match preference { + DbPluginPreference::Number { default, .. } => { + JsPreferenceUserData::Number( + default.expect("at this point preference should always have value"), + ) + } + DbPluginPreference::String { default, .. } => { + JsPreferenceUserData::String( + default.expect("at this point preference should always have value"), + ) + } + DbPluginPreference::Enum { default, .. } => { + JsPreferenceUserData::String( + default.expect("at this point preference should always have value"), + ) + } + DbPluginPreference::Bool { default, .. } => { + JsPreferenceUserData::Bool( + default.expect("at this point preference should always have value"), + ) + } + DbPluginPreference::ListOfStrings { default, .. } => { + JsPreferenceUserData::ListOfStrings( + default.expect("at this point preference should always have value"), + ) + } + DbPluginPreference::ListOfNumbers { default, .. } => { + JsPreferenceUserData::ListOfNumbers( + default.expect("at this point preference should always have value"), + ) + } + DbPluginPreference::ListOfEnums { default, .. } => { + JsPreferenceUserData::ListOfStrings( + default.expect("at this point preference should always have value"), + ) + } + } } - Some(user_data) => match user_data { - DbPluginPreferenceUserData::Number { value } => JsPreferenceUserData::Number(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::String { value } => JsPreferenceUserData::String(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::Enum { value } => JsPreferenceUserData::String(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::Bool { value } => JsPreferenceUserData::Bool(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::ListOfStrings { value } => JsPreferenceUserData::ListOfStrings(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::ListOfNumbers { value } => JsPreferenceUserData::ListOfNumbers(value.expect("at this point preference should always have value")), - DbPluginPreferenceUserData::ListOfEnums { value } => JsPreferenceUserData::ListOfStrings(value.expect("at this point preference should always have value")), + Some(user_data) => { + match user_data { + DbPluginPreferenceUserData::Number { value } => { + JsPreferenceUserData::Number( + value.expect("at this point preference should always have value"), + ) + } + DbPluginPreferenceUserData::String { value } => { + JsPreferenceUserData::String( + value.expect("at this point preference should always have value"), + ) + } + DbPluginPreferenceUserData::Enum { value } => { + JsPreferenceUserData::String( + value.expect("at this point preference should always have value"), + ) + } + DbPluginPreferenceUserData::Bool { value } => { + JsPreferenceUserData::Bool( + value.expect("at this point preference should always have value"), + ) + } + DbPluginPreferenceUserData::ListOfStrings { value } => { + JsPreferenceUserData::ListOfStrings( + value.expect("at this point preference should always have value"), + ) + } + DbPluginPreferenceUserData::ListOfNumbers { value } => { + JsPreferenceUserData::ListOfNumbers( + value.expect("at this point preference should always have value"), + ) + } + DbPluginPreferenceUserData::ListOfEnums { value } => { + JsPreferenceUserData::ListOfStrings( + value.expect("at this point preference should always have value"), + ) + } + } } }; @@ -1123,7 +1325,10 @@ fn preferences_to_js( .collect() } -fn any_preferences_missing_value(preferences: HashMap, preferences_user_data: HashMap) -> bool { +fn any_preferences_missing_value( + preferences: HashMap, + preferences_user_data: HashMap, +) -> bool { for (name, preference) in preferences { match preferences_user_data.get(&name) { None => { @@ -1138,7 +1343,7 @@ fn any_preferences_missing_value(preferences: HashMap { @@ -1153,11 +1358,11 @@ fn any_preferences_missing_value(preferences: HashMap Self { Self { db_repository, - download_status_holder: DownloadStatusHolder::new() + download_status_holder: DownloadStatusHolder::new(), } } @@ -50,21 +77,22 @@ impl PluginLoader { PluginLoader::download(temp_dir.path(), plugin_id_clone.clone())?; - let plugin_data = PluginLoader::read_plugin_dir(temp_dir.path(), plugin_id_clone.clone()) - .await?; + let plugin_data = PluginLoader::read_plugin_dir(temp_dir.path(), plugin_id_clone.clone()).await?; - data_db_repository.save_plugin(DbWritePlugin { - id: plugin_data.id, - name: plugin_data.name, - description: plugin_data.description, - enabled: false, - code: plugin_data.code, - entrypoints: plugin_data.entrypoints, - asset_data: plugin_data.asset_data, - permissions: plugin_data.permissions, - plugin_type: db_plugin_type_to_str(DbPluginType::Normal).to_owned(), - preferences: plugin_data.preferences, - }).await?; + data_db_repository + .save_plugin(DbWritePlugin { + id: plugin_data.id, + name: plugin_data.name, + description: plugin_data.description, + enabled: false, + code: plugin_data.code, + entrypoints: plugin_data.entrypoints, + asset_data: plugin_data.asset_data, + permissions: plugin_data.permissions, + plugin_type: db_plugin_type_to_str(DbPluginType::Normal).to_owned(), + preferences: plugin_data.preferences, + }) + .await?; anyhow::Ok(()) }); @@ -74,7 +102,7 @@ impl PluginLoader { Ok(()) => { tracing::info!("Finished download of plugin: {:?}", plugin_id); download_status_guard.download_finished() - }, + } Err(err) => { tracing::warn!("Download of plugin {:?} returned an error {:?}", plugin_id, err); download_status_guard.download_failed(format!("{}", err)) @@ -95,18 +123,20 @@ impl PluginLoader { .await .context(format!("Unable to read plugin: {}", &plugin_id.to_string()))?; - self.db_repository.save_plugin(DbWritePlugin { - id: plugin_data.id, - name: plugin_data.name, - description: plugin_data.description, - enabled: true, - code: plugin_data.code, - entrypoints: plugin_data.entrypoints, - asset_data: plugin_data.asset_data, - permissions: plugin_data.permissions, - plugin_type: db_plugin_type_to_str(DbPluginType::Normal).to_owned(), - preferences: plugin_data.preferences, - }).await?; + self.db_repository + .save_plugin(DbWritePlugin { + id: plugin_data.id, + name: plugin_data.name, + description: plugin_data.description, + enabled: true, + code: plugin_data.code, + entrypoints: plugin_data.entrypoints, + asset_data: plugin_data.asset_data, + permissions: plugin_data.permissions, + plugin_type: db_plugin_type_to_str(DbPluginType::Normal).to_owned(), + preferences: plugin_data.preferences, + }) + .await?; Ok(plugin_id) } @@ -121,18 +151,20 @@ impl PluginLoader { .await .context(format!("Unable to read plugin: {}", &plugin_id.to_string()))?; - self.db_repository.save_plugin(DbWritePlugin { - id: plugin_data.id, - name: plugin_data.name, - description: plugin_data.description, - enabled: true, - code: plugin_data.code, - entrypoints: plugin_data.entrypoints, - asset_data: plugin_data.asset_data, - permissions: plugin_data.permissions, - plugin_type: db_plugin_type_to_str(DbPluginType::Bundled).to_owned(), - preferences: plugin_data.preferences, - }).await?; + self.db_repository + .save_plugin(DbWritePlugin { + id: plugin_data.id, + name: plugin_data.name, + description: plugin_data.description, + enabled: true, + code: plugin_data.code, + entrypoints: plugin_data.entrypoints, + asset_data: plugin_data.asset_data, + permissions: plugin_data.permissions, + plugin_type: db_plugin_type_to_str(DbPluginType::Bundled).to_owned(), + preferences: plugin_data.preferences, + }) + .await?; Ok(plugin_id) } @@ -154,7 +186,8 @@ impl PluginLoader { let js_dir_context = js_dir.display().to_string(); let js_files = std::fs::read_dir(js_dir).context(js_dir_context)?; - let js: HashMap<_, _> = js_files.into_iter() + let js: HashMap<_, _> = js_files + .into_iter() .collect::>>() .context("Unable to get list of plugin js files")? .into_iter() @@ -162,7 +195,8 @@ impl PluginLoader { .filter(|dist_path| dist_path.extension() == Some(OsStr::new("js"))) .map(|dist_path| { let js_content = std::fs::read_to_string(&dist_path)?; - let id = dist_path.file_stem() + let id = dist_path + .file_stem() .expect("file returned from read_dir doesn't have filename?") .to_str() .ok_or(anyhow!("filename is not a valid utf-8"))? @@ -178,9 +212,11 @@ impl PluginLoader { let asset_data = WalkDir::new(&assets) .into_iter() .collect::>>() - .or_else(|err| match err.io_error() { - Some(err) if matches!(err.kind(), ErrorKind::NotFound) => Ok(vec![]), - _ => Err(err), + .or_else(|err| { + match err.io_error() { + Some(err) if matches!(err.kind(), ErrorKind::NotFound) => Ok(vec![]), + _ => Err(err), + } }) .context("Unable to get list of plugin asset data files")? .into_iter() @@ -188,8 +224,7 @@ impl PluginLoader { .map(|path| { let path = path.path(); - let data = std::fs::read(path) - .context(format!("Unable to read plugin asset file {:?}", path))?; + let data = std::fs::read(path).context(format!("Unable to read plugin asset file {:?}", path))?; let path = path .strip_prefix(&assets) @@ -198,10 +233,7 @@ impl PluginLoader { .ok_or(anyhow!("filename is not a valid utf-8"))? .to_owned(); - Ok(DbWritePluginAssetData { - path, - data, - }) + Ok(DbWritePluginAssetData { path, data }) }) .collect::>>() .context("Unable to read plugin asset data")? @@ -210,9 +242,10 @@ impl PluginLoader { let plugin_manifest_path = plugin_dir.join("gauntlet.toml"); let plugin_manifest_path_context = plugin_manifest_path.display().to_string(); - let plugin_manifest_content = std::fs::read_to_string(plugin_manifest_path).context(plugin_manifest_path_context)?; - let plugin_manifest: PluginManifest = toml::from_str(&plugin_manifest_content) - .context("Unable to read plugin manifest")?; + let plugin_manifest_content = + std::fs::read_to_string(plugin_manifest_path).context(plugin_manifest_path_context)?; + let plugin_manifest: PluginManifest = + toml::from_str(&plugin_manifest_content).context("Unable to read plugin manifest")?; tracing::debug!("Plugin config read: {:?}", plugin_manifest); @@ -221,83 +254,288 @@ impl PluginLoader { let plugin_name = plugin_manifest.gauntlet.name; let plugin_description = plugin_manifest.gauntlet.description; - let entrypoints: Vec<_> = plugin_manifest.entrypoint + let entrypoints: Vec<_> = plugin_manifest + .entrypoint .into_iter() - .map(|entrypoint| DbWritePluginEntrypoint { - id: entrypoint.id, - name: entrypoint.name, - description: entrypoint.description, - icon_path: entrypoint.icon, - entrypoint_type: db_entrypoint_to_str(match entrypoint.entrypoint_type { - PluginManifestEntrypointTypes::Command => DbPluginEntrypointType::Command, - PluginManifestEntrypointTypes::View => DbPluginEntrypointType::View, - PluginManifestEntrypointTypes::InlineView => DbPluginEntrypointType::InlineView, - PluginManifestEntrypointTypes::EntrypointGenerator => DbPluginEntrypointType::EntrypointGenerator, - }).to_owned(), - preferences: entrypoint.preferences - .into_iter() - .map(|preference| match preference { - PluginManifestPreference::Number { id, name, default, description } => (id, DbPluginPreference::Number { name: Some(name), default, description }), - PluginManifestPreference::String { id, name, default, description } => (id, DbPluginPreference::String { name: Some(name), default, description }), - PluginManifestPreference::Enum { id, name, default, description, enum_values } => { - let enum_values = enum_values.into_iter() - .map(|PluginManifestPreferenceEnumValue { label, value } | DbPreferenceEnumValue { label, value }) - .collect(); - - (id, DbPluginPreference::Enum { name: Some(name), default, description, enum_values }) - }, - PluginManifestPreference::Bool { id, name, default, description } => (id, DbPluginPreference::Bool { name: Some(name), default, description }), - PluginManifestPreference::ListOfStrings { id, name, description } => (id, DbPluginPreference::ListOfStrings { name: Some(name), default: None, description }), - PluginManifestPreference::ListOfNumbers { id, name, description } => (id, DbPluginPreference::ListOfNumbers { name: Some(name), default: None, description }), - PluginManifestPreference::ListOfEnums { id, name, description, enum_values } => { - let enum_values = enum_values.into_iter() - .map(|PluginManifestPreferenceEnumValue { label, value } | DbPreferenceEnumValue { label, value }) - .collect(); - - (id, DbPluginPreference::ListOfEnums { name: Some(name), default: None, description, enum_values }) - }, + .map(|entrypoint| { + DbWritePluginEntrypoint { + id: entrypoint.id, + name: entrypoint.name, + description: entrypoint.description, + icon_path: entrypoint.icon, + entrypoint_type: db_entrypoint_to_str(match entrypoint.entrypoint_type { + PluginManifestEntrypointTypes::Command => DbPluginEntrypointType::Command, + PluginManifestEntrypointTypes::View => DbPluginEntrypointType::View, + PluginManifestEntrypointTypes::InlineView => DbPluginEntrypointType::InlineView, + PluginManifestEntrypointTypes::EntrypointGenerator => { + DbPluginEntrypointType::EntrypointGenerator + } }) - .collect(), - actions: entrypoint.actions.into_iter() - .map(|action| DbPluginAction { - id: action.id, - description: action.description, - key: action.shortcut.key.to_model().to_value(), - kind: match action.shortcut.kind { - PluginManifestActionShortcutKind::Main => DbPluginActionShortcutKind::Main, - PluginManifestActionShortcutKind::Alternative => DbPluginActionShortcutKind::Alternative, - }, - }) - .collect(), + .to_owned(), + preferences: entrypoint + .preferences + .into_iter() + .map(|preference| { + match preference { + PluginManifestPreference::Number { + id, + name, + default, + description, + } => { + ( + id, + DbPluginPreference::Number { + name: Some(name), + default, + description, + }, + ) + } + PluginManifestPreference::String { + id, + name, + default, + description, + } => { + ( + id, + DbPluginPreference::String { + name: Some(name), + default, + description, + }, + ) + } + PluginManifestPreference::Enum { + id, + name, + default, + description, + enum_values, + } => { + let enum_values = enum_values + .into_iter() + .map(|PluginManifestPreferenceEnumValue { label, value }| { + DbPreferenceEnumValue { label, value } + }) + .collect(); + + ( + id, + DbPluginPreference::Enum { + name: Some(name), + default, + description, + enum_values, + }, + ) + } + PluginManifestPreference::Bool { + id, + name, + default, + description, + } => { + ( + id, + DbPluginPreference::Bool { + name: Some(name), + default, + description, + }, + ) + } + PluginManifestPreference::ListOfStrings { id, name, description } => { + ( + id, + DbPluginPreference::ListOfStrings { + name: Some(name), + default: None, + description, + }, + ) + } + PluginManifestPreference::ListOfNumbers { id, name, description } => { + ( + id, + DbPluginPreference::ListOfNumbers { + name: Some(name), + default: None, + description, + }, + ) + } + PluginManifestPreference::ListOfEnums { + id, + name, + description, + enum_values, + } => { + let enum_values = enum_values + .into_iter() + .map(|PluginManifestPreferenceEnumValue { label, value }| { + DbPreferenceEnumValue { label, value } + }) + .collect(); + + ( + id, + DbPluginPreference::ListOfEnums { + name: Some(name), + default: None, + description, + enum_values, + }, + ) + } + } + }) + .collect(), + actions: entrypoint + .actions + .into_iter() + .map(|action| { + DbPluginAction { + id: action.id, + description: action.description, + key: action.shortcut.key.to_model().to_value(), + kind: match action.shortcut.kind { + PluginManifestActionShortcutKind::Main => DbPluginActionShortcutKind::Main, + PluginManifestActionShortcutKind::Alternative => { + DbPluginActionShortcutKind::Alternative + } + }, + } + }) + .collect(), + } }) .collect(); - let plugin_preferences = plugin_manifest.preferences + let plugin_preferences = plugin_manifest + .preferences .into_iter() - .map(|preference| match preference { - PluginManifestPreference::Number { id, name, default, description } => (id, DbPluginPreference::Number { name: Some(name), default, description }), - PluginManifestPreference::String { id, name, default, description } => (id, DbPluginPreference::String { name: Some(name), default, description }), - PluginManifestPreference::Enum { id, name, default, description, enum_values } => { - let enum_values = enum_values.into_iter() - .map(|PluginManifestPreferenceEnumValue { label, value } | DbPreferenceEnumValue { label, value }) - .collect(); + .map(|preference| { + match preference { + PluginManifestPreference::Number { + id, + name, + default, + description, + } => { + ( + id, + DbPluginPreference::Number { + name: Some(name), + default, + description, + }, + ) + } + PluginManifestPreference::String { + id, + name, + default, + description, + } => { + ( + id, + DbPluginPreference::String { + name: Some(name), + default, + description, + }, + ) + } + PluginManifestPreference::Enum { + id, + name, + default, + description, + enum_values, + } => { + let enum_values = enum_values + .into_iter() + .map(|PluginManifestPreferenceEnumValue { label, value }| { + DbPreferenceEnumValue { label, value } + }) + .collect(); - (id, DbPluginPreference::Enum { name: Some(name), default, description, enum_values }) - }, - PluginManifestPreference::Bool { id, name, default, description } => (id, DbPluginPreference::Bool { name: Some(name), default, description }), - PluginManifestPreference::ListOfStrings { id, name, description } => (id, DbPluginPreference::ListOfStrings { name: Some(name), default: None, description }), - PluginManifestPreference::ListOfNumbers { id, name, description } => (id, DbPluginPreference::ListOfNumbers { name: Some(name), default: None, description }), - PluginManifestPreference::ListOfEnums { id, name, description, enum_values } => { - let enum_values = enum_values.into_iter() - .map(|PluginManifestPreferenceEnumValue { label, value } | DbPreferenceEnumValue { label, value }) - .collect(); + ( + id, + DbPluginPreference::Enum { + name: Some(name), + default, + description, + enum_values, + }, + ) + } + PluginManifestPreference::Bool { + id, + name, + default, + description, + } => { + ( + id, + DbPluginPreference::Bool { + name: Some(name), + default, + description, + }, + ) + } + PluginManifestPreference::ListOfStrings { id, name, description } => { + ( + id, + DbPluginPreference::ListOfStrings { + name: Some(name), + default: None, + description, + }, + ) + } + PluginManifestPreference::ListOfNumbers { id, name, description } => { + ( + id, + DbPluginPreference::ListOfNumbers { + name: Some(name), + default: None, + description, + }, + ) + } + PluginManifestPreference::ListOfEnums { + id, + name, + description, + enum_values, + } => { + let enum_values = enum_values + .into_iter() + .map(|PluginManifestPreferenceEnumValue { label, value }| { + DbPreferenceEnumValue { label, value } + }) + .collect(); - (id, DbPluginPreference::ListOfEnums { name: Some(name), default: None, description, enum_values }) - }, + ( + id, + DbPluginPreference::ListOfEnums { + name: Some(name), + default: None, + description, + enum_values, + }, + ) + } + } }) .collect(); - let clipboard = plugin_manifest.permissions + let clipboard = plugin_manifest + .permissions .clipboard .into_iter() .map(|permission| { @@ -309,7 +547,8 @@ impl PluginLoader { }) .collect(); - let main_search_bar = plugin_manifest.permissions + let main_search_bar = plugin_manifest + .permissions .main_search_bar .into_iter() .map(|permission| { @@ -339,14 +578,12 @@ impl PluginLoader { id: plugin_id.to_string(), name: plugin_name, description: plugin_description, - code: DbCode { - js - }, + code: DbCode { js }, entrypoints, asset_data, permissions, preferences: plugin_preferences, - preferences_user_data: HashMap::new() + preferences_user_data: HashMap::new(), }) } @@ -354,18 +591,39 @@ impl PluginLoader { let supported_systems = &plugin_manifest.supported_system; let supported_systems_str = supported_systems.iter().format(", "); - let supports_linux = &supported_systems.iter().any(|system| matches!(system, PluginManifestSupportedSystem::Linux)); - let supports_macos = &supported_systems.iter().any(|system| matches!(system, PluginManifestSupportedSystem::MacOS)); - let supports_windows = &supported_systems.iter().any(|system| matches!(system, PluginManifestSupportedSystem::Windows)); + let supports_linux = &supported_systems + .iter() + .any(|system| matches!(system, PluginManifestSupportedSystem::Linux)); + let supports_macos = &supported_systems + .iter() + .any(|system| matches!(system, PluginManifestSupportedSystem::MacOS)); + let supports_windows = &supported_systems + .iter() + .any(|system| matches!(system, PluginManifestSupportedSystem::Windows)); let permissions = &plugin_manifest.permissions; Self::validate_string_permissions(&permissions.environment)?; Self::validate_network_permissions(&permissions.network)?; - Self::validate_path_permissions(&permissions.filesystem.read, supports_linux, supports_macos, supports_windows)?; - Self::validate_path_permissions(&permissions.filesystem.write, supports_linux, supports_macos, supports_windows)?; + Self::validate_path_permissions( + &permissions.filesystem.read, + supports_linux, + supports_macos, + supports_windows, + )?; + Self::validate_path_permissions( + &permissions.filesystem.write, + supports_linux, + supports_macos, + supports_windows, + )?; Self::validate_command_permissions(&permissions.exec.command)?; - Self::validate_path_permissions(&permissions.exec.executable, supports_linux, supports_macos, supports_windows)?; + Self::validate_path_permissions( + &permissions.exec.executable, + supports_linux, + supports_macos, + supports_windows, + )?; // even though system accepts a list of predefined values // unknown values are ignored to allow for easier @@ -380,7 +638,8 @@ impl PluginLoader { let executable_exists = !permissions.exec.executable.is_empty(); let system_exists = !permissions.system.is_empty(); - let os_required = env_exists || fs_read_exists || fs_write_exists || command_exists || executable_exists || system_exists; + let os_required = + env_exists || fs_read_exists || fs_write_exists || command_exists || executable_exists || system_exists; if os_required { let current_system = if cfg!(target_os = "linux") { @@ -394,11 +653,15 @@ impl PluginLoader { }; if !supported_systems.contains(¤t_system) { - return Err(anyhow!("Plugin doesn't support current operating system. Operating systems supported by plugin: [{}]", supported_systems_str)) + return Err(anyhow!( + "Plugin doesn't support current operating system. Operating systems supported by plugin: [{}]", + supported_systems_str + )); } } - let has_inline_view = plugin_manifest.entrypoint + let has_inline_view = plugin_manifest + .entrypoint .iter() .find(|entrypoint| matches!(entrypoint.entrypoint_type, PluginManifestEntrypointTypes::InlineView)) .is_some(); @@ -406,14 +669,21 @@ impl PluginLoader { if has_inline_view { let main_search_bar = &permissions.main_search_bar; if !main_search_bar.contains(&PluginManifestMainSearchBarPermissions::Read) { - return Err(anyhow!("Plugin uses entrypoint type 'inline-view' but doesn't specify main search bar 'read' permission")) + return Err(anyhow!( + "Plugin uses entrypoint type 'inline-view' but doesn't specify main search bar 'read' permission" + )); } } Ok(()) } - fn validate_path_permissions(paths: &[String], supports_linux: &bool, supports_macos: &bool, supports_windows: &bool) -> anyhow::Result<()> { + fn validate_path_permissions( + paths: &[String], + supports_linux: &bool, + supports_macos: &bool, + supports_windows: &bool, + ) -> anyhow::Result<()> { for path in paths { if path.is_empty() { Err(anyhow!("Empty path is not allowed in permissions"))? @@ -429,11 +699,16 @@ impl PluginLoader { let pattern_match = variable.get(0).unwrap(); if pattern_match.start() != 0 { - Err(anyhow!("Variable can only be used in the beginning of the path: {}", path))? + Err(anyhow!( + "Variable can only be used in the beginning of the path: {}", + path + ))? } let mut path_bytes = path.bytes(); - path_bytes.nth(pattern_match.end() - 1).expect("end of match should always exist"); + path_bytes + .nth(pattern_match.end() - 1) + .expect("end of match should always exist"); let windows_like_path = match path_bytes.next() { Some(b'\\') => true, @@ -454,7 +729,11 @@ impl PluginLoader { ("common", "plugin-data") => windows_like_path, ("common", "plugin-cache") => windows_like_path, (namespace, name) => { - Err(anyhow!("Unknown variable namespace and name combination in path in permissions: {}:{}", namespace, name))? + Err(anyhow!( + "Unknown variable namespace and name combination in path in permissions: {}:{}", + namespace, + name + ))? } }; @@ -464,9 +743,7 @@ impl PluginLoader { PERMISSIONS_VARIABLE_PATTERN.replace(path, "/dummy-root").to_string() } } - [_, ..] => { - Err(anyhow!("Path includes more than one variable: {}", path))? - } + [_, ..] => Err(anyhow!("Path includes more than one variable: {}", path))?, }; let path = Utf8TypedPath::derive(&augmented_path); @@ -489,10 +766,16 @@ impl PluginLoader { match component { Utf8UnixComponent::Normal(_) | Utf8UnixComponent::RootDir => {} Utf8UnixComponent::CurDir => { - Err(anyhow!("Current directory '.' segment is not allowed in permission path: {}", path))? + Err(anyhow!( + "Current directory '.' segment is not allowed in permission path: {}", + path + ))? } Utf8UnixComponent::ParentDir => { - Err(anyhow!("Parent directory '..' segment is not allowed in permission path: {}", path))? + Err(anyhow!( + "Parent directory '..' segment is not allowed in permission path: {}", + path + ))? } } } @@ -508,24 +791,36 @@ impl PluginLoader { let components = path.components(); - let prefix = components.prefix() + let prefix = components + .prefix() .expect("prefix should always be present for absolute paths"); match prefix.kind() { Utf8WindowsPrefix::Disk('C') => {} _ => { - Err(anyhow!("Only C:/ drive prefix in windows paths is supported, prefix: {}", prefix.as_str()))? + Err(anyhow!( + "Only C:/ drive prefix in windows paths is supported, prefix: {}", + prefix.as_str() + ))? } } for component in components { match component { - Utf8WindowsComponent::Normal(_) | Utf8WindowsComponent::RootDir | Utf8WindowsComponent::Prefix(_) => {} + Utf8WindowsComponent::Normal(_) + | Utf8WindowsComponent::RootDir + | Utf8WindowsComponent::Prefix(_) => {} Utf8WindowsComponent::CurDir => { - Err(anyhow!("Current directory '.' segment is not allowed in permission path: {}", path))? + Err(anyhow!( + "Current directory '.' segment is not allowed in permission path: {}", + path + ))? } Utf8WindowsComponent::ParentDir => { - Err(anyhow!("Parent directory '..' segment is not allowed in permission path: {}", path))? + Err(anyhow!( + "Parent directory '..' segment is not allowed in permission path: {}", + path + ))? } } } @@ -574,7 +869,10 @@ impl PluginLoader { // allow only domain and optional port if contains_username || contains_password || contains_path || contains_query || contains_fragment { - Err(anyhow!("Network permission can only contain domain and optionally port: {}", value))? + Err(anyhow!( + "Network permission can only contain domain and optionally port: {}", + value + ))? } } Ok(()) @@ -674,7 +972,7 @@ enum PluginManifestPreference { // default: Option>, enum_values: Vec, description: String, - } + }, } #[derive(Debug, Deserialize, Serialize)] @@ -699,7 +997,7 @@ pub enum PluginManifestEntrypointTypes { pub struct PluginManifestAction { id: String, description: String, - shortcut: PluginManifestActionShortcut + shortcut: PluginManifestActionShortcut, } #[derive(Debug, Deserialize)] @@ -1077,7 +1375,7 @@ pub enum PluginManifestClipboardPermissions { #[serde(rename = "write")] Write, #[serde(rename = "clear")] - Clear + Clear, } #[derive(Debug, Deserialize, Eq, PartialEq)] @@ -1085,4 +1383,3 @@ pub enum PluginManifestMainSearchBarPermissions { #[serde(rename = "read")] Read, } - diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 3264292..03f2d56 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -3,44 +3,87 @@ use std::collections::HashMap; use std::sync::Mutex; use std::thread; use std::time::Duration; + use anyhow::anyhow; -use include_dir::{include_dir, Dir}; +use gauntlet_common::dirs::Dirs; +use gauntlet_common::model::DownloadStatus; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::KeyboardEventOrigin; +use gauntlet_common::model::LocalSaveData; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::PluginPreference; +use gauntlet_common::model::PluginPreferenceUserData; +use gauntlet_common::model::PreferenceEnumValue; +use gauntlet_common::model::SearchResult; +use gauntlet_common::model::SettingsEntrypoint; +use gauntlet_common::model::SettingsEntrypointType; +use gauntlet_common::model::SettingsPlugin; +use gauntlet_common::model::SettingsTheme; +use gauntlet_common::model::UiPropertyValue; +use gauntlet_common::model::UiRequestData; +use gauntlet_common::model::UiResponseData; +use gauntlet_common::model::UiSetupData; +use gauntlet_common::model::UiWidgetId; +use gauntlet_common::model::WindowPositionMode; +use gauntlet_common::rpc::frontend_api::FrontendApi; +use gauntlet_common::settings_env_data_to_string; +use gauntlet_common::SettingsEnvData; +use gauntlet_common::SETTINGS_ENV; +use gauntlet_plugin_runtime::JsPluginCode; +use gauntlet_plugin_runtime::JsPluginPermissions; +use gauntlet_plugin_runtime::JsPluginPermissionsExec; +use gauntlet_plugin_runtime::JsPluginPermissionsFileSystem; +use gauntlet_plugin_runtime::JsPluginPermissionsMainSearchBar; +use gauntlet_utils::channel::RequestSender; +use include_dir::include_dir; +use include_dir::Dir; use tokio::runtime::Handle; -use gauntlet_common::model::{DownloadStatus, EntrypointId, KeyboardEventOrigin, LocalSaveData, PhysicalKey, PhysicalShortcut, PluginId, PluginPreference, PluginPreferenceUserData, PreferenceEnumValue, SearchResult, SettingsEntrypoint, SettingsEntrypointType, SettingsPlugin, SettingsTheme, UiPropertyValue, UiRequestData, UiResponseData, UiSetupData, UiWidgetId, WindowPositionMode}; -use gauntlet_common::rpc::frontend_api::FrontendApi; -use gauntlet_common::{settings_env_data_to_string, SettingsEnvData, SETTINGS_ENV}; -use gauntlet_utils::channel::RequestSender; -use gauntlet_common::dirs::Dirs; -use gauntlet_plugin_runtime::{JsPluginCode, JsPluginPermissions, JsPluginPermissionsExec, JsPluginPermissionsFileSystem, JsPluginPermissionsMainSearchBar}; -use crate::model::{ActionShortcutKey}; +use crate::model::ActionShortcutKey; use crate::plugins::clipboard::Clipboard; use crate::plugins::config_reader::ConfigReader; -use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginActionShortcutKind, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginMainSearchBarPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint}; +use crate::plugins::data_db_repository::db_entrypoint_from_str; +use crate::plugins::data_db_repository::DataDbRepository; +use crate::plugins::data_db_repository::DbPluginActionShortcutKind; +use crate::plugins::data_db_repository::DbPluginClipboardPermissions; +use crate::plugins::data_db_repository::DbPluginEntrypointType; +use crate::plugins::data_db_repository::DbPluginMainSearchBarPermissions; +use crate::plugins::data_db_repository::DbPluginPreference; +use crate::plugins::data_db_repository::DbPluginPreferenceUserData; +use crate::plugins::data_db_repository::DbReadPluginEntrypoint; use crate::plugins::icon_cache::IconCache; -use crate::plugins::js::{start_plugin_runtime, AllPluginCommandData, OnePluginCommandData, PluginCommand, PluginPermissions, PluginPermissionsClipboard, PluginRuntimeData}; +use crate::plugins::js::start_plugin_runtime; +use crate::plugins::js::AllPluginCommandData; +use crate::plugins::js::OnePluginCommandData; +use crate::plugins::js::PluginCommand; +use crate::plugins::js::PluginPermissions; +use crate::plugins::js::PluginPermissionsClipboard; +use crate::plugins::js::PluginRuntimeData; use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; use crate::plugins::settings::Settings; use crate::search::SearchIndex; -pub mod js; -mod data_db_repository; +mod clipboard; mod config_reader; +mod data_db_repository; +mod download_status; +pub(super) mod frecency; +mod icon_cache; +mod image_gatherer; +pub mod js; mod loader; mod run_status; -mod download_status; -mod icon_cache; -pub(super) mod frecency; -mod clipboard; mod runtime; -mod image_gatherer; mod settings; mod theme; -static BUNDLED_PLUGINS: [(&str, Dir); 1] = [ - ("gauntlet", include_dir!("$CARGO_MANIFEST_DIR/../../bundled_plugins/gauntlet/dist")), -]; +static BUNDLED_PLUGINS: [(&str, Dir); 1] = [( + "gauntlet", + include_dir!("$CARGO_MANIFEST_DIR/../../bundled_plugins/gauntlet/dist"), +)]; pub struct ApplicationManager { config_reader: ConfigReader, @@ -82,7 +125,7 @@ impl ApplicationManager { frontend_api, clipboard, settings, - dirs + dirs, }) } @@ -98,7 +141,7 @@ impl ApplicationManager { theme, global_shortcut, close_on_unfocus, - window_position_mode + window_position_mode, }) } @@ -138,24 +181,26 @@ impl ApplicationManager { Ok(()) } - pub async fn save_local_plugin( - &self, - path: &str, - ) -> anyhow::Result { + pub async fn save_local_plugin(&self, path: &str) -> anyhow::Result { tracing::info!(target = "plugin", "Saving local plugin at path: {:?}", path); let plugin_id = self.plugin_downloader.save_local_plugin(path).await?; - let plugin = self.db_repository.get_plugin_by_id(&plugin_id.to_string()) - .await?; + let plugin = self.db_repository.get_plugin_by_id(&plugin_id.to_string()).await?; self.reload_plugin(plugin_id.clone()).await?; let (stdout_file_path, stderr_file_path) = self.dirs.plugin_log_files(&plugin.uuid); Ok(LocalSaveData { - stdout_file_path: stdout_file_path.into_os_string().into_string().map_err(|_| anyhow!("non uft8 paths are not supported"))?, - stderr_file_path: stderr_file_path.into_os_string().into_string().map_err(|_| anyhow!("non uft8 paths are not supported"))?, + stdout_file_path: stdout_file_path + .into_os_string() + .into_string() + .map_err(|_| anyhow!("non uft8 paths are not supported"))?, + stderr_file_path: stderr_file_path + .into_os_string() + .into_string() + .map_err(|_| anyhow!("non uft8 paths are not supported"))?, }) } @@ -172,7 +217,8 @@ impl ApplicationManager { } pub async fn plugins(&self) -> anyhow::Result> { - let result = self.db_repository + let result = self + .db_repository .list_plugins_and_entrypoints() .await? .into_iter() @@ -191,15 +237,22 @@ impl ApplicationManager { DbPluginEntrypointType::Command => SettingsEntrypointType::Command, DbPluginEntrypointType::View => SettingsEntrypointType::View, DbPluginEntrypointType::InlineView => SettingsEntrypointType::InlineView, - DbPluginEntrypointType::EntrypointGenerator => SettingsEntrypointType::EntrypointGenerator, - }.into(), - preferences: entrypoint.preferences.into_iter() + DbPluginEntrypointType::EntrypointGenerator => { + SettingsEntrypointType::EntrypointGenerator + } + } + .into(), + preferences: entrypoint + .preferences + .into_iter() .map(|(key, value)| { let preference = plugin_preference_from_db(&key, value); (key, preference) }) .collect(), - preferences_user_data: entrypoint.preferences_user_data.into_iter() + preferences_user_data: entrypoint + .preferences_user_data + .into_iter() .map(|(key, value)| (key, plugin_preference_user_data_from_db(value))) .collect(), }; @@ -214,13 +267,17 @@ impl ApplicationManager { plugin_description: plugin.description, enabled: plugin.enabled, entrypoints, - preferences: plugin.preferences.into_iter() + preferences: plugin + .preferences + .into_iter() .map(|(key, value)| { let preference = plugin_preference_from_db(&key, value); (key, preference) }) .collect(), - preferences_user_data: plugin.preferences_user_data.into_iter() + preferences_user_data: plugin + .preferences_user_data + .into_iter() .map(|(key, value)| (key, plugin_preference_user_data_from_db(value))) .collect(), } @@ -234,11 +291,19 @@ impl ApplicationManager { let currently_running = self.run_status_holder.is_plugin_running(&plugin_id); let currently_enabled = self.is_plugin_enabled(&plugin_id).await?; - tracing::info!(target = "plugin", "Setting plugin state for plugin id: {:?}, currently_running: {}, currently_enabled: {}, set_enabled: {}", plugin_id, currently_running, currently_enabled, set_enabled); + tracing::info!( + target = "plugin", + "Setting plugin state for plugin id: {:?}, currently_running: {}, currently_enabled: {}, set_enabled: {}", + plugin_id, + currently_running, + currently_enabled, + set_enabled + ); match (currently_running, currently_enabled, set_enabled) { (false, false, true) => { - self.db_repository.set_plugin_enabled(&plugin_id.to_string(), true) + self.db_repository + .set_plugin_enabled(&plugin_id.to_string(), true) .await?; self.start_plugin(plugin_id).await?; @@ -247,14 +312,18 @@ impl ApplicationManager { self.start_plugin(plugin_id).await?; } (true, true, false) => { - self.db_repository.set_plugin_enabled(&plugin_id.to_string(), false) + self.db_repository + .set_plugin_enabled(&plugin_id.to_string(), false) .await?; self.stop_plugin(plugin_id.clone()).await; self.search_index.remove_for_plugin(plugin_id)?; } (true, false, _) => { - tracing::error!("Plugin is running but is disabled, please report this: {}", plugin_id.to_string()) + tracing::error!( + "Plugin is running but is disabled, please report this: {}", + plugin_id.to_string() + ) } _ => {} } @@ -262,13 +331,24 @@ impl ApplicationManager { Ok(()) } - pub async fn set_entrypoint_state(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, enabled: bool) -> anyhow::Result<()> { - tracing::debug!(target = "plugin", "Setting entrypoint state for plugin id: {:?}, entrypoint_id: {:?}, enabled: {}", plugin_id, entrypoint_id, enabled); + pub async fn set_entrypoint_state( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + enabled: bool, + ) -> anyhow::Result<()> { + tracing::debug!( + target = "plugin", + "Setting entrypoint state for plugin id: {:?}, entrypoint_id: {:?}, enabled: {}", + plugin_id, + entrypoint_id, + enabled + ); - self.db_repository.set_plugin_entrypoint_enabled(&plugin_id.to_string(), &entrypoint_id.to_string(), enabled) + self.db_repository + .set_plugin_entrypoint_enabled(&plugin_id.to_string(), &entrypoint_id.to_string(), enabled) .await?; - self.reload_plugin(plugin_id.clone()).await?; Ok(()) @@ -298,12 +378,30 @@ impl ApplicationManager { self.settings.window_position_mode_setting().await } - pub async fn set_preference_value(&self, plugin_id: PluginId, entrypoint_id: Option, preference_id: String, preference_value: PluginPreferenceUserData) -> anyhow::Result<()> { - tracing::debug!(target = "plugin", "Setting preference value for plugin id: {:?}, entrypoint_id: {:?}, preference_id: {}", plugin_id, entrypoint_id, preference_id); + pub async fn set_preference_value( + &self, + plugin_id: PluginId, + entrypoint_id: Option, + preference_id: String, + preference_value: PluginPreferenceUserData, + ) -> anyhow::Result<()> { + tracing::debug!( + target = "plugin", + "Setting preference value for plugin id: {:?}, entrypoint_id: {:?}, preference_id: {}", + plugin_id, + entrypoint_id, + preference_id + ); let user_data = plugin_preference_user_data_to_db(preference_value); - self.db_repository.set_preference_value(plugin_id.to_string(), entrypoint_id.map(|id| id.to_string()), preference_id, user_data) + self.db_repository + .set_preference_value( + plugin_id.to_string(), + entrypoint_id.map(|id| id.to_string()), + preference_id, + user_data, + ) .await?; self.reload_plugin(plugin_id.clone()).await?; @@ -354,9 +452,7 @@ impl ApplicationManager { pub fn handle_inline_view(&self, text: &str) { self.send_command(PluginCommand::All { - data: AllPluginCommandData::OpenInlineView { - text: text.to_owned() - } + data: AllPluginCommandData::OpenInlineView { text: text.to_owned() }, }) } @@ -365,33 +461,43 @@ impl ApplicationManager { id: plugin_id.clone(), data: OnePluginCommandData::RunCommand { entrypoint_id: entrypoint_id.to_string(), - } + }, }); self.mark_entrypoint_frecency(plugin_id, entrypoint_id).await } - pub async fn handle_run_generated_entrypoint(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_index: usize) { + pub async fn handle_run_generated_entrypoint( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + action_index: usize, + ) { self.send_command(PluginCommand::One { id: plugin_id.clone(), data: OnePluginCommandData::RunGeneratedEntrypoint { entrypoint_id: entrypoint_id.to_string(), action_index, - } + }, }); self.mark_entrypoint_frecency(plugin_id, entrypoint_id).await } - pub async fn handle_render_view(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> anyhow::Result> { + pub async fn handle_render_view( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + ) -> anyhow::Result> { self.send_command(PluginCommand::One { id: plugin_id.clone(), data: OnePluginCommandData::RenderView { entrypoint_id: entrypoint_id.clone(), - } + }, }); - self.mark_entrypoint_frecency(plugin_id.clone(), entrypoint_id.clone()).await; + self.mark_entrypoint_frecency(plugin_id.clone(), entrypoint_id.clone()) + .await; let shortcuts = self.action_shortcuts(plugin_id, entrypoint_id).await?; @@ -401,22 +507,38 @@ impl ApplicationManager { pub fn handle_view_close(&self, plugin_id: PluginId) { self.send_command(PluginCommand::One { id: plugin_id, - data: OnePluginCommandData::CloseView + data: OnePluginCommandData::CloseView, }) } - pub fn handle_view_event(&self, plugin_id: PluginId, widget_id: UiWidgetId, event_name: String, event_arguments: Vec) { + pub fn handle_view_event( + &self, + plugin_id: PluginId, + widget_id: UiWidgetId, + event_name: String, + event_arguments: Vec, + ) { self.send_command(PluginCommand::One { id: plugin_id, data: OnePluginCommandData::HandleViewEvent { widget_id, event_name, - event_arguments - } + event_arguments, + }, }) } - pub fn handle_keyboard_event(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, origin: KeyboardEventOrigin, key: PhysicalKey, modifier_shift: bool, modifier_control: bool, modifier_alt: bool, modifier_meta: bool) { + pub fn handle_keyboard_event( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + origin: KeyboardEventOrigin, + key: PhysicalKey, + modifier_shift: bool, + modifier_control: bool, + modifier_alt: bool, + modifier_meta: bool, + ) { self.send_command(PluginCommand::One { id: plugin_id, data: OnePluginCommandData::HandleKeyboardEvent { @@ -427,14 +549,14 @@ impl ApplicationManager { modifier_control, modifier_alt, modifier_meta, - } + }, }) } pub fn request_search_index_refresh(&self, plugin_id: PluginId) { self.send_command(PluginCommand::One { id: plugin_id, - data: OnePluginCommandData::RefreshSearchIndex + data: OnePluginCommandData::RefreshSearchIndex, }) } @@ -446,8 +568,7 @@ impl ApplicationManager { } pub fn handle_open_settings_window(&self) { - let current_exe = std::env::current_exe() - .expect("unable to get current_exe"); + let current_exe = std::env::current_exe().expect("unable to get current_exe"); std::process::Command::new(current_exe) .args(["settings"]) @@ -463,12 +584,11 @@ impl ApplicationManager { } } else { SettingsEnvData::OpenPluginPreferences { - plugin_id: plugin_id.to_string() + plugin_id: plugin_id.to_string(), } }; - let current_exe = std::env::current_exe() - .expect("unable to get current_exe"); + let current_exe = std::env::current_exe().expect("unable to get current_exe"); std::process::Command::new(current_exe) .args(["settings"]) @@ -493,12 +613,17 @@ impl ApplicationManager { } async fn is_plugin_enabled(&self, plugin_id: &PluginId) -> anyhow::Result { - self.db_repository.is_plugin_enabled(&plugin_id.to_string()) - .await + self.db_repository.is_plugin_enabled(&plugin_id.to_string()).await } - async fn action_shortcuts(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) -> anyhow::Result> { - self.db_repository.action_shortcuts(&plugin_id.to_string(), &entrypoint_id.to_string()).await + async fn action_shortcuts( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + ) -> anyhow::Result> { + self.db_repository + .action_shortcuts(&plugin_id.to_string(), &entrypoint_id.to_string()) + .await } async fn start_plugin(&self, plugin_id: PluginId) -> anyhow::Result<()> { @@ -506,35 +631,44 @@ impl ApplicationManager { let plugin_id_str = plugin_id.to_string(); - let plugin = self.db_repository.get_plugin_by_id(&plugin_id_str) - .await?; + let plugin = self.db_repository.get_plugin_by_id(&plugin_id_str).await?; - let entrypoint_names = self.db_repository.get_entrypoints_by_plugin_id(&plugin_id_str) + let entrypoint_names = self + .db_repository + .get_entrypoints_by_plugin_id(&plugin_id_str) .await? .into_iter() .map(|entrypoint| (EntrypointId::from_string(entrypoint.id), entrypoint.name)) .collect::>(); - let inline_view_entrypoint_id = self.db_repository.get_inline_view_entrypoint_id_for_plugin(&plugin_id_str) + let inline_view_entrypoint_id = self + .db_repository + .get_inline_view_entrypoint_id_for_plugin(&plugin_id_str) .await?; let receiver = self.command_broadcaster.subscribe(); - let clipboard_permissions = plugin.permissions + let clipboard_permissions = plugin + .permissions .clipboard .into_iter() - .map(|permission| match permission { - DbPluginClipboardPermissions::Read => PluginPermissionsClipboard::Read, - DbPluginClipboardPermissions::Write => PluginPermissionsClipboard::Write, - DbPluginClipboardPermissions::Clear => PluginPermissionsClipboard::Clear, + .map(|permission| { + match permission { + DbPluginClipboardPermissions::Read => PluginPermissionsClipboard::Read, + DbPluginClipboardPermissions::Write => PluginPermissionsClipboard::Write, + DbPluginClipboardPermissions::Clear => PluginPermissionsClipboard::Clear, + } }) .collect(); - let main_search_bar_permissions = plugin.permissions + let main_search_bar_permissions = plugin + .permissions .main_search_bar .into_iter() - .map(|permission| match permission { - DbPluginMainSearchBarPermissions::Read => JsPluginPermissionsMainSearchBar::Read, + .map(|permission| { + match permission { + DbPluginMainSearchBarPermissions::Read => JsPluginPermissionsMainSearchBar::Read, + } }) .collect(); @@ -558,7 +692,7 @@ impl ApplicationManager { }, system: plugin.permissions.system, clipboard: clipboard_permissions, - main_search_bar: main_search_bar_permissions + main_search_bar: main_search_bar_permissions, }, command_receiver: receiver, db_repository: self.db_repository.clone(), @@ -596,18 +730,26 @@ impl ApplicationManager { } async fn mark_entrypoint_frecency(&self, plugin_id: PluginId, entrypoint_id: EntrypointId) { - let result = self.db_repository.mark_entrypoint_frecency(&plugin_id.to_string(), &entrypoint_id.to_string()) + let result = self + .db_repository + .mark_entrypoint_frecency(&plugin_id.to_string(), &entrypoint_id.to_string()) .await; if let Err(err) = &result { - tracing::warn!(target = "rpc", "error occurred when marking entrypoint frecency {:?}", err) + tracing::warn!( + target = "rpc", + "error occurred when marking entrypoint frecency {:?}", + err + ) } self.request_search_index_refresh(plugin_id); } pub async fn inline_view_shortcuts(&self) -> anyhow::Result>> { - let result: HashMap<_, _> = self.db_repository.inline_view_shortcuts() + let result: HashMap<_, _> = self + .db_repository + .inline_view_shortcuts() .await? .into_iter() .map(|(plugin_id, shortcuts)| (PluginId::from_string(plugin_id), shortcuts)) @@ -619,65 +761,107 @@ impl ApplicationManager { fn plugin_preference_from_db(id: &str, value: DbPluginPreference) -> PluginPreference { match value { - DbPluginPreference::Number { name, default, description } => { + DbPluginPreference::Number { + name, + default, + description, + } => { PluginPreference::Number { name: name.unwrap_or_else(|| id.to_string()), default, - description + description, } - }, - DbPluginPreference::String { name, default, description } => { + } + DbPluginPreference::String { + name, + default, + description, + } => { PluginPreference::String { name: name.unwrap_or_else(|| id.to_string()), default, - description + description, } - }, - DbPluginPreference::Enum { name, default, description, enum_values } => { - let enum_values = enum_values.into_iter() - .map(|value| PreferenceEnumValue { label: value.label, value: value.value }) + } + DbPluginPreference::Enum { + name, + default, + description, + enum_values, + } => { + let enum_values = enum_values + .into_iter() + .map(|value| { + PreferenceEnumValue { + label: value.label, + value: value.value, + } + }) .collect(); PluginPreference::Enum { name: name.unwrap_or_else(|| id.to_string()), default, description, - enum_values + enum_values, } - }, - DbPluginPreference::Bool { name, default, description } => { + } + DbPluginPreference::Bool { + name, + default, + description, + } => { PluginPreference::Bool { name: name.unwrap_or_else(|| id.to_string()), default, - description + description, } - }, - DbPluginPreference::ListOfStrings { name, default, description } => { + } + DbPluginPreference::ListOfStrings { + name, + default, + description, + } => { PluginPreference::ListOfStrings { name: name.unwrap_or_else(|| id.to_string()), default, - description + description, } - }, - DbPluginPreference::ListOfNumbers { name, default, description } => { + } + DbPluginPreference::ListOfNumbers { + name, + default, + description, + } => { PluginPreference::ListOfNumbers { name: name.unwrap_or_else(|| id.to_string()), default, - description + description, } - }, - DbPluginPreference::ListOfEnums { name, default, enum_values, description } => { - let enum_values = enum_values.into_iter() - .map(|value| PreferenceEnumValue { label: value.label, value: value.value }) + } + DbPluginPreference::ListOfEnums { + name, + default, + enum_values, + description, + } => { + let enum_values = enum_values + .into_iter() + .map(|value| { + PreferenceEnumValue { + label: value.label, + value: value.value, + } + }) .collect(); PluginPreference::ListOfEnums { name: name.unwrap_or_else(|| id.to_string()), default, enum_values, - description + description, } - }, + } } } @@ -704,4 +888,3 @@ fn plugin_preference_user_data_from_db(value: DbPluginPreferenceUserData) -> Plu DbPluginPreferenceUserData::ListOfEnums { value, .. } => PluginPreferenceUserData::ListOfEnums { value }, } } - diff --git a/rust/server/src/plugins/run_status.rs b/rust/server/src/plugins/run_status.rs index 46106f2..345cee9 100644 --- a/rust/server/src/plugins/run_status.rs +++ b/rust/server/src/plugins/run_status.rs @@ -1,18 +1,19 @@ use std::collections::HashMap; -use std::sync::{Arc, Mutex}; - -use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; +use std::sync::Arc; +use std::sync::Mutex; use gauntlet_common::model::PluginId; +use tokio_util::sync::CancellationToken; +use tokio_util::sync::WaitForCancellationFutureOwned; pub struct RunStatusHolder { - running_plugins: Arc>> + running_plugins: Arc>>, } impl RunStatusHolder { pub fn new() -> Self { Self { - running_plugins: Arc::new(Mutex::new(HashMap::new())) + running_plugins: Arc::new(Mutex::new(HashMap::new())), } } diff --git a/rust/server/src/plugins/runtime.rs b/rust/server/src/plugins/runtime.rs index e69de29..8b13789 100644 --- a/rust/server/src/plugins/runtime.rs +++ b/rust/server/src/plugins/runtime.rs @@ -0,0 +1 @@ + diff --git a/rust/server/src/plugins/settings.rs b/rust/server/src/plugins/settings.rs index df1f68e..1ec4d33 100644 --- a/rust/server/src/plugins/settings.rs +++ b/rust/server/src/plugins/settings.rs @@ -1,11 +1,20 @@ -use crate::plugins::data_db_repository::{DataDbRepository, DbTheme, DbWindowPositionMode}; -use crate::plugins::theme::{read_theme_file, BundledThemes}; +use std::env::consts::OS; + use anyhow::anyhow; use dark_light::Mode; use gauntlet_common::dirs::Dirs; -use gauntlet_common::model::{PhysicalKey, PhysicalShortcut, SettingsTheme, UiTheme, WindowPositionMode}; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::SettingsTheme; +use gauntlet_common::model::UiTheme; +use gauntlet_common::model::WindowPositionMode; use gauntlet_common::rpc::frontend_api::FrontendApi; -use std::env::consts::OS; + +use crate::plugins::data_db_repository::DataDbRepository; +use crate::plugins::data_db_repository::DbTheme; +use crate::plugins::data_db_repository::DbWindowPositionMode; +use crate::plugins::theme::read_theme_file; +use crate::plugins::theme::BundledThemes; pub struct Settings { dirs: Dirs, @@ -20,14 +29,14 @@ impl Settings { dirs, repository, frontend_api, - themes: BundledThemes::new()? + themes: BundledThemes::new()?, }) } pub async fn effective_global_shortcut(&self) -> anyhow::Result> { match self.global_shortcut().await? { None => { - if cfg!(target_os = "windows") { + if cfg!(target_os = "windows") { Ok(Some(PhysicalShortcut { physical_key: PhysicalKey::Space, modifier_shift: false, @@ -45,7 +54,7 @@ impl Settings { })) } } - Some((shortcut, _)) => Ok(shortcut) + Some((shortcut, _)) => Ok(shortcut), } } @@ -58,8 +67,7 @@ impl Settings { let db_err = err.as_ref().map_err(|err| format!("{:#}", err)).err(); - self.repository.set_global_shortcut(shortcut, db_err) - .await?; + self.repository.set_global_shortcut(shortcut, db_err).await?; err } @@ -68,9 +76,7 @@ impl Settings { match self.repository.get_global_shortcut().await? { None => {} Some((shortcut, _)) => { - self.repository.set_global_shortcut(shortcut, error) - .await?; - + self.repository.set_global_shortcut(shortcut, error).await?; } }; @@ -84,16 +90,16 @@ impl Settings { // TODO config - let settings = self.repository - .get_settings() - .await?; + let settings = self.repository.get_settings().await?; let theme = match &settings.theme { None => self.autodetect_theme(), - Some(theme) => match theme { - DbTheme::MacOSLight => self.themes.macos_light_theme.clone(), - DbTheme::MacOSDark => self.themes.macos_dark_theme.clone(), - DbTheme::Legacy => self.themes.legacy_theme.clone(), + Some(theme) => { + match theme { + DbTheme::MacOSLight => self.themes.macos_light_theme.clone(), + DbTheme::MacOSDark => self.themes.macos_dark_theme.clone(), + DbTheme::Legacy => self.themes.legacy_theme.clone(), + } } }; @@ -107,23 +113,18 @@ impl Settings { // TODO config - let mut settings = self.repository - .get_settings() - .await?; + let mut settings = self.repository.get_settings().await?; match settings.theme { - None => Ok(SettingsTheme::AutoDetect), - Some(DbTheme::MacOSLight) => Ok(SettingsTheme::MacOSLight), - Some(DbTheme::MacOSDark) => Ok(SettingsTheme::MacOSDark), - Some(DbTheme::Legacy) => Ok(SettingsTheme::Legacy), + None => Ok(SettingsTheme::AutoDetect), + Some(DbTheme::MacOSLight) => Ok(SettingsTheme::MacOSLight), + Some(DbTheme::MacOSDark) => Ok(SettingsTheme::MacOSDark), + Some(DbTheme::Legacy) => Ok(SettingsTheme::Legacy), } } pub async fn set_theme_setting(&self, theme: SettingsTheme) -> anyhow::Result<()> { - - let mut settings = self.repository - .get_settings() - .await?; + let mut settings = self.repository.get_settings().await?; settings.theme = match theme { SettingsTheme::AutoDetect => None, @@ -132,15 +133,17 @@ impl Settings { SettingsTheme::Legacy => Some(DbTheme::Legacy), // these should not be visible in settings ui SettingsTheme::Config => Err(anyhow!("Unable to set current theme to config"))?, - SettingsTheme::ThemeFile => Err(anyhow!("Unable to set current theme to a file"))? + SettingsTheme::ThemeFile => Err(anyhow!("Unable to set current theme to a file"))?, }; let theme = match &settings.theme { None => self.autodetect_theme(), - Some(theme) => match theme { - DbTheme::MacOSLight => self.themes.macos_light_theme.clone(), - DbTheme::MacOSDark => self.themes.macos_dark_theme.clone(), - DbTheme::Legacy => self.themes.legacy_theme.clone(), + Some(theme) => { + match theme { + DbTheme::MacOSLight => self.themes.macos_light_theme.clone(), + DbTheme::MacOSDark => self.themes.macos_dark_theme.clone(), + DbTheme::Legacy => self.themes.legacy_theme.clone(), + } } }; @@ -152,23 +155,18 @@ impl Settings { } pub async fn window_position_mode_setting(&self) -> anyhow::Result { - let mut settings = self.repository - .get_settings() - .await?; + let mut settings = self.repository.get_settings().await?; let window_position_mode = match &settings.window_position_mode { None => WindowPositionMode::Static, - Some(DbWindowPositionMode::ActiveMonitor) => WindowPositionMode::ActiveMonitor + Some(DbWindowPositionMode::ActiveMonitor) => WindowPositionMode::ActiveMonitor, }; Ok(window_position_mode) } pub async fn set_window_position_mode_setting(&self, mode: WindowPositionMode) -> anyhow::Result<()> { - - let mut settings = self.repository - .get_settings() - .await?; + let mut settings = self.repository.get_settings().await?; let window_position_mode = match mode { WindowPositionMode::Static => None, @@ -188,7 +186,7 @@ impl Settings { match dark_light::detect() { Mode::Dark => self.themes.macos_dark_theme.clone(), Mode::Light => self.themes.macos_light_theme.clone(), - Mode::Default => self.themes.macos_dark_theme.clone() + Mode::Default => self.themes.macos_dark_theme.clone(), } } } diff --git a/rust/server/src/plugins/theme.rs b/rust/server/src/plugins/theme.rs index 71face3..8b9ded9 100644 --- a/rust/server/src/plugins/theme.rs +++ b/rust/server/src/plugins/theme.rs @@ -1,12 +1,22 @@ use std::env::consts::OS; use std::io::ErrorKind; use std::path::PathBuf; -use anyhow::{anyhow, Context}; + +use anyhow::anyhow; +use anyhow::Context; use dark_light::Mode; -use serde::{Deserialize, Serialize}; use gauntlet_common::dirs::Dirs; -use gauntlet_common::model::{UiTheme, UiThemeColor, UiThemeContent, UiThemeContentBorder, UiThemeMode, UiThemeWindow, UiThemeWindowBorder}; +use gauntlet_common::model::UiTheme; +use gauntlet_common::model::UiThemeColor; +use gauntlet_common::model::UiThemeContent; +use gauntlet_common::model::UiThemeContentBorder; +use gauntlet_common::model::UiThemeMode; +use gauntlet_common::model::UiThemeWindow; +use gauntlet_common::model::UiThemeWindowBorder; use gauntlet_common::rpc::frontend_api::FrontendApi; +use serde::Deserialize; +use serde::Serialize; + use crate::plugins::data_db_repository::DataDbRepository; pub struct BundledThemes { @@ -16,8 +26,14 @@ pub struct BundledThemes { } const LEGACY_THEME: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../bundled_themes/legacy.toml")); -const MACOS_DARK_THEME: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../bundled_themes/macos_dark.toml")); -const MACOS_LIGHT_THEME: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../bundled_themes/macos_light.toml")); +const MACOS_DARK_THEME: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../bundled_themes/macos_dark.toml" +)); +const MACOS_LIGHT_THEME: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../bundled_themes/macos_light.toml" +)); impl BundledThemes { pub fn new() -> anyhow::Result { @@ -36,13 +52,13 @@ pub fn convert_theme(config_theme: ConfigTheme) -> anyhow::Result { Ok(UiTheme { mode: match config_theme.mode { ConfigThemeMode::Light => UiThemeMode::Light, - ConfigThemeMode::Dark => UiThemeMode::Dark + ConfigThemeMode::Dark => UiThemeMode::Dark, }, background: [ convert_complex_color(background_100)?, convert_complex_color(background_200)?, convert_complex_color(background_300)?, - convert_complex_color(background_400)? + convert_complex_color(background_400)?, ], text: [ convert_complex_color(text_100)?, @@ -69,7 +85,6 @@ fn convert_complex_color(color: ConfigThemeColor) -> anyhow::Result convert_color(value, true), ConfigThemeColor::Object { color, alpha } => { - if !(0.0..=1.0).contains(&alpha) { Err(anyhow!("Alpha component must be on [0, 1] range"))?; } @@ -91,8 +106,7 @@ fn convert_color(color: String, allow_alpha: bool) -> anyhow::Result anyhow::Result { - let num = usize::from_str_radix(&hex[from..=to], 16) - .context("Unable to parse as hex number")?; + let num = usize::from_str_radix(&hex[from..=to], 16).context("Unable to parse as hex number")?; let num = num as f32 / 255.0; // If we only got half a byte (one letter), expand it into a full byte (two letters) @@ -100,12 +114,14 @@ fn convert_color(color: String, allow_alpha: bool) -> anyhow::Result UiThemeColor { - r: parse_channel(0, 0)?, - g: parse_channel(1, 1)?, - b: parse_channel(2, 2)?, - a: 1.0, - }, + 3 => { + UiThemeColor { + r: parse_channel(0, 0)?, + g: parse_channel(1, 1)?, + b: parse_channel(2, 2)?, + a: 1.0, + } + } 4 => { if allow_alpha { UiThemeColor { @@ -117,13 +133,15 @@ fn convert_color(color: String, allow_alpha: bool) -> anyhow::Result UiThemeColor { - r: parse_channel(0, 1)?, - g: parse_channel(2, 3)?, - b: parse_channel(4, 5)?, - a: 1.0, - }, + } + 6 => { + UiThemeColor { + r: parse_channel(0, 1)?, + g: parse_channel(2, 3)?, + b: parse_channel(4, 5)?, + a: 1.0, + } + } 8 => { if allow_alpha { UiThemeColor { @@ -135,7 +153,7 @@ fn convert_color(color: String, allow_alpha: bool) -> anyhow::Result Err(anyhow!("invalid length of a color string"))?, }; @@ -143,12 +161,11 @@ fn convert_color(color: String, allow_alpha: bool) -> anyhow::Result anyhow::Result { - let value = toml::from_str::(value) - .context("Unable to parse theme file")?; + let value = toml::from_str::(value).context("Unable to parse theme file")?; match convert_theme(value) { Ok(value) => Ok(value), - Err(err) => Err(err.context("Unable to parse theme file")) + Err(err) => Err(err.context("Unable to parse theme file")), } } @@ -162,17 +179,17 @@ pub fn read_theme_file(theme_file: PathBuf) -> Option { None } } - }, + } Err(err) => { match err.kind() { ErrorKind::NotFound => { tracing::debug!("No theme file was found"); None - }, + } err @ _ => { tracing::warn!("Unable to read theme file: {}", err); None - }, + } } } } @@ -183,17 +200,14 @@ pub enum ConfigThemeMode { #[serde(rename = "light")] Light, #[serde(rename = "dark")] - Dark + Dark, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum ConfigThemeColor { String(String), - Object { - color: String, - alpha: f32 - } + Object { color: String, alpha: f32 }, } pub type ConfigThemeColorPalette = [ConfigThemeColor; 4]; diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index c422631..4439ca8 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -1,9 +1,23 @@ use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; -use gauntlet_common::{settings_env_data_to_string, SettingsEnvData}; -use gauntlet_common::model::{DownloadStatus, EntrypointId, PluginId, PluginPreferenceUserData, SettingsPlugin, UiPropertyValue, SearchResult, UiWidgetId, PhysicalKey, PhysicalShortcut, LocalSaveData, SettingsTheme, WindowPositionMode}; + +use gauntlet_common::model::DownloadStatus; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::LocalSaveData; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::PluginPreferenceUserData; +use gauntlet_common::model::SearchResult; +use gauntlet_common::model::SettingsPlugin; +use gauntlet_common::model::SettingsTheme; +use gauntlet_common::model::UiPropertyValue; +use gauntlet_common::model::UiWidgetId; +use gauntlet_common::model::WindowPositionMode; use gauntlet_common::rpc::backend_server::BackendServer; +use gauntlet_common::settings_env_data_to_string; +use gauntlet_common::SettingsEnvData; use crate::plugins::ApplicationManager; use crate::search::SearchIndex; @@ -14,15 +28,12 @@ pub struct BackendServerImpl { impl BackendServerImpl { pub fn new(application_manager: Arc) -> Self { - Self { - application_manager - } + Self { application_manager } } } #[tonic::async_trait] impl BackendServer for BackendServerImpl { - async fn show_window(&self) -> anyhow::Result<()> { self.application_manager.show_window().await } @@ -34,51 +45,73 @@ impl BackendServer for BackendServerImpl { } async fn plugins(&self) -> anyhow::Result> { - let result = self.application_manager.plugins() - .await; + let result = self.application_manager.plugins().await; if let Err(err) = &result { - tracing::warn!(target = "rpc", "error occurred when handling 'plugins' request {:?}", err) + tracing::warn!( + target = "rpc", + "error occurred when handling 'plugins' request {:?}", + err + ) } result } async fn set_plugin_state(&self, plugin_id: PluginId, enabled: bool) -> anyhow::Result<()> { - let result = self.application_manager.set_plugin_state(plugin_id, enabled) - .await; + let result = self.application_manager.set_plugin_state(plugin_id, enabled).await; if let Err(err) = &result { - tracing::warn!(target = "rpc", "error occurred when handling 'set_plugin_state' request {:?}", err) + tracing::warn!( + target = "rpc", + "error occurred when handling 'set_plugin_state' request {:?}", + err + ) } Ok(()) } - async fn set_entrypoint_state(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, enabled: bool) -> anyhow::Result<()> { - let result = self.application_manager.set_entrypoint_state(plugin_id, entrypoint_id, enabled) + async fn set_entrypoint_state( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + enabled: bool, + ) -> anyhow::Result<()> { + let result = self + .application_manager + .set_entrypoint_state(plugin_id, entrypoint_id, enabled) .await; if let Err(err) = &result { - tracing::warn!(target = "rpc", "error occurred when handling 'set_entrypoint_state' request {:?}", err) + tracing::warn!( + target = "rpc", + "error occurred when handling 'set_entrypoint_state' request {:?}", + err + ) } Ok(()) } async fn set_global_shortcut(&self, shortcut: Option) -> anyhow::Result<()> { - let result = self.application_manager.set_global_shortcut(shortcut) - .await; + let result = self.application_manager.set_global_shortcut(shortcut).await; if let Err(err) = &result { - tracing::warn!(target = "rpc", "error occurred when handling 'set_global_shortcut' request {:?}", err) + tracing::warn!( + target = "rpc", + "error occurred when handling 'set_global_shortcut' request {:?}", + err + ) } result } async fn get_global_shortcut(&self) -> anyhow::Result<(Option, Option)> { - let result = self.application_manager.get_global_shortcut() + let result = self + .application_manager + .get_global_shortcut() .await? .unwrap_or((None, None)); @@ -97,27 +130,42 @@ impl BackendServer for BackendServerImpl { self.application_manager.set_window_position_mode(mode).await } - async fn get_window_position_mode(&self, ) -> anyhow::Result { + async fn get_window_position_mode(&self) -> anyhow::Result { self.application_manager.get_window_position_mode().await } - async fn set_preference_value(&self, plugin_id: PluginId, entrypoint_id: Option, preference_id: String, preference_value: PluginPreferenceUserData) -> anyhow::Result<()> { - let result = self.application_manager.set_preference_value(plugin_id, entrypoint_id, preference_id, preference_value) + async fn set_preference_value( + &self, + plugin_id: PluginId, + entrypoint_id: Option, + preference_id: String, + preference_value: PluginPreferenceUserData, + ) -> anyhow::Result<()> { + let result = self + .application_manager + .set_preference_value(plugin_id, entrypoint_id, preference_id, preference_value) .await; if let Err(err) = &result { - tracing::warn!(target = "rpc", "error occurred when handling 'set_preference_value' request {:?}", err) + tracing::warn!( + target = "rpc", + "error occurred when handling 'set_preference_value' request {:?}", + err + ) } Ok(()) } async fn download_plugin(&self, plugin_id: PluginId) -> anyhow::Result<()> { - let result = self.application_manager.download_plugin(plugin_id) - .await; + let result = self.application_manager.download_plugin(plugin_id).await; if let Err(err) = &result { - tracing::warn!(target = "rpc", "error occurred when handling 'download_plugin' request {:?}", err) + tracing::warn!( + target = "rpc", + "error occurred when handling 'download_plugin' request {:?}", + err + ) } Ok(()) @@ -128,19 +176,21 @@ impl BackendServer for BackendServerImpl { } async fn remove_plugin(&self, plugin_id: PluginId) -> anyhow::Result<()> { - let result = self.application_manager.remove_plugin(plugin_id) - .await; + let result = self.application_manager.remove_plugin(plugin_id).await; if let Err(err) = &result { - tracing::warn!(target = "rpc", "error occurred when handling 'remove_plugin' request {:?}", err) + tracing::warn!( + target = "rpc", + "error occurred when handling 'remove_plugin' request {:?}", + err + ) } Ok(()) } async fn save_local_plugin(&self, path: String) -> anyhow::Result { - let result = self.application_manager.save_local_plugin(&path) - .await?; + let result = self.application_manager.save_local_plugin(&path).await?; Ok(result) } diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index 3a06299..78f9cd9 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -1,13 +1,32 @@ use std::cmp::Ordering; use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use tantivy::{doc, Index, IndexReader, IndexWriter, ReloadPolicy, Searcher}; +use std::sync::Arc; +use std::sync::Mutex; + +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::SearchResult; +use gauntlet_common::model::SearchResultAccessory; +use gauntlet_common::model::SearchResultEntrypointAction; +use gauntlet_common::model::SearchResultEntrypointActionType; +use gauntlet_common::model::SearchResultEntrypointType; +use gauntlet_common::rpc::frontend_api::FrontendApi; use tantivy::collector::TopDocs; -use tantivy::query::{AllQuery, BooleanQuery, FuzzyTermQuery, Query, RegexQuery, TermQuery}; +use tantivy::doc; +use tantivy::query::AllQuery; +use tantivy::query::BooleanQuery; +use tantivy::query::FuzzyTermQuery; +use tantivy::query::Query; +use tantivy::query::RegexQuery; +use tantivy::query::TermQuery; use tantivy::schema::*; use tantivy::tokenizer::TokenizerManager; -use gauntlet_common::model::{EntrypointId, PhysicalShortcut, PluginId, SearchResult, SearchResultAccessory, SearchResultEntrypointAction, SearchResultEntrypointActionType, SearchResultEntrypointType}; -use gauntlet_common::rpc::frontend_api::FrontendApi; +use tantivy::Index; +use tantivy::IndexReader; +use tantivy::IndexWriter; +use tantivy::ReloadPolicy; +use tantivy::Searcher; #[derive(Clone)] pub struct SearchIndex { @@ -69,7 +88,6 @@ pub enum SearchIndexItemActionActionType { View, } - impl SearchIndex { pub fn create_index(frontend_api: FrontendApi) -> tantivy::Result { let schema = { @@ -83,17 +101,18 @@ impl SearchIndex { schema_builder.build() }; - let entrypoint_name = schema.get_field("entrypoint_name").expect("entrypoint_name field should exist"); - let entrypoint_id = schema.get_field("entrypoint_id").expect("entrypoint_id field should exist"); + let entrypoint_name = schema + .get_field("entrypoint_name") + .expect("entrypoint_name field should exist"); + let entrypoint_id = schema + .get_field("entrypoint_id") + .expect("entrypoint_id field should exist"); let plugin_name = schema.get_field("plugin_name").expect("plugin_name field should exist"); let plugin_id = schema.get_field("plugin_id").expect("plugin_id field should exist"); let index = Index::create_in_ram(schema.clone()); - let index_reader = index - .reader_builder() - .reload_policy(ReloadPolicy::Manual) - .try_into()?; + let index_reader = index.reader_builder().reload_policy(ReloadPolicy::Manual).try_into()?; Ok(Self { frontend_api, @@ -115,9 +134,10 @@ impl SearchIndex { let mut index_writer = self.index.writer::(15_000_000)?; - index_writer.delete_query(Box::new( - TermQuery::new(Term::from_field_text(self.plugin_id, &plugin_id.to_string()), IndexRecordOption::Basic) - ))?; + index_writer.delete_query(Box::new(TermQuery::new( + Term::from_field_text(self.plugin_id, &plugin_id.to_string()), + IndexRecordOption::Basic, + )))?; index_writer.commit()?; self.index_reader.reload()?; @@ -126,7 +146,13 @@ impl SearchIndex { Ok(()) } - pub fn save_for_plugin(&self, plugin_id: PluginId, plugin_name: String, search_items: Vec, refresh_search_list: bool) -> tantivy::Result<()> { + pub fn save_for_plugin( + &self, + plugin_id: PluginId, + plugin_name: String, + search_items: Vec, + refresh_search_list: bool, + ) -> tantivy::Result<()> { tracing::debug!("Reloading search index for plugin {:?}", plugin_id); // writer panics if another writer exists @@ -135,9 +161,10 @@ impl SearchIndex { let mut index_writer = self.index.writer::(15_000_000)?; - index_writer.delete_query(Box::new( - TermQuery::new(Term::from_field_text(self.plugin_id, &plugin_id.to_string()), IndexRecordOption::Basic) - ))?; + index_writer.delete_query(Box::new(TermQuery::new( + Term::from_field_text(self.plugin_id, &plugin_id.to_string()), + IndexRecordOption::Basic, + )))?; for search_item in &search_items { index_writer.add_document(doc!( @@ -151,16 +178,21 @@ impl SearchIndex { index_writer.commit()?; self.index_reader.reload()?; - let data = search_items.into_iter() + let data = search_items + .into_iter() .map(|item| { - let actions = item.entrypoint_actions.into_iter() - .map(|action| EntrypointActionData { - label: action.label, - action_type: match action.action_type { - SearchIndexItemActionActionType::Command => EntrypointActionType::Command, - SearchIndexItemActionActionType::View => EntrypointActionType::View, - }, - shortcut: action.shortcut, + let actions = item + .entrypoint_actions + .into_iter() + .map(|action| { + EntrypointActionData { + label: action.label, + action_type: match action.action_type { + SearchIndexItemActionActionType::Command => EntrypointActionType::Command, + SearchIndexItemActionActionType::View => EntrypointActionType::View, + }, + shortcut: action.shortcut, + } }) .collect(); @@ -182,10 +214,12 @@ impl SearchIndex { if refresh_search_list { let mut frontend_api = self.frontend_api.clone(); tokio::spawn(async move { - tracing::info!("requesting search results update because search index update for plugin: {:?}", plugin_id); + tracing::info!( + "requesting search results update because search index update for plugin: {:?}", + plugin_id + ); - let result = frontend_api.request_search_results_update() - .await; + let result = frontend_api.request_search_results_update().await; if let Err(err) = &result { tracing::warn!("error occurred when requesting search results update {:?}", err) @@ -201,18 +235,19 @@ impl SearchIndex { let searcher = self.index_reader.searcher(); - let query_parser = QueryParser::new( - self.index.tokenizers().clone(), - self.entrypoint_name, - self.plugin_name, - ); + let query_parser = QueryParser::new(self.index.tokenizers().clone(), self.entrypoint_name, self.plugin_name); let query = query_parser.create_query(query); let mut index = 0; let fetch = std::iter::from_fn(|| -> Option>> { - let result = self.fetch(&entrypoint_data, &query, TopDocs::with_limit(20).and_offset(index * 20), &searcher); + let result = self.fetch( + &entrypoint_data, + &query, + TopDocs::with_limit(20).and_offset(index * 20), + &searcher, + ); index += 1; @@ -224,42 +259,55 @@ impl SearchIndex { Some(Ok(result)) } } - Err(error) => { - Some(Err(error)) - } + Err(error) => Some(Err(error)), } }); let result = fetch.collect::>, _>>()?; - let mut result = result.into_iter() - .flatten() - .collect::>(); + let mut result = result.into_iter().flatten().collect::>(); result.sort_by(|(_, score_a), (_, score_b)| score_b.total_cmp(score_a)); - let result = result.into_iter() - .map(|(item, _)| item) - .collect::>(); + let result = result.into_iter().map(|(item, _)| item).collect::>(); drop(entrypoint_data); Ok(result) } - fn fetch(&self, entrypoint_data: &HashMap>, query: &dyn Query, collector: TopDocs, searcher: &Searcher) -> anyhow::Result> { + fn fetch( + &self, + entrypoint_data: &HashMap>, + query: &dyn Query, + collector: TopDocs, + searcher: &Searcher, + ) -> anyhow::Result> { let get_str_field = |retrieved_doc: &TantivyDocument, field: Field| -> String { - retrieved_doc.get_first(field) - .unwrap_or_else(|| panic!("there should be a field with name {:?}", searcher.schema().get_field_name(field))) + retrieved_doc + .get_first(field) + .unwrap_or_else(|| { + panic!( + "there should be a field with name {:?}", + searcher.schema().get_field_name(field) + ) + }) .as_str() - .unwrap_or_else(|| panic!("field with name {:?} should contain string", searcher.schema().get_field_name(field))) + .unwrap_or_else(|| { + panic!( + "field with name {:?} should contain string", + searcher.schema().get_field_name(field) + ) + }) .to_owned() }; - let result = searcher.search(query, &collector)? + let result = searcher + .search(query, &collector)? .into_iter() .map(|(_score, doc_address)| { - let retrieved_doc = searcher.doc::(doc_address) + let retrieved_doc = searcher + .doc::(doc_address) .expect("index should contain just searched results"); let entrypoint_id = EntrypointId::from_string(get_str_field(&retrieved_doc, self.entrypoint_id)); @@ -273,20 +321,22 @@ impl SearchIndex { .get(&entrypoint_id) .expect("Entrypoint should always exist in plugin in entrypoint data"); - let entrypoint_actions = entrypoint_data.actions.iter() - .map(|data| SearchResultEntrypointAction { - action_type: match data.action_type { - EntrypointActionType::Command => SearchResultEntrypointActionType::Command, - EntrypointActionType::View => SearchResultEntrypointActionType::View - }, - label: data.label.clone(), - shortcut: data.shortcut.clone(), + let entrypoint_actions = entrypoint_data + .actions + .iter() + .map(|data| { + SearchResultEntrypointAction { + action_type: match data.action_type { + EntrypointActionType::Command => SearchResultEntrypointActionType::Command, + EntrypointActionType::View => SearchResultEntrypointActionType::View, + }, + label: data.label.clone(), + shortcut: data.shortcut.clone(), + } }) .collect(); - let entrypoint_accessories = entrypoint_data.accessories.iter() - .cloned() - .collect(); + let entrypoint_accessories = entrypoint_data.accessories.iter().cloned().collect(); let result_item = SearchResult { entrypoint_type: entrypoint_data.entrypoint_type.clone(), @@ -329,13 +379,14 @@ impl QueryParser { } let contains_terms_fn = |field: Field| -> Box { - let res = self.tokenize(query) + let res = self + .tokenize(query) .into_iter() .map(|term| -> Box { Box::new( // basically a "contains" query RegexQuery::from_pattern(&format!(".*{}.*", regex::escape(&term)), field) - .expect("there should not exist a situation where that regex is invalid") + .expect("there should not exist a situation where that regex is invalid"), ) }) .collect::>(); @@ -343,21 +394,15 @@ impl QueryParser { Box::new(BooleanQuery::intersection(res)) }; - let terms_fn = |field: Field| -> Box { - Box::new( - contains_terms_fn(field) - ) - }; + let terms_fn = |field: Field| -> Box { Box::new(contains_terms_fn(field)) }; let entrypoint_name_terms = terms_fn(self.entrypoint_name); let plugin_name_terms = terms_fn(self.plugin_name); - Box::new( - BooleanQuery::union(vec![ - Box::new(entrypoint_name_terms), - Box::new(plugin_name_terms), - ]), - ) + Box::new(BooleanQuery::union(vec![ + Box::new(entrypoint_name_terms), + Box::new(plugin_name_terms), + ])) } fn tokenize(&self, query: &str) -> Vec { diff --git a/rust/utils/src/channel.rs b/rust/utils/src/channel.rs index aeb308c..0b84d57 100644 --- a/rust/utils/src/channel.rs +++ b/rust/utils/src/channel.rs @@ -1,6 +1,8 @@ use std::time::Duration; + use thiserror::Error; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::mpsc; +use tokio::sync::oneshot; use tokio::time::error::Elapsed; #[derive(Error, Debug)] @@ -32,33 +34,31 @@ impl ResponseReceiver { } pub async fn recv(&mut self) -> Res { - self.response_receiver.take() + self.response_receiver + .take() .expect("recv was called second time") .await .expect("oneshot was dropped before sending") } } - #[derive(Debug)] pub struct RequestSender { request_sender: mpsc::UnboundedSender>, } impl RequestSender { - fn new( - request_sender: mpsc::UnboundedSender>, - ) -> Self { - RequestSender { - request_sender, - } + fn new(request_sender: mpsc::UnboundedSender>) -> Self { + RequestSender { request_sender } } pub fn send(&self, request: Req) -> Result, RequestError> { let (response_sender, response_receiver) = oneshot::channel::(); let responder = Responder::new(response_sender); let payload = (request, responder); - self.request_sender.send(payload).map_err(|err| RequestError::OtherSideWasDropped)?; + self.request_sender + .send(payload) + .map_err(|err| RequestError::OtherSideWasDropped)?; Ok(ResponseReceiver::new(response_receiver)) } @@ -81,7 +81,6 @@ impl Clone for RequestSender { } } - #[derive(Debug)] pub struct RequestReceiver { request_receiver: mpsc::UnboundedReceiver>, @@ -95,7 +94,8 @@ impl RequestReceiver { } pub async fn recv(&mut self) -> Payload { - self.request_receiver.recv() + self.request_receiver + .recv() .await .expect("the other side of a channel was dropped") } diff --git a/rust/utils/src/lib.rs b/rust/utils/src/lib.rs index ff4a736..ff02972 100644 --- a/rust/utils/src/lib.rs +++ b/rust/utils/src/lib.rs @@ -1 +1 @@ -pub mod channel; \ No newline at end of file +pub mod channel; diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..6276ba7 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,9 @@ +unstable_features = true + +imports_granularity = "Item" +max_width = 120 +force_multiline_blocks = true +group_imports = "StdExternalCrate" +single_line_let_else_max_width = 0 +combine_control_expr = false +use_field_init_shorthand = true \ No newline at end of file From 7f185f29724586783ac88e62ab4d757f3a1b7011 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 3 Feb 2025 18:15:52 +0100 Subject: [PATCH 335/540] In search results, move name of plugin to entrypoint name, add entrypoint type in that place use. Use generator name as a type --- rust/client/src/ui/search_list.rs | 39 ++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index 85aa0a5..c261d3a 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use gauntlet_common::model::IconAccessoryWidget; +use gauntlet_common::model::{IconAccessoryWidget, SearchResultEntrypointType}; use gauntlet_common::model::ImageLike; use gauntlet_common::model::SearchResult; use gauntlet_common::model::SearchResultAccessory; @@ -35,23 +35,16 @@ pub fn search_list<'a>( .iter() .enumerate() .map(|(index, search_result)| { - let main_text: Element<_> = text(&search_result.entrypoint_name).shaping(Shaping::Advanced).into(); - let main_text: Element<_> = container(main_text).themed(ContainerStyle::MainListItemText); + let entrypoint_name: Element<_> = text(&search_result.entrypoint_name).shaping(Shaping::Advanced).into(); + let entrypoint_name: Element<_> = container(entrypoint_name).themed(ContainerStyle::MainListItemText); let spacer: Element<_> = horizontal_space().width(Length::Fill).into(); - let sub_text = match &search_result.entrypoint_generator_name { - None => &search_result.plugin_name, - Some(entrypoint_generator_name) => { - &format!("{} - {}", entrypoint_generator_name, &search_result.plugin_name) - } - }; - - let sub_text: Element<_> = text(sub_text.clone()) + let plugin_name_text: Element<_> = text(search_result.plugin_name.clone()) .shaping(Shaping::Advanced) .themed(TextStyle::MainListItemSubtext); - let sub_text: Element<_> = container(sub_text).themed(ContainerStyle::MainListItemSubText); // FIXME find a way to set padding based on whether the scroll bar is visible + let plugin_name_text: Element<_> = container(plugin_name_text).themed(ContainerStyle::MainListItemSubText); // FIXME find a way to set padding based on whether the scroll bar is visible let mut button_content = vec![]; @@ -71,7 +64,8 @@ pub fn search_list<'a>( button_content.push(spacer); } - button_content.push(main_text); + button_content.push(entrypoint_name); + button_content.push(plugin_name_text); button_content.push(spacer); if search_result.entrypoint_accessories.len() > 0 { @@ -110,7 +104,24 @@ pub fn search_list<'a>( button_content.push(accessories); } - button_content.push(sub_text); + let type_text = match search_result.entrypoint_type { + SearchResultEntrypointType::Command => "Command", + SearchResultEntrypointType::View => "View", + SearchResultEntrypointType::Generated => { + match &search_result.entrypoint_generator_name { + None => "", + Some(entrypoint_generator_name) => entrypoint_generator_name, + } + } + }; + + let type_text: Element<_> = text(type_text.to_string()) + .shaping(Shaping::Advanced) + .themed(TextStyle::MainListItemSubtext); + + let type_text: Element<_> = container(type_text).themed(ContainerStyle::MainListItemSubText); + + button_content.push(type_text); let button_content: Element<_> = row(button_content).align_y(Alignment::Center).into(); From b949886314bf3d19dbf3e06de04babbab2a5612b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 3 Feb 2025 18:16:43 +0100 Subject: [PATCH 336/540] Make subtext in main window search items a little smaller --- rust/client/src/ui/theme/text.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/client/src/ui/theme/text.rs b/rust/client/src/ui/theme/text.rs index e681380..13be5d2 100644 --- a/rust/client/src/ui/theme/text.rs +++ b/rust/client/src/ui/theme/text.rs @@ -43,6 +43,7 @@ impl<'a, Message: 'a> ThemableWidget<'a, Message> for Text<'a, GauntletComplexTh self.class(kind).size(theme.metadata_item_label.text_size).into() } TextStyle::InlineName => self.size(15).class(kind).into(), + TextStyle::MainListItemSubtext => self.size(15).class(kind).into(), _ => self.class(kind).into(), } } From 9a1ee39485ba5c992b5a9479ba3ee8229bf86a42 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 3 Feb 2025 18:18:05 +0100 Subject: [PATCH 337/540] Fix formatting --- rust/client/src/ui/search_list.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index c261d3a..859dcbe 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -1,9 +1,10 @@ use std::collections::HashMap; -use gauntlet_common::model::{IconAccessoryWidget, SearchResultEntrypointType}; +use gauntlet_common::model::IconAccessoryWidget; use gauntlet_common::model::ImageLike; use gauntlet_common::model::SearchResult; use gauntlet_common::model::SearchResultAccessory; +use gauntlet_common::model::SearchResultEntrypointType; use gauntlet_common::model::TextAccessoryWidget; use iced::advanced::image::Handle; use iced::widget::button; From e36ff58a94da110d0d7d7e18ed8d5edfd7bea51a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 8 Feb 2025 22:19:14 +0100 Subject: [PATCH 338/540] Fix errors now showing up in console when developing --- rust/plugin_runtime/Cargo.toml | 1 + rust/plugin_runtime/src/deno.rs | 34 +++++++++++++++++++-------------- rust/server/Cargo.toml | 2 +- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml index 9e208fc..5f94b15 100644 --- a/rust/plugin_runtime/Cargo.toml +++ b/rust/plugin_runtime/Cargo.toml @@ -61,3 +61,4 @@ windows = { version = "0.58.0", features = ["Win32_Storage_FileSystem", "Win32_U [features] scenario_runner = [] +release = [] diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index 5e46983..259e4f4 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -444,6 +444,8 @@ pub async fn start_js_runtime( event_stream: Receiver, api: BackendForPluginRuntimeApiProxy, ) -> anyhow::Result<()> { + let bundled = init.plugin_id.to_string().starts_with("bundled://"); + let stdout = if let Some(stdout_file) = init.stdout_file { let stdout_file = PathBuf::from(stdout_file); @@ -451,13 +453,15 @@ pub async fn start_js_runtime( StdioPipe::file(out_log_file) } else { - #[cfg(not(windows))] - let stdout = StdioPipe::file(File::options().write(true).open("/dev/null")?); - - #[cfg(windows)] - let stdout = StdioPipe::file(File::options().write(true).open("nul")?); - - stdout + if cfg!(all(windows, feature = "release")) { + StdioPipe::file(File::options().write(true).open("nul")?) + } else { + if bundled { + StdioPipe::inherit() + } else { + StdioPipe::file(File::options().write(true).open("/dev/null")?) + } + } }; let stderr = if let Some(stderr_file) = init.stderr_file { @@ -467,13 +471,15 @@ pub async fn start_js_runtime( StdioPipe::file(err_log_file) } else { - #[cfg(not(windows))] - let stderr = StdioPipe::file(File::options().write(true).open("/dev/null")?); - - #[cfg(windows)] - let stderr = StdioPipe::file(File::options().write(true).open("nul")?); - - stderr + if cfg!(all(windows, feature = "release")) { + StdioPipe::file(File::options().write(true).open("nul")?) + } else { + if bundled { + StdioPipe::inherit() + } else { + StdioPipe::file(File::options().write(true).open("/dev/null")?) + } + } }; #[cfg(not(windows))] diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index f3d7cf1..52349af 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -43,7 +43,7 @@ vergen-pretty = "0.3" dark-light = "1.1.1" [features] -release = ["gauntlet-common/release"] +release = ["gauntlet-common/release", "gauntlet-plugin-runtime/release"] scenario_runner = ["dep:gauntlet-scenario-runner", "gauntlet-common/scenario_runner", "gauntlet-plugin-runtime/scenario_runner"] [build-dependencies] From 1b8ddf67b176a6a1d088726cfff0fd0f2fa0db35 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 9 Feb 2025 14:00:59 +0100 Subject: [PATCH 339/540] Implement ability to run entrypoints from cli --- CHANGELOG.md | 15 + rust/cli/src/lib.rs | 32 +- rust/client/src/ui/mod.rs | 547 +++++++++++++++----------- rust/client/src/ui/state/mod.rs | 52 ++- rust/common/src/model.rs | 13 + rust/common/src/rpc/backend_api.rs | 21 +- rust/common/src/rpc/backend_server.rs | 29 ++ rust/common/src/rpc/frontend_api.rs | 42 ++ rust/server/src/lib.rs | 31 ++ rust/server/src/plugins/js.rs | 1 + rust/server/src/plugins/mod.rs | 162 ++++++++ rust/server/src/rpc.rs | 6 + rust/server/src/search.rs | 92 ++++- schema/backend.proto | 9 + 14 files changed, 809 insertions(+), 243 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80876c9..1c158c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,21 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- It is now possible to run commands and open views using CLI command + - Format: `gauntlet run ` + - Plugin ID can be found in Settings UI + - Entrypoint ID can be found in: + - For entrypoint types `command` and `view` - in Plugin Manifest or in Settings UI TODO + - For entrypoint type `entrypoint-generator` - in Settings UI TODO + - Action ID can also be found in Plugin Manifest + - Action ID option also supports special values + - `:primary` - to run primary action of the entrypoint + - `:secondary` - to run secondary action of the entrypoint +- Slightly improved --help documentation of CLI command + +- In main window search result, moved plugin name next to entrypoint name +- In main window search result, displayed type of entrypoint in place of plugin name, use generator entrypoint name if generated + - Fix no plugins starting on Windows in release mode - Fix all global shortcut registrations failing if one shortcut registration failed - Fix error when registering shortcut erroring whole settings window instead of adding an icon diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs index c46bd28..14a7788 100644 --- a/rust/cli/src/lib.rs +++ b/rust/cli/src/lib.rs @@ -1,23 +1,46 @@ +use std::time::Duration; + use anyhow::anyhow; use anyhow::Context; use clap::Parser; use gauntlet_client::open_window; use gauntlet_management_client::start_management_client; +use gauntlet_server::run_action; use gauntlet_server::start; +/// Gauntlet CLI +/// +/// If no subcommand is provided server will be started or if one is already running window will be opened #[derive(Debug, clap::Parser)] struct Cli { #[command(subcommand)] command: Option, + /// Start server without opening Gauntlet window, only used if no subcommand is provided #[arg(long)] minimized: bool, } #[derive(Debug, clap::Subcommand)] enum Commands { + /// Open Gauntlet window Open, + /// Open Gauntlet settings Settings, + /// Run action (only ones visible in main window search results) of specific entrypoint of specific plugin + Run { + /// Plugin ID, can be found in settings + plugin_id: String, + + /// Entrypoint ID, can be found in plugin manifest at `entrypoint.*.id` + entrypoint_id: String, + + /// Action ID, can be found in plugin manifest at `entrypoint.actions.*.id`. + /// Alternatively, following special values are supported: + /// `:primary` (action run with Enter shortcut) or + /// `:secondary` (action run with Shift+Enter shortcut) + action_id: String, + }, } pub fn init() { @@ -25,7 +48,7 @@ pub fn init() { let cli = Cli::parse(); - match &cli.command { + match cli.command { None => { if cfg!(feature = "release") { #[cfg(target_os = "macos")] @@ -46,6 +69,13 @@ pub fn init() { match command { Commands::Open => open_window(), Commands::Settings => start_management_client(), + Commands::Run { + plugin_id, + entrypoint_id, + action_id, + } => { + run_action(plugin_id, entrypoint_id, action_id); + } }; } } diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index f4af368..bc9cceb 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -53,9 +53,11 @@ use iced::futures; use iced::futures::channel::mpsc::Sender; use iced::futures::SinkExt; use iced::keyboard; +use iced::keyboard::key; use iced::keyboard::key::Named; use iced::keyboard::key::Physical; use iced::keyboard::Key; +use iced::keyboard::Location; use iced::keyboard::Modifiers; use iced::stream; use iced::widget::button; @@ -185,6 +187,19 @@ pub enum AppMsg { entrypoint_name: String, action_index: usize, }, + ShowNewView { + plugin_id: PluginId, + plugin_name: String, + entrypoint_id: EntrypointId, + entrypoint_name: String, + }, + ShowNewGeneratedView { + plugin_id: PluginId, + plugin_name: String, + entrypoint_id: EntrypointId, + entrypoint_name: String, + action_index: usize, + }, RunCommand { plugin_id: PluginId, entrypoint_id: EntrypointId, @@ -605,14 +620,17 @@ fn new( match render_location { UiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique()), UiRenderLocation::View => { - GlobalState::new_plugin(PluginViewData { - top_level_view, - plugin_id, - plugin_name: "Screenshot Gen".to_string(), - entrypoint_id, - entrypoint_name: gen_name, - action_shortcuts: Default::default(), - }) + GlobalState::new_plugin( + PluginViewData { + top_level_view, + plugin_id, + plugin_name: "Screenshot Gen".to_string(), + entrypoint_id, + entrypoint_name: gen_name, + action_shortcuts: Default::default(), + }, + true, + ) } } } @@ -709,12 +727,13 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { }); Task::batch([ - state.open_plugin_view(plugin_id, entrypoint_id), + Task::done(AppMsg::OpenPluginView(plugin_id, entrypoint_id)), Task::done(AppMsg::PendingPluginViewLoadingBar), ]) } GlobalState::ErrorView { .. } => Task::none(), GlobalState::PluginView { .. } => Task::none(), + GlobalState::PendingPluginView { .. } => Task::none(), } } AppMsg::OpenGeneratedView { @@ -745,6 +764,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } GlobalState::ErrorView { .. } => Task::none(), GlobalState::PluginView { .. } => Task::none(), + GlobalState::PendingPluginView { .. } => Task::none(), } } AppMsg::RunCommand { @@ -856,6 +876,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } GlobalState::ErrorView { .. } => {} GlobalState::PluginView { .. } => {} + GlobalState::PendingPluginView { .. } => {} } state.search(new_prompt, true) @@ -934,6 +955,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { top_level_view, ..pending_plugin_view_data }, + false, ) } }; @@ -950,6 +972,19 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } + GlobalState::PendingPluginView { + pending_plugin_view_data, + } => { + let pending_plugin_view_data = pending_plugin_view_data.clone(); + GlobalState::plugin( + &mut state.global_state, + PluginViewData { + top_level_view, + ..pending_plugin_view_data + }, + true, + ) + } } } AppMsg::IcedEvent(window_id, Event::Keyboard(event)) => { @@ -1033,226 +1068,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { PluginViewState::ActionPanel { .. } => Task::none(), } } + GlobalState::PendingPluginView { .. } => Task::none(), } } - _ => { - let Physical::Code(physical_key) = physical_key else { - return Task::none(); - }; - - match &mut state.global_state { - GlobalState::MainView { - sub_state, - search_field_id, - focused_search_result, - .. - } => { - match sub_state { - MainViewState::None => { - match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { - physical_key: PhysicalKey::KeyK, - modifier_shift: false, - modifier_control: false, - modifier_alt: true, - modifier_meta: false, - }) => { - Task::perform(async {}, |_| { - AppMsg::ToggleActionPanel { keyboard: true } - }) - } - Some(PhysicalShortcut { - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - }) => { - if modifier_shift - || modifier_control - || modifier_alt - || modifier_meta - { - if let Some(search_item) = - focused_search_result.get(&state.search_results) - { - if search_item.entrypoint_actions.len() > 0 { - state.handle_main_view_keyboard_event( - search_item.plugin_id.clone(), - search_item.entrypoint_id.clone(), - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - ) - } else { - Task::none() - } - } else { - state.handle_inline_plugin_view_keyboard_event( - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - ) - } - } else { - AppModel::append_prompt( - &mut state.prompt, - text, - search_field_id.clone(), - modifiers, - ) - } - } - _ => { - AppModel::append_prompt( - &mut state.prompt, - text, - search_field_id.clone(), - modifiers, - ) - } - } - } - MainViewState::SearchResultActionPanel { .. } => { - match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { - physical_key: PhysicalKey::KeyK, - modifier_shift: false, - modifier_control: false, - modifier_alt: true, - modifier_meta: false, - }) => { - Task::perform(async {}, |_| { - AppMsg::ToggleActionPanel { keyboard: true } - }) - } - Some(PhysicalShortcut { - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - }) => { - if modifier_shift - || modifier_control - || modifier_alt - || modifier_meta - { - if let Some(search_item) = - focused_search_result.get(&state.search_results) - { - if search_item.entrypoint_actions.len() > 0 { - state.handle_main_view_keyboard_event( - search_item.plugin_id.clone(), - search_item.entrypoint_id.clone(), - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - ) - } else { - Task::none() - } - } else { - Task::none() - } - } else { - Task::none() - } - } - _ => Task::none(), - } - } - MainViewState::InlineViewActionPanel { .. } => { - match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { - physical_key: PhysicalKey::KeyK, - modifier_shift: false, - modifier_control: false, - modifier_alt: true, - modifier_meta: false, - }) => { - Task::perform(async {}, |_| { - AppMsg::ToggleActionPanel { keyboard: true } - }) - } - Some(PhysicalShortcut { - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - }) => { - if modifier_shift - || modifier_control - || modifier_alt - || modifier_meta - { - state.handle_inline_plugin_view_keyboard_event( - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - ) - } else { - Task::none() - } - } - _ => Task::none(), - } - } - } - } - GlobalState::ErrorView { .. } => Task::none(), - GlobalState::PluginView { sub_state, .. } => { - match physical_key_model(physical_key, modifiers) { - Some(PhysicalShortcut { - physical_key: PhysicalKey::KeyK, - modifier_shift: false, - modifier_control: false, - modifier_alt: true, - modifier_meta: false, - }) => Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }), - Some(PhysicalShortcut { - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - }) => { - if modifier_shift || modifier_control || modifier_alt || modifier_meta { - state.handle_plugin_view_keyboard_event( - physical_key, - modifier_shift, - modifier_control, - modifier_alt, - modifier_meta, - ) - } else { - match sub_state { - PluginViewState::None => { - match text { - None => Task::none(), - Some(text) => { - state.client_context.append_text(text.as_str()) - } - } - } - PluginViewState::ActionPanel { .. } => Task::none(), - } - } - } - _ => Task::none(), - } - } - } - } + _ => state.handle_shortcut_key(physical_key, modifiers, text), } } _ => Task::none(), @@ -1374,6 +1193,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { GlobalState::PluginView { plugin_view_data, .. } => { plugin_view_data.action_shortcuts = action_shortcuts; } + GlobalState::PendingPluginView { .. } => {} } Task::none() @@ -1455,6 +1275,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { PluginViewState::ActionPanel { .. } => PluginViewState::initial(sub_state), } } + GlobalState::PendingPluginView { .. } => {} } Task::none() @@ -1529,6 +1350,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } GlobalState::PluginView { .. } => Task::none(), GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PendingPluginView { .. } => Task::none(), } } AppMsg::OnAnyActionMainViewNoPanelKeyboardAtIndex { index } => { @@ -1578,6 +1400,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } GlobalState::ErrorView { .. } => Task::none(), GlobalState::PluginView { .. } => Task::none(), + GlobalState::PendingPluginView { .. } => Task::none(), } } AppMsg::OnAnyActionPluginViewAnyPanel { widget_id, id } => { @@ -1612,6 +1435,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } GlobalState::ErrorView { .. } => Task::none(), GlobalState::PluginView { .. } => Task::none(), + GlobalState::PendingPluginView { .. } => Task::none(), } } AppMsg::SetGlobalShortcut { shortcut, responder } => { @@ -1709,6 +1533,51 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } + AppMsg::ShowNewView { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + } => { + Task::batch([ + GlobalState::pending_plugin( + &mut state.global_state, + PluginViewData { + top_level_view: true, + plugin_id: plugin_id.clone(), + plugin_name, + entrypoint_id: entrypoint_id.clone(), + entrypoint_name, + action_shortcuts: HashMap::new(), + }, + ), + Task::done(AppMsg::OpenPluginView(plugin_id, entrypoint_id)), + Task::done(AppMsg::ShowWindow), + ]) + } + AppMsg::ShowNewGeneratedView { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + action_index, + } => { + Task::batch([ + GlobalState::pending_plugin( + &mut state.global_state, + PluginViewData { + top_level_view: true, + plugin_id: plugin_id.clone(), + plugin_name, + entrypoint_id: entrypoint_id.clone(), + entrypoint_name, + action_shortcuts: HashMap::new(), + }, + ), + state.run_generated_entrypoint(plugin_id, entrypoint_id, action_index), + Task::done(AppMsg::ShowWindow), + ]) + } } } @@ -2235,6 +2104,10 @@ fn view_main(state: &AppModel) -> Element<'_, AppMsg> { element } + GlobalState::PendingPluginView { .. } => { + // this should never be shown, but in case it does, do not make it fully transparent + container(horizontal_space()).themed(ContainerStyle::Hud) + } } } @@ -2334,7 +2207,7 @@ impl AppModel { self.focused = false; self.opened = false; - let mut commands = vec![]; + let mut commands = vec![self.reset_window_state()]; #[cfg(target_os = "linux")] if self.wayland { @@ -2368,6 +2241,7 @@ impl AppModel { } GlobalState::MainView { .. } => {} GlobalState::ErrorView { .. } => {} + GlobalState::PendingPluginView { .. } => {} } Task::batch(commands) @@ -2402,7 +2276,7 @@ impl AppModel { window::change_mode(self.main_window_id, Mode::Windowed), ]); - Task::batch([open_task, self.reset_window_state()]) + open_task } fn reset_window_state(&mut self) -> Task { @@ -2669,6 +2543,189 @@ impl AppModel { handle_backend_error(result, |shortcuts| AppMsg::InlineViewShortcuts { shortcuts }) }) } + + fn handle_shortcut_key( + &mut self, + physical_key: Physical, + modifiers: Modifiers, + text: Option, + ) -> Task { + let Physical::Code(physical_key) = physical_key else { + return Task::none(); + }; + + match &mut self.global_state { + GlobalState::MainView { + sub_state, + search_field_id, + focused_search_result, + .. + } => { + match sub_state { + MainViewState::None => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { + physical_key: PhysicalKey::KeyK, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + }) => Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }), + Some(PhysicalShortcut { + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + }) => { + if modifier_shift || modifier_control || modifier_alt || modifier_meta { + if let Some(search_item) = focused_search_result.get(&self.search_results) { + if search_item.entrypoint_actions.len() > 0 { + self.handle_main_view_keyboard_event( + search_item.plugin_id.clone(), + search_item.entrypoint_id.clone(), + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + ) + } else { + Task::none() + } + } else { + self.handle_inline_plugin_view_keyboard_event( + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + ) + } + } else { + AppModel::append_prompt(&mut self.prompt, text, search_field_id.clone(), modifiers) + } + } + _ => AppModel::append_prompt(&mut self.prompt, text, search_field_id.clone(), modifiers), + } + } + MainViewState::SearchResultActionPanel { .. } => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { + physical_key: PhysicalKey::KeyK, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + }) => Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }), + Some(PhysicalShortcut { + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + }) => { + if modifier_shift || modifier_control || modifier_alt || modifier_meta { + if let Some(search_item) = focused_search_result.get(&self.search_results) { + if search_item.entrypoint_actions.len() > 0 { + self.handle_main_view_keyboard_event( + search_item.plugin_id.clone(), + search_item.entrypoint_id.clone(), + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + ) + } else { + Task::none() + } + } else { + Task::none() + } + } else { + Task::none() + } + } + _ => Task::none(), + } + } + MainViewState::InlineViewActionPanel { .. } => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { + physical_key: PhysicalKey::KeyK, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + }) => Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }), + Some(PhysicalShortcut { + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + }) => { + if modifier_shift || modifier_control || modifier_alt || modifier_meta { + self.handle_inline_plugin_view_keyboard_event( + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + ) + } else { + Task::none() + } + } + _ => Task::none(), + } + } + } + } + GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PluginView { sub_state, .. } => { + match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { + physical_key: PhysicalKey::KeyK, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + }) => Task::perform(async {}, |_| AppMsg::ToggleActionPanel { keyboard: true }), + Some(PhysicalShortcut { + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + }) => { + if modifier_shift || modifier_control || modifier_alt || modifier_meta { + self.handle_plugin_view_keyboard_event( + physical_key, + modifier_shift, + modifier_control, + modifier_alt, + modifier_meta, + ) + } else { + match sub_state { + PluginViewState::None => { + match text { + None => Task::none(), + Some(text) => self.client_context.append_text(text.as_str()), + } + } + PluginViewState::ActionPanel { .. } => Task::none(), + } + } + } + _ => Task::none(), + } + } + GlobalState::PendingPluginView { .. } => Task::none(), + } + } } // these are needed to force focus the text_input in main search view when @@ -2828,6 +2885,38 @@ async fn request_loop( AppMsg::SetWindowPositionMode { mode } } + UiRequestData::ShowPluginView { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + } => { + responder.respond(UiResponseData::Nothing); + + AppMsg::ShowNewView { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + } + } + UiRequestData::ShowGeneratedPluginView { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + action_index, + } => { + responder.respond(UiResponseData::Nothing); + + AppMsg::ShowNewGeneratedView { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + action_index, + } + } } }; diff --git a/rust/client/src/ui/state/mod.rs b/rust/client/src/ui/state/mod.rs index c461894..fade508 100644 --- a/rust/client/src/ui/state/mod.rs +++ b/rust/client/src/ui/state/mod.rs @@ -31,6 +31,9 @@ pub enum GlobalState { pending_plugin_view_data: Option, pending_plugin_view_loading_bar: LoadingBarState, }, + PendingPluginView { + pending_plugin_view_data: PluginViewData, + }, ErrorView { error_view: ErrorViewData, }, @@ -38,6 +41,7 @@ pub enum GlobalState { // state plugin_view_data: PluginViewData, sub_state: PluginViewState, + close_window_on_esc: bool, }, } @@ -92,10 +96,16 @@ impl GlobalState { } } - pub fn new_plugin(plugin_view_data: PluginViewData) -> GlobalState { + pub fn new_plugin(plugin_view_data: PluginViewData, close_window_on_esc: bool) -> GlobalState { GlobalState::PluginView { plugin_view_data, sub_state: PluginViewState::new(), + close_window_on_esc, + } + } + pub fn new_pending_plugin(pending_plugin_view_data: PluginViewData) -> GlobalState { + GlobalState::PendingPluginView { + pending_plugin_view_data, } } @@ -113,8 +123,18 @@ impl GlobalState { Task::none() } - pub fn plugin(prev_global_state: &mut GlobalState, plugin_view_data: PluginViewData) -> Task { - *prev_global_state = GlobalState::new_plugin(plugin_view_data); + pub fn plugin( + prev_global_state: &mut GlobalState, + plugin_view_data: PluginViewData, + close_window_on_esc: bool, + ) -> Task { + *prev_global_state = GlobalState::new_plugin(plugin_view_data, close_window_on_esc); + + Task::none() + } + + pub fn pending_plugin(prev_global_state: &mut GlobalState, plugin_view_data: PluginViewData) -> Task { + *prev_global_state = GlobalState::new_pending_plugin(plugin_view_data); Task::none() } @@ -209,6 +229,7 @@ impl Focus for GlobalState { } } GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PendingPluginView { .. } => Task::none(), } } @@ -257,6 +278,7 @@ impl Focus for GlobalState { } } GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PendingPluginView { .. } => Task::none(), } } @@ -284,17 +306,24 @@ impl Focus for GlobalState { .. }, sub_state, - .. + close_window_on_esc, } => { match sub_state { PluginViewState::None => { if *top_level_view { let plugin_id = plugin_id.clone(); - Task::batch([ - Task::done(AppMsg::ClosePluginView(plugin_id)), - GlobalState::initial(self), - ]) + if *close_window_on_esc { + Task::batch([ + Task::done(AppMsg::ClosePluginView(plugin_id)), + Task::done(AppMsg::HideWindow), + ]) + } else { + Task::batch([ + Task::done(AppMsg::ClosePluginView(plugin_id)), + GlobalState::initial(self), + ]) + } } else { let plugin_id = plugin_id.clone(); let entrypoint_id = entrypoint_id.clone(); @@ -305,6 +334,7 @@ impl Focus for GlobalState { } } GlobalState::ErrorView { .. } => Task::done(AppMsg::HideWindow), + GlobalState::PendingPluginView { .. } => Task::none(), } } fn next(&mut self, _client_context: &ClientContext) -> Task { @@ -312,6 +342,7 @@ impl Focus for GlobalState { GlobalState::MainView { .. } => Task::none(), GlobalState::PluginView { .. } => Task::none(), GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PendingPluginView { .. } => Task::none(), } } fn previous(&mut self, _client_context: &ClientContext) -> Task { @@ -319,6 +350,7 @@ impl Focus for GlobalState { GlobalState::MainView { .. } => Task::none(), GlobalState::PluginView { .. } => Task::none(), GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PendingPluginView { .. } => Task::none(), } } fn up(&mut self, client_context: &mut ClientContext, _focus_list: &[SearchResult]) -> Task { @@ -347,6 +379,7 @@ impl Focus for GlobalState { } } } + GlobalState::PendingPluginView { .. } => Task::none(), } } fn down(&mut self, client_context: &mut ClientContext, focus_list: &[SearchResult]) -> Task { @@ -412,6 +445,7 @@ impl Focus for GlobalState { } } } + GlobalState::PendingPluginView { .. } => Task::none(), } } fn left(&mut self, client_context: &mut ClientContext, _focus_list: &[SearchResult]) -> Task { @@ -424,6 +458,7 @@ impl Focus for GlobalState { } GlobalState::MainView { .. } => Task::none(), GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PendingPluginView { .. } => Task::none(), } } fn right(&mut self, client_context: &mut ClientContext, _focus_list: &[SearchResult]) -> Task { @@ -436,6 +471,7 @@ impl Focus for GlobalState { } GlobalState::MainView { .. } => Task::none(), GlobalState::ErrorView { .. } => Task::none(), + GlobalState::PendingPluginView { .. } => Task::none(), } } } diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 8d6208b..e1aeb1f 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -279,6 +279,19 @@ pub enum UiRequestData { SetWindowPositionMode { mode: WindowPositionMode, }, + ShowPluginView { + plugin_id: PluginId, + plugin_name: String, + entrypoint_id: EntrypointId, + entrypoint_name: String, + }, + ShowGeneratedPluginView { + plugin_id: PluginId, + plugin_name: String, + entrypoint_id: EntrypointId, + entrypoint_name: String, + action_index: usize, + }, } #[derive(Debug)] diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index e16cd68..ddcd5bd 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -37,6 +37,7 @@ use crate::rpc::grpc::RpcGetWindowPositionModeRequest; use crate::rpc::grpc::RpcPingRequest; use crate::rpc::grpc::RpcPluginsRequest; use crate::rpc::grpc::RpcRemovePluginRequest; +use crate::rpc::grpc::RpcRunActionRequest; use crate::rpc::grpc::RpcSaveLocalPluginRequest; use crate::rpc::grpc::RpcSetEntrypointStateRequest; use crate::rpc::grpc::RpcSetGlobalShortcutRequest; @@ -306,7 +307,7 @@ impl From for BackendApiError { Code::DeadlineExceeded => BackendApiError::Timeout, _ => { BackendApiError::Internal { - display: format!("{}", error), + display: format!("{}", error.message()), } } } @@ -357,6 +358,24 @@ impl BackendApi { Ok(()) } + pub async fn run_action( + &mut self, + plugin_id: String, + entrypoint_id: String, + action_id: String, + ) -> Result<(), BackendApiError> { + let _ = self + .client + .run_action(Request::new(RpcRunActionRequest { + plugin_id, + entrypoint_id, + action_id, + })) + .await?; + + Ok(()) + } + pub async fn plugins(&mut self) -> Result, BackendApiError> { let plugins = self .client diff --git a/rust/common/src/rpc/backend_server.rs b/rust/common/src/rpc/backend_server.rs index a506759..f03cfc3 100644 --- a/rust/common/src/rpc/backend_server.rs +++ b/rust/common/src/rpc/backend_server.rs @@ -42,6 +42,8 @@ use crate::rpc::grpc::RpcPluginsRequest; use crate::rpc::grpc::RpcPluginsResponse; use crate::rpc::grpc::RpcRemovePluginRequest; use crate::rpc::grpc::RpcRemovePluginResponse; +use crate::rpc::grpc::RpcRunActionRequest; +use crate::rpc::grpc::RpcRunActionResponse; use crate::rpc::grpc::RpcSaveLocalPluginRequest; use crate::rpc::grpc::RpcSaveLocalPluginResponse; use crate::rpc::grpc::RpcSetEntrypointStateRequest; @@ -103,6 +105,13 @@ pub trait BackendServer { async fn show_settings_window(&self) -> anyhow::Result<()>; + async fn run_action( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + action_id: String, + ) -> anyhow::Result<()>; + async fn plugins(&self) -> anyhow::Result>; async fn set_plugin_state(&self, plugin_id: PluginId, enabled: bool) -> anyhow::Result<()>; @@ -173,6 +182,26 @@ impl RpcBackend for RpcBackendServerImpl { Ok(Response::new(RpcShowSettingsWindowResponse::default())) } + async fn run_action( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let plugin_id = request.plugin_id; + let entrypoint_id = request.entrypoint_id; + let action_id = request.action_id; + + let plugin_id = PluginId::from_string(plugin_id); + let entrypoint_id = EntrypointId::from_string(entrypoint_id); + + self.server + .run_action(plugin_id, entrypoint_id, action_id) + .await + .map_err(|err| Status::internal(format!("{:#}", err)))?; + + Ok(Response::new(RpcRunActionResponse::default())) + } + async fn plugins(&self, _: Request) -> Result, Status> { let plugins = self .server diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index 7f0cc73..3c65ea2 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -220,4 +220,46 @@ impl FrontendApi { UiResponseData::Err(err) => Err(err), } } + + pub async fn open_generated_plugin_view( + &self, + plugin_id: PluginId, + plugin_name: String, + entrypoint_id: EntrypointId, + entrypoint_name: String, + action_index: usize, + ) -> Result<(), FrontendApiError> { + let data = UiRequestData::ShowGeneratedPluginView { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + action_index, + }; + let UiResponseData::Nothing = self.frontend_sender.send_receive(data).await? else { + unreachable!() + }; + + Ok(()) + } + + pub async fn open_plugin_view( + &self, + plugin_id: PluginId, + plugin_name: String, + entrypoint_id: EntrypointId, + entrypoint_name: String, + ) -> Result<(), FrontendApiError> { + let data = UiRequestData::ShowPluginView { + plugin_id, + plugin_name, + entrypoint_id, + entrypoint_name, + }; + let UiResponseData::Nothing = self.frontend_sender.send_receive(data).await? else { + unreachable!() + }; + + Ok(()) + } } diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index b601be2..6ea48bf 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -12,9 +12,12 @@ use gauntlet_client::start_client; use gauntlet_common::dirs::Dirs; use gauntlet_common::model::BackendRequestData; use gauntlet_common::model::BackendResponseData; +use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::PluginId; use gauntlet_common::model::UiRequestData; use gauntlet_common::model::UiResponseData; use gauntlet_common::rpc::backend_api::BackendApi; +use gauntlet_common::rpc::backend_api::BackendApiError; use gauntlet_common::rpc::backend_server::start_backend_server; use gauntlet_common::settings_env_data_from_string; use gauntlet_common::settings_env_data_to_string; @@ -74,6 +77,34 @@ pub fn start(minimized: bool) { } } +pub fn run_action(plugin_id: String, entrypoint_id: String, action_id: String) { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("unable to start server tokio runtime") + .block_on(async { + let result = BackendApi::new().await; + + match result { + Ok(mut backend_api) => { + if let Err(err) = backend_api.run_action(plugin_id, entrypoint_id, action_id).await { + match err { + BackendApiError::Timeout => { + tracing::error!("Timeout occurred when handling command"); + } + BackendApiError::Internal { display: value } => { + tracing::error!("Error occurred when handling command: {}", value); + } + } + } + } + Err(_) => { + tracing::error!("Unable to connect to server. Please check if you have Gauntlet running on your PC") + } + } + }) +} + #[cfg(feature = "scenario_runner")] fn run_scenario_runner() { let runner_type = diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 909899a..2615a0f 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -862,6 +862,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { }; SearchIndexItemAction { + id: action.id.clone(), label: action.label.clone(), action_type: match action.action_type { JsGeneratedSearchItemActionType::View => SearchIndexItemActionActionType::View, diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 03f2d56..8538a46 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -1,10 +1,12 @@ use std::cell::RefCell; use std::collections::HashMap; +use std::ops::Index; use std::sync::Mutex; use std::thread; use std::time::Duration; use anyhow::anyhow; +use anyhow::Context; use gauntlet_common::dirs::Dirs; use gauntlet_common::model::DownloadStatus; use gauntlet_common::model::EntrypointId; @@ -17,6 +19,7 @@ use gauntlet_common::model::PluginPreference; use gauntlet_common::model::PluginPreferenceUserData; use gauntlet_common::model::PreferenceEnumValue; use gauntlet_common::model::SearchResult; +use gauntlet_common::model::SearchResultEntrypointType; use gauntlet_common::model::SettingsEntrypoint; use gauntlet_common::model::SettingsEntrypointType; use gauntlet_common::model::SettingsPlugin; @@ -64,6 +67,10 @@ use crate::plugins::js::PluginRuntimeData; use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; use crate::plugins::settings::Settings; +use crate::search::EntrypointActionDataView; +use crate::search::EntrypointActionType; +use crate::search::EntrypointDataView; +use crate::search::PluginDataView; use crate::search::SearchIndex; mod clipboard; @@ -181,6 +188,161 @@ impl ApplicationManager { Ok(()) } + pub async fn run_action( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + action_id: String, + ) -> anyhow::Result<()> { + let data = self.search_index.plugin_entrypoint_actions(); + + let Some(data) = data.get(&plugin_id) else { + return Err(anyhow!("Unable to find plugin with id: {}", plugin_id)); + }; + + let PluginDataView { + plugin_name, + entrypoints, + } = data; + + let Some(entrypoint_data) = &entrypoints.get(&entrypoint_id) else { + return Err(anyhow!("Unable to find entrypoint with id: {}", entrypoint_id)); + }; + + let EntrypointDataView { + entrypoint_name, + entrypoint_type, + actions, + .. + } = entrypoint_data; + + match action_id.as_str() { + ":primary" => { + match entrypoint_type { + SearchResultEntrypointType::Command => { + self.handle_run_command(plugin_id, entrypoint_id).await; + } + SearchResultEntrypointType::View => { + self.frontend_api + .open_plugin_view( + plugin_id, + plugin_name.to_string(), + entrypoint_id, + entrypoint_name.to_string(), + ) + .await?; + } + SearchResultEntrypointType::Generated => { + let Some(action_data) = actions.get(0) else { + return Err(anyhow!("Requested entrypoint doesn't provide primary action")); + }; + + let EntrypointActionDataView { action_type, .. } = action_data; + + match action_type { + EntrypointActionType::Command => { + self.handle_run_generated_entrypoint(plugin_id, entrypoint_id, 0).await; + } + EntrypointActionType::View => { + self.frontend_api + .open_generated_plugin_view( + plugin_id, + plugin_name.to_string(), + entrypoint_id, + entrypoint_name.to_string(), + 0, + ) + .await?; + } + } + } + } + } + ":secondary" => { + match entrypoint_type { + SearchResultEntrypointType::Command => { + return Err(anyhow!("Command entrypoints support only ':primary' action")); + } + SearchResultEntrypointType::View => { + return Err(anyhow!("View entrypoints support only ':primary' action")); + } + SearchResultEntrypointType::Generated => { + let Some(action_data) = actions.get(1) else { + return Err(anyhow!("Requested entrypoint doesn't provide secondary action")); + }; + + let EntrypointActionDataView { action_type, .. } = action_data; + + match action_type { + EntrypointActionType::Command => { + self.handle_run_generated_entrypoint(plugin_id, entrypoint_id, 1).await; + } + EntrypointActionType::View => { + self.frontend_api + .open_generated_plugin_view( + plugin_id, + plugin_name.to_string(), + entrypoint_id, + entrypoint_name.to_string(), + 1, + ) + .await?; + } + } + } + } + } + action_id @ _ => { + match entrypoint_type { + SearchResultEntrypointType::Command => { + return Err(anyhow!("Command entrypoints support only ':primary' action")); + } + SearchResultEntrypointType::View => { + return Err(anyhow!("View entrypoints support only ':primary' action")); + } + SearchResultEntrypointType::Generated => { + let index = entrypoint_data + .actions + .iter() + .position(|data| &data.id == &Some(action_id.to_string())); + + match index { + None => { + return Err(anyhow!( + "Requested entrypoint doesn't provide action with id: {}", + action_id + )); + } + Some(index) => { + let action_data = &entrypoint_data.actions[index]; + + match action_data.action_type { + EntrypointActionType::Command => { + self.handle_run_generated_entrypoint(plugin_id, entrypoint_id, index) + .await; + } + EntrypointActionType::View => { + self.frontend_api + .open_generated_plugin_view( + plugin_id, + plugin_name.to_string(), + entrypoint_id, + entrypoint_name.to_string(), + index, + ) + .await?; + } + } + } + } + } + } + } + } + + Ok(()) + } + pub async fn save_local_plugin(&self, path: &str) -> anyhow::Result { tracing::info!(target = "plugin", "Saving local plugin at path: {:?}", path); diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index 4439ca8..243de31 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -44,6 +44,12 @@ impl BackendServer for BackendServerImpl { Ok(()) } + async fn run_action(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_id: String) -> anyhow::Result<()> { + self.application_manager.run_action(plugin_id, entrypoint_id, action_id).await?; + + Ok(()) + } + async fn plugins(&self) -> anyhow::Result> { let result = self.application_manager.plugins().await; diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index 78f9cd9..95219bb 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -2,6 +2,7 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::sync::Arc; use std::sync::Mutex; +use std::sync::MutexGuard; use gauntlet_common::model::EntrypointId; use gauntlet_common::model::PhysicalShortcut; @@ -35,7 +36,7 @@ pub struct SearchIndex { index_reader: IndexReader, index_writer_mutex: Arc>, - entrypoint_data: Arc>>>, + entrypoint_data: Arc>>, entrypoint_name: Field, entrypoint_id: Field, @@ -43,7 +44,13 @@ pub struct SearchIndex { plugin_id: Field, } +struct PluginData { + plugin_name: String, + entrypoints: HashMap, +} + struct EntrypointData { + entrypoint_name: String, entrypoint_generator_name: Option, entrypoint_type: SearchResultEntrypointType, icon: Option, @@ -53,16 +60,37 @@ struct EntrypointData { } struct EntrypointActionData { + id: Option, label: String, action_type: EntrypointActionType, shortcut: Option, } -enum EntrypointActionType { +#[derive(Clone, Copy)] +pub enum EntrypointActionType { Command, View, } +pub struct PluginDataView { + pub plugin_name: String, + pub entrypoints: HashMap, +} + +pub struct EntrypointDataView { + pub entrypoint_name: String, + pub entrypoint_generator_name: Option, + pub entrypoint_type: SearchResultEntrypointType, + pub actions: Vec, +} + +pub struct EntrypointActionDataView { + pub id: Option, + pub label: String, + pub action_type: EntrypointActionType, + pub shortcut: Option, +} + #[derive(Clone, Debug)] pub struct SearchIndexItem { pub entrypoint_type: SearchResultEntrypointType, @@ -77,6 +105,7 @@ pub struct SearchIndexItem { #[derive(Clone, Debug)] pub struct SearchIndexItemAction { + pub id: Option, pub label: String, pub action_type: SearchIndexItemActionActionType, pub shortcut: Option, @@ -186,6 +215,7 @@ impl SearchIndex { .into_iter() .map(|action| { EntrypointActionData { + id: action.id, label: action.label, action_type: match action.action_type { SearchIndexItemActionActionType::Command => EntrypointActionType::Command, @@ -197,6 +227,7 @@ impl SearchIndex { .collect(); let data = EntrypointData { + entrypoint_name: item.entrypoint_name, entrypoint_generator_name: item.entrypoint_generator_name, entrypoint_type: item.entrypoint_type, icon: item.entrypoint_icon, @@ -209,7 +240,13 @@ impl SearchIndex { }) .collect(); - entrypoint_data.insert(plugin_id.clone(), data); + entrypoint_data.insert( + plugin_id.clone(), + PluginData { + plugin_name, + entrypoints: data, + }, + ); if refresh_search_list { let mut frontend_api = self.frontend_api.clone(); @@ -230,6 +267,52 @@ impl SearchIndex { Ok(()) } + pub fn plugin_entrypoint_actions(&self) -> HashMap { + let entrypoint_data = self.entrypoint_data.lock().expect("lock is poisoned"); + + entrypoint_data + .iter() + .map(|(plugin_id, data)| { + let entrypoints = data + .entrypoints + .iter() + .map(|(entrypoint_id, data)| { + let actions = data + .actions + .iter() + .map(|data| { + EntrypointActionDataView { + id: data.id.clone(), + label: data.label.clone(), + action_type: data.action_type, + shortcut: data.shortcut.clone(), + } + }) + .collect(); + + ( + entrypoint_id.clone(), + EntrypointDataView { + entrypoint_name: data.entrypoint_name.clone(), + entrypoint_generator_name: data.entrypoint_generator_name.clone(), + entrypoint_type: data.entrypoint_type.clone(), + actions, + }, + ) + }) + .collect(); + + ( + plugin_id.clone(), + PluginDataView { + plugin_name: data.plugin_name.clone(), + entrypoints, + }, + ) + }) + .collect() + } + pub fn search(&self, query: &str) -> anyhow::Result> { let entrypoint_data = self.entrypoint_data.lock().expect("lock is poisoned"); @@ -278,7 +361,7 @@ impl SearchIndex { fn fetch( &self, - entrypoint_data: &HashMap>, + entrypoint_data: &HashMap, query: &dyn Query, collector: TopDocs, searcher: &Searcher, @@ -318,6 +401,7 @@ impl SearchIndex { let entrypoint_data = entrypoint_data .get(&plugin_id) .expect("Plugin should always exist in entrypoint data") + .entrypoints .get(&entrypoint_id) .expect("Entrypoint should always exist in plugin in entrypoint data"); diff --git a/schema/backend.proto b/schema/backend.proto index fc6954c..1404a2f 100644 --- a/schema/backend.proto +++ b/schema/backend.proto @@ -9,6 +9,7 @@ service RpcBackend { // cli rpc ShowWindow (RpcShowWindowRequest) returns (RpcShowWindowResponse); rpc ShowSettingsWindow (RpcShowSettingsWindowRequest) returns (RpcShowSettingsWindowResponse); + rpc RunAction (RpcRunActionRequest) returns (RpcRunActionResponse); // settings rpc Plugins (RpcPluginsRequest) returns (RpcPluginsResponse); @@ -48,6 +49,14 @@ message RpcShowSettingsWindowRequest { message RpcShowSettingsWindowResponse { } +message RpcRunActionRequest { + string plugin_id = 1; + string entrypoint_id = 2; + string action_id = 3; +} +message RpcRunActionResponse { +} + message RpcPingRequest { } message RpcPingResponse { From ff1c1b8754c69cda8cf21f9c6703446f64c595de Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 9 Feb 2025 14:03:31 +0100 Subject: [PATCH 340/540] Fix formatting --- rust/server/src/rpc.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index 243de31..179c635 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -44,8 +44,15 @@ impl BackendServer for BackendServerImpl { Ok(()) } - async fn run_action(&self, plugin_id: PluginId, entrypoint_id: EntrypointId, action_id: String) -> anyhow::Result<()> { - self.application_manager.run_action(plugin_id, entrypoint_id, action_id).await?; + async fn run_action( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + action_id: String, + ) -> anyhow::Result<()> { + self.application_manager + .run_action(plugin_id, entrypoint_id, action_id) + .await?; Ok(()) } From 5dade37cb4cd6ff11b64e6ab977eecf3847c3036 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 10 Feb 2025 20:58:35 +0100 Subject: [PATCH 341/540] Upload artifacts to github actions workflow after building --- .github/workflows/build.yaml | 5 +++++ .github/workflows/setup-linux.yaml | 11 +++++++++++ .github/workflows/setup-macos.yaml | 11 +++++++++++ .github/workflows/setup-windows.yaml | 12 ++++++++++++ CHANGELOG.md | 2 +- 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4d52eea..19aa618 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -11,14 +11,19 @@ jobs: uses: ./.github/workflows/setup-linux.yaml with: command: npm run build-linux-project --workspace @project-gauntlet/build + upload-artifact: 'true' + secrets: inherit build-macos: uses: ./.github/workflows/setup-macos.yaml with: command: npm run build-macos-project --workspace @project-gauntlet/build + upload-artifact: 'true' secrets: inherit build-windows: uses: ./.github/workflows/setup-windows.yaml with: command: npm run build-windows-project --workspace @project-gauntlet/build + upload-artifact: 'true' + secrets: inherit diff --git a/.github/workflows/setup-linux.yaml b/.github/workflows/setup-linux.yaml index a02813a..1da039b 100644 --- a/.github/workflows/setup-linux.yaml +++ b/.github/workflows/setup-linux.yaml @@ -6,6 +6,9 @@ on: command: required: true type: string + upload-artifact: + default: false + type: boolean github-release-id: type: string @@ -40,3 +43,11 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} GITHUB_RELEASE_ID: ${{ inputs.github-release-id }} + + - uses: actions/upload-artifact@v4 + if: ${{ inputs.upload-artifact }} + with: + name: 'gauntlet-x86_64-linux.tar.gz' + path: 'target/x86_64-unknown-linux-gnu/release/archive/gauntlet-x86_64-linux.tar.gz' + if-no-files-found: 'error' + retention-days: 7 diff --git a/.github/workflows/setup-macos.yaml b/.github/workflows/setup-macos.yaml index 98104f3..09eb52e 100644 --- a/.github/workflows/setup-macos.yaml +++ b/.github/workflows/setup-macos.yaml @@ -6,6 +6,9 @@ on: command: required: true type: string + upload-artifact: + default: false + type: boolean github-release-id: type: string @@ -52,3 +55,11 @@ jobs: APPLE_SIGNING_KEY_PEM: ${{ secrets.APPLE_SIGNING_KEY_PEM }} APPLE_SIGNING_CERT_PEM: ${{ secrets.APPLE_SIGNING_CERT_PEM }} APP_STORE_CONNECT_KEY: ${{ secrets.APP_STORE_CONNECT_KEY }} + + - uses: actions/upload-artifact@v4 + if: ${{ inputs.upload-artifact }} + with: + name: 'gauntlet-aarch64-macos.dmg' + path: 'target/aarch64-apple-darwin/release/gauntlet-aarch64-macos.dmg' + if-no-files-found: 'error' + retention-days: 7 diff --git a/.github/workflows/setup-windows.yaml b/.github/workflows/setup-windows.yaml index 380d448..7ea891d 100644 --- a/.github/workflows/setup-windows.yaml +++ b/.github/workflows/setup-windows.yaml @@ -6,6 +6,9 @@ on: command: required: true type: string + upload-artifact: + default: false + type: boolean github-release-id: type: string @@ -38,3 +41,12 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} GITHUB_RELEASE_ID: ${{ inputs.github-release-id }} + + - uses: actions/upload-artifact@v4 + if: ${{ inputs.upload-artifact }} + with: + name: 'gauntlet-x86_64-windows.msi' + path: 'target/x86_64-pc-windows-msvc/release/gauntlet-x86_64-windows.msi' + if-no-files-found: 'error' + retention-days: 7 + diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c158c2..e3823fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git - For entrypoint types `command` and `view` - in Plugin Manifest or in Settings UI TODO - For entrypoint type `entrypoint-generator` - in Settings UI TODO - Action ID can also be found in Plugin Manifest - - Action ID option also supports special values + - Action ID option also accepts special values - `:primary` - to run primary action of the entrypoint - `:secondary` - to run secondary action of the entrypoint - Slightly improved --help documentation of CLI command From 8c017baeddbd3af85f7791d97c3620887dd83ee4 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 10 Feb 2025 21:00:54 +0100 Subject: [PATCH 342/540] Try fix github workflow --- .github/workflows/build.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 19aa618..e9364a7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -11,19 +11,19 @@ jobs: uses: ./.github/workflows/setup-linux.yaml with: command: npm run build-linux-project --workspace @project-gauntlet/build - upload-artifact: 'true' + upload-artifact: true secrets: inherit build-macos: uses: ./.github/workflows/setup-macos.yaml with: command: npm run build-macos-project --workspace @project-gauntlet/build - upload-artifact: 'true' + upload-artifact: true secrets: inherit build-windows: uses: ./.github/workflows/setup-windows.yaml with: command: npm run build-windows-project --workspace @project-gauntlet/build - upload-artifact: 'true' + upload-artifact: true secrets: inherit From c8b3cfe09cb462a7b7e73165ba13d41e572fbe66 Mon Sep 17 00:00:00 2001 From: Benno <107504783+bennocrafter@users.noreply.github.com> Date: Tue, 11 Feb 2025 20:12:35 +0100 Subject: [PATCH 343/540] Global shortcut now hides the main window when the window is already open --- .gitignore | 1 + CHANGELOG.md | 1 + rust/client/src/global_shortcut.rs | 2 +- rust/client/src/ui/mod.rs | 10 ++++++++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ebe541e..8d7a724 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /node_modules /.direnv result +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index e3823fa..9de78aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Global shortcut how hides the main window if it is already open - It is now possible to run commands and open views using CLI command - Format: `gauntlet run ` - Plugin ID can be found in Settings UI diff --git a/rust/client/src/global_shortcut.rs b/rust/client/src/global_shortcut.rs index 02e9707..32bad0d 100644 --- a/rust/client/src/global_shortcut.rs +++ b/rust/client/src/global_shortcut.rs @@ -17,7 +17,7 @@ pub fn register_listener(msg_sender: Sender) { if let global_hotkey::HotKeyState::Released = e.state() { handle.spawn(async move { - if let Err(err) = msg_sender.send(AppMsg::ShowWindow).await { + if let Err(err) = msg_sender.send(AppMsg::ToggleWindowFocus).await { tracing::warn!(target = "rpc", "error occurred when receiving shortcut event {:?}", err) } }); diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index bc9cceb..cf82f2d 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -245,6 +245,7 @@ pub enum AppMsg { FontLoaded(Result<(), font::Error>), ShowWindow, HideWindow, + ToggleWindowFocus, ToggleActionPanel { keyboard: bool, }, @@ -1132,6 +1133,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { result.expect("unable to load font"); Task::none() } + AppMsg::ToggleWindowFocus => state.toggle_window_focus(), AppMsg::ShowWindow => state.show_window(), AppMsg::HideWindow => state.hide_window(), AppMsg::ShowPreferenceRequiredView { @@ -2203,6 +2205,14 @@ impl AppModel { } } + fn toggle_window_focus(&mut self) -> Task { + if self.focused { + self.hide_window() + } else { + self.show_window() + } + } + fn hide_window(&mut self) -> Task { self.focused = false; self.opened = false; From fcbeb206994bed85e7bf00b570ed32c8dd6e02ce Mon Sep 17 00:00:00 2001 From: Benno <107504783+bennocrafter@users.noreply.github.com> Date: Tue, 11 Feb 2025 20:22:36 +0100 Subject: [PATCH 344/540] Add cmd/ctrl+comma to open settings ui --- CHANGELOG.md | 3 +++ rust/client/src/ui/mod.rs | 11 +++++++++++ rust/client/src/ui/sys_tray.rs | 10 +++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9de78aa..6d99b27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Add shortcut to open Settings UI + - Ctrl + , on Windows and Linux + - Cmd + , on macOS - Global shortcut how hides the main window if it is already open - It is now possible to run commands and open views using CLI command - Format: `gauntlet run ` diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index cf82f2d..7e11ecf 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -2574,6 +2574,17 @@ impl AppModel { match sub_state { MainViewState::None => { match physical_key_model(physical_key, modifiers) { + Some(PhysicalShortcut { + physical_key: PhysicalKey::Comma, + modifier_shift: false, + modifier_control: cfg!(any(target_os = "linux", target_os = "windows")), + modifier_alt: false, + modifier_meta: cfg!(target_os = "macos"), + }) => { + crate::open_settings_window(); + + Task::none() + } Some(PhysicalShortcut { physical_key: PhysicalKey::KeyK, modifier_shift: false, diff --git a/rust/client/src/ui/sys_tray.rs b/rust/client/src/ui/sys_tray.rs index 9a6cebe..ccd4f5e 100644 --- a/rust/client/src/ui/sys_tray.rs +++ b/rust/client/src/ui/sys_tray.rs @@ -1,6 +1,9 @@ use image::ImageFormat; pub fn create_tray() -> tray_icon::TrayIcon { + use global_hotkey::hotkey::Code; + use global_hotkey::hotkey::CMD_OR_CTRL; + use tray_icon::menu::accelerator::Accelerator; use tray_icon::menu::AboutMetadataBuilder; use tray_icon::menu::Menu; use tray_icon::menu::MenuEvent; @@ -49,7 +52,12 @@ pub fn create_tray() -> tray_icon::TrayIcon { let menu = Menu::with_items(&[ &MenuItem::new("Gauntlet", false, None), &MenuItem::with_id("GAUNTLET_OPEN_MAIN_WINDOW", "Open", true, None), - &MenuItem::with_id("GAUNTLET_OPEN_SETTING_WINDOW", "Open Settings", true, None), + &MenuItem::with_id( + "GAUNTLET_OPEN_SETTING_WINDOW", + "Open Settings", + true, + Some(Accelerator::new(Some(CMD_OR_CTRL), Code::Comma)), + ), &PredefinedMenuItem::separator(), &PredefinedMenuItem::about(Some("About..."), Some(about_metadata)), &PredefinedMenuItem::quit(Some("Quit Gauntlet")), From 6be4c2aeae49afcaeaf7ac1956859be923b7cebb Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 11 Feb 2025 20:51:51 +0100 Subject: [PATCH 345/540] When using active screen setting for window positioning, position calculated is now relative to size of the screen --- CHANGELOG.md | 5 +++-- Cargo.lock | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d99b27..83f9314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,11 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] -- Add shortcut to open Settings UI +- When using active screen setting for window positioning, position calculated is now relative to size of the screen. Fixes unexpected position when using monitors of different size (contributed by @BennoCrafter) +- Add shortcut to open Settings UI (contributed by @BennoCrafter) - Ctrl + , on Windows and Linux - Cmd + , on macOS -- Global shortcut how hides the main window if it is already open +- Global shortcut how hides the main window if it is already open (contributed by @BennoCrafter) - It is now possible to run commands and open views using CLI command - Format: `gauntlet run ` - Plugin ID can be found in Settings UI diff --git a/Cargo.lock b/Cargo.lock index c5d87f8..751c367 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5136,7 +5136,7 @@ dependencies = [ [[package]] name = "iced" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#b6fbbc062c4b007acdcd90cbf498a2c3ece9ae25" dependencies = [ "iced_core", "iced_futures", @@ -5164,7 +5164,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#b6fbbc062c4b007acdcd90cbf498a2c3ece9ae25" dependencies = [ "bitflags 2.6.0", "bytes", @@ -5191,7 +5191,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#b6fbbc062c4b007acdcd90cbf498a2c3ece9ae25" dependencies = [ "futures", "iced_core", @@ -5205,7 +5205,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#b6fbbc062c4b007acdcd90cbf498a2c3ece9ae25" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5257,7 +5257,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#b6fbbc062c4b007acdcd90cbf498a2c3ece9ae25" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -5269,7 +5269,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#b6fbbc062c4b007acdcd90cbf498a2c3ece9ae25" dependencies = [ "bytes", "iced_core", @@ -5290,7 +5290,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#b6fbbc062c4b007acdcd90cbf498a2c3ece9ae25" dependencies = [ "bytemuck", "cosmic-text", @@ -5305,7 +5305,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#b6fbbc062c4b007acdcd90cbf498a2c3ece9ae25" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -5324,7 +5324,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#b6fbbc062c4b007acdcd90cbf498a2c3ece9ae25" dependencies = [ "iced_renderer", "iced_runtime", @@ -5338,7 +5338,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.13.99" -source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#e97b4e10768225c3a38a417cb6185d5ec096ca8d" +source = "git+https://github.com/project-gauntlet/iced.git?branch=gauntlet-0.13#b6fbbc062c4b007acdcd90cbf498a2c3ece9ae25" dependencies = [ "iced_futures", "iced_graphics", From 7ce6844ad0ec952df7e54575822ac2bd83ab5dee Mon Sep 17 00:00:00 2001 From: Benno <107504783+BennoCrafter@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:09:52 +0100 Subject: [PATCH 346/540] feat: auto scroll to top in window hiding process --- rust/client/src/ui/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 7e11ecf..1b52386 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -2249,7 +2249,11 @@ impl AppModel { } => { commands.push(self.close_plugin_view(plugin_id.clone())); } - GlobalState::MainView { .. } => {} + GlobalState::MainView { + focused_search_result, .. + } => { + commands.push(focused_search_result.scroll_to(0)); + } GlobalState::ErrorView { .. } => {} GlobalState::PendingPluginView { .. } => {} } From 89c28bae6c51f82fe03e57f5a85d9b8e5220b299 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Feb 2025 08:20:52 +0100 Subject: [PATCH 347/540] Change global shortcut to be executed on key down instead of key up --- CHANGELOG.md | 1 + rust/client/src/global_shortcut.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f9314..dadc6b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Global shortcut is now executed on key press, instead of key release - When using active screen setting for window positioning, position calculated is now relative to size of the screen. Fixes unexpected position when using monitors of different size (contributed by @BennoCrafter) - Add shortcut to open Settings UI (contributed by @BennoCrafter) - Ctrl + , on Windows and Linux diff --git a/rust/client/src/global_shortcut.rs b/rust/client/src/global_shortcut.rs index 32bad0d..5dfece8 100644 --- a/rust/client/src/global_shortcut.rs +++ b/rust/client/src/global_shortcut.rs @@ -15,7 +15,7 @@ pub fn register_listener(msg_sender: Sender) { global_hotkey::GlobalHotKeyEvent::set_event_handler(Some(move |e: global_hotkey::GlobalHotKeyEvent| { let mut msg_sender = msg_sender.clone(); - if let global_hotkey::HotKeyState::Released = e.state() { + if let global_hotkey::HotKeyState::Pressed = e.state() { handle.spawn(async move { if let Err(err) = msg_sender.send(AppMsg::ToggleWindowFocus).await { tracing::warn!(target = "rpc", "error occurred when receiving shortcut event {:?}", err) From b542633cb6a1490b838cedfe26ef59fa29d3892d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Feb 2025 09:29:54 +0100 Subject: [PATCH 348/540] On X11 listen on _NET_ACTIVE_WINDOW change instead of unfocus event for close-on-unfocus behaviour --- CHANGELOG.md | 1 + Cargo.lock | 1 + rust/client/Cargo.toml | 1 + rust/client/src/global_shortcut.rs | 2 +- rust/client/src/ui/mod.rs | 74 ++++++++++++++++++++++++--- rust/client/src/ui/platform/linux.rs | 76 ++++++++++++++++++++++++++++ rust/client/src/ui/platform/mod.rs | 2 + rust/plugin_runtime/Cargo.toml | 4 +- 8 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 rust/client/src/ui/platform/linux.rs create mode 100644 rust/client/src/ui/platform/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index dadc6b0..c91b651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Slightly improved close-on-unfocus behaviour of main window on X11 - Global shortcut is now executed on key press, instead of key release - When using active screen setting for window positioning, position calculated is now relative to size of the screen. Fixes unexpected position when using monitors of different size (contributed by @BennoCrafter) - Add shortcut to open Settings UI (contributed by @BennoCrafter) diff --git a/Cargo.lock b/Cargo.lock index 751c367..d59ade2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4012,6 +4012,7 @@ dependencies = [ "tokio", "tracing", "tray-icon", + "x11rb", ] [[package]] diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index 53b072d..b2fdd0c 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -31,6 +31,7 @@ tray-icon = { version = "0.19.2", default-features = false } [target.'cfg(target_os = "linux")'.dependencies] iced_layershell.workspace = true +x11rb = { version = "0.13", features = ["extra-traits"] } [target.'cfg(target_os = "macos")'.dependencies] objc2-app-kit = { version = "0.2.2", features = ["NSWorkspace"] } diff --git a/rust/client/src/global_shortcut.rs b/rust/client/src/global_shortcut.rs index 5dfece8..7d253b1 100644 --- a/rust/client/src/global_shortcut.rs +++ b/rust/client/src/global_shortcut.rs @@ -17,7 +17,7 @@ pub fn register_listener(msg_sender: Sender) { if let global_hotkey::HotKeyState::Pressed = e.state() { handle.spawn(async move { - if let Err(err) = msg_sender.send(AppMsg::ToggleWindowFocus).await { + if let Err(err) = msg_sender.send(AppMsg::ToggleWindow).await { tracing::warn!(target = "rpc", "error occurred when receiving shortcut event {:?}", err) } }); diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 1b52386..d3efae3 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -93,6 +93,7 @@ use iced::Subscription; use iced::Task; use iced_fonts::BOOTSTRAP_FONT_BYTES; use serde::Deserialize; +use tokio::runtime::Handle; use tokio::sync::Mutex as TokioMutex; use tokio::sync::RwLock as TokioRwLock; @@ -121,12 +122,16 @@ mod theme; mod widget; mod widget_container; +mod platform; + pub use theme::GauntletComplexTheme; use crate::global_shortcut::convert_physical_shortcut_to_hotkey; use crate::global_shortcut::register_listener; use crate::ui::custom_widgets::loading_bar::LoadingBar; use crate::ui::hud::show_hud_window; +#[cfg(target_os = "linux")] +use crate::ui::platform::linux::listen_on_x11_active_window_change; use crate::ui::scroll_handle::ScrollHandle; use crate::ui::state::ErrorViewData; use crate::ui::state::Focus; @@ -153,6 +158,8 @@ pub struct AppModel { window_position_mode: WindowPositionMode, close_on_unfocus: bool, window_position_file: PathBuf, + #[cfg(target_os = "linux")] + x11_active_window: Option, // ephemeral state prompt: String, @@ -245,7 +252,7 @@ pub enum AppMsg { FontLoaded(Result<(), font::Error>), ShowWindow, HideWindow, - ToggleWindowFocus, + ToggleWindow, ToggleActionPanel { keyboard: bool, }, @@ -345,6 +352,10 @@ pub enum AppMsg { SetWindowPositionMode { mode: WindowPositionMode, }, + #[cfg(target_os = "linux")] + X11ActiveWindowChanged { + window: u32, + }, } #[cfg(target_os = "linux")] @@ -372,6 +383,11 @@ fn window_settings(visible: bool, position: Position) -> window::Settings { transparent: true, closeable: false, minimizable: false, + #[cfg(target_os = "linux")] + platform_specific: window::settings::PlatformSpecific { + application_id: "gauntlet".to_string(), + ..Default::default() + }, ..Default::default() } } @@ -682,6 +698,8 @@ fn new( window_position_mode: setup_data.window_position_mode, close_on_unfocus: setup_data.close_on_unfocus, window_position_file: setup_data.window_position_file, + #[cfg(target_os = "linux")] + x11_active_window: None, // ephemeral state prompt: "".to_string(), @@ -1098,11 +1116,16 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { return Task::none(); } + #[cfg(target_os = "linux")] if state.wayland { state.hide_window() } else { - state.on_unfocused() + // x11 uses separate mechanism based on _NET_ACTIVE_WINDOW property + Task::none() } + + #[cfg(not(target_os = "linux"))] + state.on_unfocused() } AppMsg::IcedEvent(window_id, Event::Window(window::Event::Moved(point))) => { if window_id != state.main_window_id { @@ -1133,7 +1156,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { result.expect("unable to load font"); Task::none() } - AppMsg::ToggleWindowFocus => state.toggle_window_focus(), + AppMsg::ToggleWindow => state.toggle_window(), AppMsg::ShowWindow => state.show_window(), AppMsg::HideWindow => state.hide_window(), AppMsg::ShowPreferenceRequiredView { @@ -1580,6 +1603,14 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::done(AppMsg::ShowWindow), ]) } + AppMsg::X11ActiveWindowChanged { window } => { + if state.x11_active_window != Some(window) { + state.x11_active_window = Some(window); + Task::done(AppMsg::HideWindow) + } else { + Task::none() + } + } } } @@ -2118,6 +2149,7 @@ fn subscription(state: &AppModel) -> Subscription { struct RequestLoop; struct GlobalShortcutListener; + struct X11ActiveWindowListener; let events_subscription = event::listen_with(|event, status, window_id| { match status { @@ -2134,7 +2166,7 @@ fn subscription(state: &AppModel) -> Subscription { } }); - Subscription::batch([ + let mut subscriptions = vec![ Subscription::run_with_id( std::any::TypeId::of::(), stream::channel(10, |sender| { @@ -2158,7 +2190,29 @@ fn subscription(state: &AppModel) -> Subscription { } }), ), - ]) + ]; + + #[cfg(target_os = "linux")] + if !state.wayland { + let handle = Handle::current(); + + let subscription = Subscription::run_with_id( + std::any::TypeId::of::(), + stream::channel(100, |sender| { + async move { + let err = tokio::task::spawn_blocking(|| listen_on_x11_active_window_change(sender, handle)).await; + + if let Err(err) = err { + tracing::error!("error occurred when listening on x11 events: {:?}", err); + } + } + }), + ); + + subscriptions.push(subscription) + } + + Subscription::batch(subscriptions) } fn assign_global_shortcut( @@ -2197,7 +2251,7 @@ impl AppModel { } fn on_unfocused(&mut self) -> Task { - // for some reason (on both macOS and linux x11) duplicate Unfocused fires right before Focus event + // for some reason (on both macOS and linux x11 but x11 now uses separate impl) duplicate Unfocused fires right before Focus event if self.focused { self.hide_window() } else { @@ -2205,8 +2259,8 @@ impl AppModel { } } - fn toggle_window_focus(&mut self) -> Task { - if self.focused { + fn toggle_window(&mut self) -> Task { + if self.opened { self.hide_window() } else { self.show_window() @@ -2214,6 +2268,10 @@ impl AppModel { } fn hide_window(&mut self) -> Task { + if !self.opened { + return Task::none(); + } + self.focused = false; self.opened = false; diff --git a/rust/client/src/ui/platform/linux.rs b/rust/client/src/ui/platform/linux.rs new file mode 100644 index 0000000..ea66203 --- /dev/null +++ b/rust/client/src/ui/platform/linux.rs @@ -0,0 +1,76 @@ +use std::convert::Infallible; + +use anyhow::anyhow; +use iced::futures::channel::mpsc::Sender; +use iced::futures::SinkExt; +use tokio::runtime::Handle; +use x11rb::connection::Connection; +use x11rb::properties::WmClass; +use x11rb::protocol::xproto::AtomEnum; +use x11rb::protocol::xproto::ChangeWindowAttributesAux; +use x11rb::protocol::xproto::ConnectionExt; +use x11rb::protocol::xproto::EventMask; +use x11rb::protocol::xproto::Window; +use x11rb::rust_connection::RustConnection; + +use crate::ui::AppMsg; + +pub fn listen_on_x11_active_window_change(sender: Sender, handle: Handle) -> anyhow::Result { + let (conn, screen_num) = RustConnection::connect(None)?; + let screen = &conn.setup().roots[screen_num]; + let atoms = atoms::Atoms::new(&conn)?.reply()?; + + let aux = ChangeWindowAttributesAux::new().event_mask(EventMask::PROPERTY_CHANGE); + + conn.change_window_attributes(screen.root, &aux)?.check()?; + + loop { + if let x11rb::protocol::Event::PropertyNotify(event) = conn.wait_for_event()? { + if event.atom == atoms._NET_ACTIVE_WINDOW { + let Ok(window) = fetch_window_id(&conn, screen.root, &atoms) else { + continue; + }; + + if let Ok(wm_name) = fetch_app_wm_name(&conn, window) { + if wm_name == "gauntlet" { + continue; + } + } + + let mut sender = sender.clone(); + handle.spawn(async move { sender.send(AppMsg::X11ActiveWindowChanged { window }).await }); + } + } + } +} + +fn fetch_window_id(conn: &impl Connection, root: Window, atoms: &atoms::Atoms) -> anyhow::Result { + let window = conn + .get_property(false, root, atoms._NET_ACTIVE_WINDOW, AtomEnum::WINDOW, 0, 1)? + .reply()? + .value32() + .ok_or(anyhow!("_NET_ACTIVE_WINDOW has incorrect format"))? + .next() + .ok_or(anyhow!("_NET_ACTIVE_WINDOW is empty"))?; + + Ok(window) +} + +fn fetch_app_wm_name(conn: &impl Connection, window_id: Window) -> anyhow::Result { + let wm_class = WmClass::get(conn, window_id)?; + let wm_class = wm_class.reply()?.ok_or(anyhow!("no WM_CLASS prop on the window"))?; + let class = std::str::from_utf8(wm_class.class())?; + + Ok(class.to_string()) +} + +mod atoms { + x11rb::atom_manager! { + pub Atoms: + AtomsCookie { + _NET_ACTIVE_WINDOW, + _NET_WM_NAME, + UTF8_STRING, + } + } +} diff --git a/rust/client/src/ui/platform/mod.rs b/rust/client/src/ui/platform/mod.rs new file mode 100644 index 0000000..8daba25 --- /dev/null +++ b/rust/client/src/ui/platform/mod.rs @@ -0,0 +1,2 @@ +#[cfg(target_os = "linux")] +pub mod linux; diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml index 5f94b15..e7d725c 100644 --- a/rust/plugin_runtime/Cargo.toml +++ b/rust/plugin_runtime/Cargo.toml @@ -37,8 +37,6 @@ open = "5" [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] libc = "0.2" -x11rb = { version = "0.13", features = ["extra-traits"] } -encoding = "0.2" [target.'cfg(target_os = "linux")'.dependencies] freedesktop_entry_parser = "1.3" @@ -47,6 +45,8 @@ wayland-protocols-wlr = { version = "0.3.5", features = ["client"] } cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols.git" } wayland-client = "0.31.7" smithay-client-toolkit = "0.19.2" +x11rb = { version = "0.13", features = ["extra-traits"] } +encoding = "0.2" [target.'cfg(target_os = "macos")'.dependencies] cacao = "0.3.2" From 9f816c52ef0541f0a41ed48710fea0b21b96fe08 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Feb 2025 09:33:21 +0100 Subject: [PATCH 349/540] Fix windows build --- .github/workflows/setup-windows.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/setup-windows.yaml b/.github/workflows/setup-windows.yaml index 7ea891d..e71f519 100644 --- a/.github/workflows/setup-windows.yaml +++ b/.github/workflows/setup-windows.yaml @@ -28,7 +28,7 @@ jobs: - run: choco install protoc - run: dotnet tool install --global wix - - run: wix extension add -g WixToolset.Util.wixext + - run: wix extension add -g WixToolset.Util.wixext/5.0.2 - uses: Swatinem/rust-cache@v2 with: From ebc18be7c923f529649e33dd04c9d650b5e2d76a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Feb 2025 09:41:05 +0100 Subject: [PATCH 350/540] Fix missing cfg causing build to fail on non-linux platforms --- rust/client/src/ui/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index d3efae3..9e016c9 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -1603,6 +1603,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::done(AppMsg::ShowWindow), ]) } + #[cfg(target_os = "linux")] AppMsg::X11ActiveWindowChanged { window } => { if state.x11_active_window != Some(window) { state.x11_active_window = Some(window); From 2d7af693729ce04a188a257a1c5fc63007a24a69 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Feb 2025 09:51:29 +0100 Subject: [PATCH 351/540] Add --version flag --- CHANGELOG.md | 3 ++- rust/cli/src/lib.rs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c91b651..c05c504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,11 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Added --version flag to CLI to display Gauntlet version - Slightly improved close-on-unfocus behaviour of main window on X11 - Global shortcut is now executed on key press, instead of key release - When using active screen setting for window positioning, position calculated is now relative to size of the screen. Fixes unexpected position when using monitors of different size (contributed by @BennoCrafter) -- Add shortcut to open Settings UI (contributed by @BennoCrafter) +- Added shortcut to open Settings UI (contributed by @BennoCrafter) - Ctrl + , on Windows and Linux - Cmd + , on macOS - Global shortcut how hides the main window if it is already open (contributed by @BennoCrafter) diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs index 14a7788..f4c641c 100644 --- a/rust/cli/src/lib.rs +++ b/rust/cli/src/lib.rs @@ -19,6 +19,10 @@ struct Cli { /// Start server without opening Gauntlet window, only used if no subcommand is provided #[arg(long)] minimized: bool, + + /// Display version and exit + #[arg(long)] + version: bool, } #[derive(Debug, clap::Subcommand)] @@ -48,6 +52,14 @@ pub fn init() { let cli = Cli::parse(); + if cli.version { + println!( + "Gauntlet v{}", + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../VERSION")) + ); + return; + } + match cli.command { None => { if cfg!(feature = "release") { From 0c6b0282c220dae719e69d58ae3d7f2d359b12a3 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Feb 2025 18:58:35 +0100 Subject: [PATCH 352/540] Refactor 3.5k line file into multiple files --- rust/client/src/ui/client_context.rs | 4 +- rust/client/src/ui/mod.rs | 8 +- rust/client/src/ui/search_list.rs | 4 +- rust/client/src/ui/widget.rs | 3508 --------------------- rust/client/src/ui/widget/accessories.rs | 82 + rust/client/src/ui/widget/action_panel.rs | 325 ++ rust/client/src/ui/widget/content.rs | 140 + rust/client/src/ui/widget/data.rs | 417 +++ rust/client/src/ui/widget/data_mut.rs | 402 +++ rust/client/src/ui/widget/detail.rs | 95 + rust/client/src/ui/widget/empty_view.rs | 54 + rust/client/src/ui/widget/events.rs | 246 ++ rust/client/src/ui/widget/form.rs | 246 ++ rust/client/src/ui/widget/grid.rs | 332 ++ rust/client/src/ui/widget/images.rs | 262 ++ rust/client/src/ui/widget/inline.rs | 85 + rust/client/src/ui/widget/list.rs | 274 ++ rust/client/src/ui/widget/metadata.rs | 201 ++ rust/client/src/ui/widget/mod.rs | 18 + rust/client/src/ui/widget/root.rs | 421 +++ rust/client/src/ui/widget/search_bar.rs | 25 + rust/client/src/ui/widget/state.rs | 206 ++ rust/client/src/ui/widget/text.rs | 44 + rust/client/src/ui/widget_container.rs | 12 +- 24 files changed, 3889 insertions(+), 3522 deletions(-) delete mode 100644 rust/client/src/ui/widget.rs create mode 100644 rust/client/src/ui/widget/accessories.rs create mode 100644 rust/client/src/ui/widget/action_panel.rs create mode 100644 rust/client/src/ui/widget/content.rs create mode 100644 rust/client/src/ui/widget/data.rs create mode 100644 rust/client/src/ui/widget/data_mut.rs create mode 100644 rust/client/src/ui/widget/detail.rs create mode 100644 rust/client/src/ui/widget/empty_view.rs create mode 100644 rust/client/src/ui/widget/events.rs create mode 100644 rust/client/src/ui/widget/form.rs create mode 100644 rust/client/src/ui/widget/grid.rs create mode 100644 rust/client/src/ui/widget/images.rs create mode 100644 rust/client/src/ui/widget/inline.rs create mode 100644 rust/client/src/ui/widget/list.rs create mode 100644 rust/client/src/ui/widget/metadata.rs create mode 100644 rust/client/src/ui/widget/mod.rs create mode 100644 rust/client/src/ui/widget/root.rs create mode 100644 rust/client/src/ui/widget/search_bar.rs create mode 100644 rust/client/src/ui/widget/state.rs create mode 100644 rust/client/src/ui/widget/text.rs diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs index 75660b8..9d0f9c8 100644 --- a/rust/client/src/ui/client_context.rs +++ b/rust/client/src/ui/client_context.rs @@ -10,8 +10,8 @@ use gauntlet_common::model::UiWidgetId; use iced::Task; use crate::model::UiViewEvent; -use crate::ui::widget::ActionPanel; -use crate::ui::widget::ComponentWidgetEvent; +use crate::ui::widget::action_panel::ActionPanel; +use crate::ui::widget::events::ComponentWidgetEvent; use crate::ui::widget_container::PluginWidgetContainer; use crate::ui::AppMsg; diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 9e016c9..fbbda0e 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -104,10 +104,6 @@ use crate::ui::theme::container::ContainerStyleInner; use crate::ui::theme::text_input::TextInputStyle; use crate::ui::theme::Element; use crate::ui::theme::ThemableWidget; -use crate::ui::widget::render_root; -use crate::ui::widget::ActionPanel; -use crate::ui::widget::ActionPanelItem; -use crate::ui::widget::ComponentWidgetEvent; mod client_context; mod custom_widgets; @@ -140,6 +136,10 @@ use crate::ui::state::LoadingBarState; use crate::ui::state::MainViewState; use crate::ui::state::PluginViewData; use crate::ui::state::PluginViewState; +use crate::ui::widget::action_panel::ActionPanel; +use crate::ui::widget::action_panel::ActionPanelItem; +use crate::ui::widget::events::ComponentWidgetEvent; +use crate::ui::widget::root::render_root; use crate::ui::widget_container::PluginWidgetContainer; pub struct AppModel { diff --git a/rust/client/src/ui/search_list.rs b/rust/client/src/ui/search_list.rs index 859dcbe..aa28dbe 100644 --- a/rust/client/src/ui/search_list.rs +++ b/rust/client/src/ui/search_list.rs @@ -25,8 +25,8 @@ use crate::ui::theme::space::ThemeKindSpace; use crate::ui::theme::text::TextStyle; use crate::ui::theme::Element; use crate::ui::theme::ThemableWidget; -use crate::ui::widget::render_icon_accessory; -use crate::ui::widget::render_text_accessory; +use crate::ui::widget::accessories::render_icon_accessory; +use crate::ui::widget::accessories::render_text_accessory; pub fn search_list<'a>( search_results: &'a [SearchResult], diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs deleted file mode 100644 index d579f9a..0000000 --- a/rust/client/src/ui/widget.rs +++ /dev/null @@ -1,3508 +0,0 @@ -use std::cell::Cell; -use std::collections::HashMap; -use std::fmt::Debug; -use std::fmt::Display; -use std::sync::Arc; - -use gauntlet_common::model::ActionPanelSectionWidget; -use gauntlet_common::model::ActionPanelSectionWidgetOrderedMembers; -use gauntlet_common::model::ActionPanelWidget; -use gauntlet_common::model::ActionPanelWidgetOrderedMembers; -use gauntlet_common::model::ActionWidget; -use gauntlet_common::model::CheckboxWidget; -use gauntlet_common::model::CodeBlockWidget; -use gauntlet_common::model::ContentWidget; -use gauntlet_common::model::ContentWidgetOrderedMembers; -use gauntlet_common::model::DatePickerWidget; -use gauntlet_common::model::DetailWidget; -use gauntlet_common::model::EmptyViewWidget; -use gauntlet_common::model::FormWidget; -use gauntlet_common::model::FormWidgetOrderedMembers; -use gauntlet_common::model::GridItemWidget; -use gauntlet_common::model::GridSectionWidget; -use gauntlet_common::model::GridSectionWidgetOrderedMembers; -use gauntlet_common::model::GridWidget; -use gauntlet_common::model::GridWidgetOrderedMembers; -use gauntlet_common::model::H1Widget; -use gauntlet_common::model::H2Widget; -use gauntlet_common::model::H3Widget; -use gauntlet_common::model::H4Widget; -use gauntlet_common::model::H5Widget; -use gauntlet_common::model::H6Widget; -use gauntlet_common::model::HorizontalBreakWidget; -use gauntlet_common::model::IconAccessoryWidget; -use gauntlet_common::model::Icons; -use gauntlet_common::model::ImageLike; -use gauntlet_common::model::ImageWidget; -use gauntlet_common::model::InlineSeparatorWidget; -use gauntlet_common::model::InlineWidget; -use gauntlet_common::model::InlineWidgetOrderedMembers; -use gauntlet_common::model::ListItemAccessories; -use gauntlet_common::model::ListItemWidget; -use gauntlet_common::model::ListSectionWidget; -use gauntlet_common::model::ListSectionWidgetOrderedMembers; -use gauntlet_common::model::ListWidget; -use gauntlet_common::model::ListWidgetOrderedMembers; -use gauntlet_common::model::MetadataIconWidget; -use gauntlet_common::model::MetadataLinkWidget; -use gauntlet_common::model::MetadataSeparatorWidget; -use gauntlet_common::model::MetadataTagItemWidget; -use gauntlet_common::model::MetadataTagListWidget; -use gauntlet_common::model::MetadataTagListWidgetOrderedMembers; -use gauntlet_common::model::MetadataValueWidget; -use gauntlet_common::model::MetadataWidget; -use gauntlet_common::model::MetadataWidgetOrderedMembers; -use gauntlet_common::model::ParagraphWidget; -use gauntlet_common::model::PasswordFieldWidget; -use gauntlet_common::model::PhysicalKey; -use gauntlet_common::model::PhysicalShortcut; -use gauntlet_common::model::PluginId; -use gauntlet_common::model::RootWidget; -use gauntlet_common::model::RootWidgetMembers; -use gauntlet_common::model::SearchBarWidget; -use gauntlet_common::model::SelectWidget; -use gauntlet_common::model::SelectWidgetOrderedMembers; -use gauntlet_common::model::SeparatorWidget; -use gauntlet_common::model::TextAccessoryWidget; -use gauntlet_common::model::TextFieldWidget; -use gauntlet_common::model::UiRenderLocation; -use gauntlet_common::model::UiWidgetId; -use gauntlet_common_ui::shortcut_to_text; -use iced::alignment::Horizontal; -use iced::alignment::Vertical; -use iced::font::Weight; -use iced::widget::button; -use iced::widget::checkbox; -use iced::widget::column; -use iced::widget::container; -use iced::widget::horizontal_rule; -use iced::widget::horizontal_space; -use iced::widget::image; -use iced::widget::image::Handle; -use iced::widget::mouse_area; -use iced::widget::pick_list; -use iced::widget::row; -use iced::widget::scrollable; -use iced::widget::stack; -use iced::widget::text; -use iced::widget::text::Shaping; -use iced::widget::text_input; -use iced::widget::tooltip; -use iced::widget::tooltip::Position; -use iced::widget::value; -use iced::widget::vertical_rule; -use iced::widget::Space; -use iced::Alignment; -use iced::Font; -use iced::Length; -use iced::Task; -use iced_aw::date_picker::Date; -use iced_aw::helpers::date_picker; -use iced_aw::helpers::grid; -use iced_aw::helpers::grid_row; -use iced_aw::GridRow; -use iced_fonts::Bootstrap; -use iced_fonts::BOOTSTRAP_FONT; -use itertools::Itertools; - -use crate::model::UiViewEvent; -use crate::ui::custom_widgets::loading_bar::LoadingBar; -use crate::ui::grid_navigation::grid_down_offset; -use crate::ui::grid_navigation::grid_up_offset; -use crate::ui::grid_navigation::GridSectionData; -use crate::ui::scroll_handle::ScrollHandle; -use crate::ui::scroll_handle::ESTIMATED_MAIN_LIST_ITEM_HEIGHT; -use crate::ui::state::PluginViewState; -use crate::ui::theme::button::ButtonStyle; -use crate::ui::theme::container::ContainerStyle; -use crate::ui::theme::date_picker::DatePickerStyle; -use crate::ui::theme::grid::GridStyle; -use crate::ui::theme::pick_list::PickListStyle; -use crate::ui::theme::row::RowStyle; -use crate::ui::theme::rule::RuleStyle; -use crate::ui::theme::text::TextStyle; -use crate::ui::theme::text_input::TextInputStyle; -use crate::ui::theme::tooltip::TooltipStyle; -use crate::ui::theme::Element; -use crate::ui::theme::ThemableWidget; -use crate::ui::AppMsg; - -#[derive(Debug)] -pub struct ComponentWidgets<'b> { - root_widget: &'b Option>, - state: &'b HashMap, - plugin_id: PluginId, - images: &'b HashMap>, -} - -impl<'b> ComponentWidgets<'b> { - pub fn new( - root_widget: &'b Option>, - state: &'b HashMap, - plugin_id: PluginId, - images: &'b HashMap>, - ) -> ComponentWidgets<'b> { - Self { - root_widget, - state, - plugin_id, - images, - } - } - - fn text_field_state(&self, widget_id: UiWidgetId) -> &TextFieldState { - let state = self.state.get(&widget_id).expect(&format!( - "requested state should always be present for id: {}", - widget_id - )); - - match state { - ComponentWidgetState::TextField(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state), - } - } - - fn checkbox_state(&self, widget_id: UiWidgetId) -> &CheckboxState { - let state = self.state.get(&widget_id).expect(&format!( - "requested state should always be present for id: {}", - widget_id - )); - - match state { - ComponentWidgetState::Checkbox(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state), - } - } - - fn date_picker_state(&self, widget_id: UiWidgetId) -> &DatePickerState { - let state = self.state.get(&widget_id).expect(&format!( - "requested state should always be present for id: {}", - widget_id - )); - - match state { - ComponentWidgetState::DatePicker(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state), - } - } - - fn select_state(&self, widget_id: UiWidgetId) -> &SelectState { - let state = self.state.get(&widget_id).expect(&format!( - "requested state should always be present for id: {}", - widget_id - )); - - match state { - ComponentWidgetState::Select(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state), - } - } - - fn root_state(&self, widget_id: UiWidgetId) -> &RootState { - let state = self.state.get(&widget_id).expect(&format!( - "requested state should always be present for id: {}", - widget_id - )); - - match state { - ComponentWidgetState::Root(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state), - } - } -} - -#[derive(Debug)] -pub struct ComponentWidgetsMut<'b> { - root_widget: &'b mut Option>, - state: &'b mut HashMap, - plugin_id: PluginId, - images: &'b HashMap>, -} - -impl<'b> ComponentWidgetsMut<'b> { - pub fn new( - root_widget: &'b mut Option>, - state: &'b mut HashMap, - plugin_id: PluginId, - images: &'b HashMap>, - ) -> ComponentWidgetsMut<'b> { - Self { - root_widget, - state, - plugin_id, - images, - } - } - - fn text_field_state_mut(&mut self, widget_id: UiWidgetId) -> &mut TextFieldState { - Self::text_field_state_mut_on_state(&mut self.state, widget_id) - } - - fn text_field_state_mut_on_state( - state: &mut HashMap, - widget_id: UiWidgetId, - ) -> &mut TextFieldState { - let state = state.get_mut(&widget_id).expect(&format!( - "requested state should always be present for id: {}", - widget_id - )); - - match state { - ComponentWidgetState::TextField(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state), - } - } - - fn root_state_mut(&mut self, widget_id: UiWidgetId) -> &mut RootState { - Self::root_state_mut_on_field(&mut self.state, widget_id) - } - - fn root_state_mut_on_field( - state: &mut HashMap, - widget_id: UiWidgetId, - ) -> &mut RootState { - let state = state.get_mut(&widget_id).expect(&format!( - "requested state should always be present for id: {}", - widget_id - )); - - match state { - ComponentWidgetState::Root(state) => state, - _ => panic!("TextFieldState expected, {:?} found", state), - } - } -} - -pub fn create_state(root_widget: &RootWidget) -> HashMap { - let mut result = HashMap::new(); - - match &root_widget.content { - None => {} - Some(members) => { - match members { - RootWidgetMembers::Detail(widget) => { - result.insert(widget.__id__, ComponentWidgetState::root(0.0, 0)); - } - RootWidgetMembers::Form(widget) => { - result.insert(widget.__id__, ComponentWidgetState::root(0.0, 0)); - - for members in &widget.content.ordered_members { - match members { - FormWidgetOrderedMembers::TextField(widget) => { - result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); - } - FormWidgetOrderedMembers::PasswordField(widget) => { - result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); - } - FormWidgetOrderedMembers::Checkbox(widget) => { - result.insert(widget.__id__, ComponentWidgetState::checkbox(&widget.value)); - } - FormWidgetOrderedMembers::DatePicker(widget) => { - result.insert(widget.__id__, ComponentWidgetState::date_picker(&widget.value)); - } - FormWidgetOrderedMembers::Select(widget) => { - result.insert(widget.__id__, ComponentWidgetState::select(&widget.value)); - } - FormWidgetOrderedMembers::Separator(_) => {} - } - } - } - RootWidgetMembers::List(widget) => { - result.insert( - widget.__id__, - ComponentWidgetState::root(ESTIMATED_MAIN_LIST_ITEM_HEIGHT, 7), - ); - - if let Some(widget) = &widget.content.search_bar { - result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); - } - } - RootWidgetMembers::Grid(widget) => { - // cursed heuristic - let has_title = widget - .content - .ordered_members - .iter() - .flat_map(|members| { - match members { - GridWidgetOrderedMembers::GridItem(widget) => vec![widget], - GridWidgetOrderedMembers::GridSection(widget) => { - widget - .content - .ordered_members - .iter() - .map(|members| { - match members { - GridSectionWidgetOrderedMembers::GridItem(widget) => widget, - } - }) - .collect() - } - } - }) - .next() - .map(|widget| widget.title.is_some() || widget.subtitle.is_some()) - .unwrap_or_default(); - - let (height, rows_per_view) = match grid_width(&widget.columns) { - ..4 => (150.0, 0), - 4 => (150.0, 0), - 5 => (130.0, 0), - 6 => (110.0, 1), - 7 => (90.0, 3), - 8 => (if has_title { 50.0 } else { 50.0 }, if has_title { 3 } else { 4 }), - 8.. => (50.0, 4), - }; - - result.insert(widget.__id__, ComponentWidgetState::root(height, rows_per_view)); - - if let Some(widget) = &widget.content.search_bar { - result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); - } - } - RootWidgetMembers::Inline(_) => {} - } - } - } - - result -} - -#[derive(Debug, Clone)] -pub enum ComponentWidgetState { - TextField(TextFieldState), - Checkbox(CheckboxState), - DatePicker(DatePickerState), - Select(SelectState), - Root(RootState), -} - -#[derive(Debug, Clone)] -struct TextFieldState { - text_input_id: text_input::Id, - state_value: String, -} - -#[derive(Debug, Clone)] -struct CheckboxState { - state_value: bool, -} - -#[derive(Debug, Clone)] -struct DatePickerState { - show_picker: bool, - state_value: Date, -} - -#[derive(Debug, Clone)] -struct SelectState { - state_value: Option, -} - -#[derive(Debug, Clone)] -struct RootState { - show_action_panel: bool, - focused_item: ScrollHandle, -} - -impl ComponentWidgetState { - fn root(item_height: f32, rows_per_view: usize) -> ComponentWidgetState { - ComponentWidgetState::Root(RootState { - show_action_panel: false, - focused_item: ScrollHandle::new(false, item_height, rows_per_view), - }) - } - - fn text_field(value: &Option) -> ComponentWidgetState { - ComponentWidgetState::TextField(TextFieldState { - text_input_id: text_input::Id::unique(), - state_value: value.to_owned().unwrap_or_default(), - }) - } - - fn checkbox(value: &Option) -> ComponentWidgetState { - ComponentWidgetState::Checkbox(CheckboxState { - state_value: value.to_owned().unwrap_or(false), - }) - } - - fn date_picker(value: &Option) -> ComponentWidgetState { - let value = value - .to_owned() - .map(|value| parse_date(&value)) - .flatten() - .map(|(year, month, day)| Date::from_ymd(year, month, day)) - .unwrap_or(Date::today()); - - ComponentWidgetState::DatePicker(DatePickerState { - state_value: value, - show_picker: false, - }) - } - - fn select(value: &Option) -> ComponentWidgetState { - ComponentWidgetState::Select(SelectState { - state_value: value.to_owned(), - }) - } -} - -#[derive(Debug, Clone)] -pub enum TextRenderType { - None, - H1, - H2, - H3, - H4, - H5, - H6, -} - -impl<'b> ComponentWidgetsMut<'b> { - pub fn toggle_action_panel(&mut self) { - let Some(root_widget) = &self.root_widget else { - return; - }; - - let Some(content) = &root_widget.content else { - return; - }; - - let widget_id = match content { - RootWidgetMembers::Detail(widget) => widget.__id__, - RootWidgetMembers::Form(widget) => widget.__id__, - RootWidgetMembers::Inline(widget) => widget.__id__, - RootWidgetMembers::List(widget) => widget.__id__, - RootWidgetMembers::Grid(widget) => widget.__id__, - }; - - let state = self.root_state_mut(widget_id); - - state.show_action_panel = !state.show_action_panel; - } -} - -impl<'b> ComponentWidgets<'b> { - pub fn get_action_ids(&self) -> Vec { - let Some(root_widget) = &self.root_widget else { - return vec![]; - }; - - let Some(content) = &root_widget.content else { - return vec![]; - }; - - let actions = match content { - RootWidgetMembers::Detail(widget) => &widget.content.actions, - RootWidgetMembers::Form(widget) => &widget.content.actions, - RootWidgetMembers::Inline(widget) => &widget.content.actions, - RootWidgetMembers::List(widget) => &widget.content.actions, - RootWidgetMembers::Grid(widget) => &widget.content.actions, - }; - - let mut result = vec![]; - match actions { - None => {} - Some(widget) => { - for members in &widget.content.ordered_members { - match members { - ActionPanelWidgetOrderedMembers::Action(widget) => result.push(widget.__id__), - ActionPanelWidgetOrderedMembers::ActionPanelSection(widget) => { - for members in &widget.content.ordered_members { - match members { - ActionPanelSectionWidgetOrderedMembers::Action(widget) => { - result.push(widget.__id__) - } - } - } - } - } - } - } - } - - result - } - - pub fn get_focused_item_id(&self) -> Option { - let Some(root_widget) = &self.root_widget else { - return None; - }; - - let Some(content) = &root_widget.content else { - return None; - }; - - match content { - RootWidgetMembers::Detail(_) => None, - RootWidgetMembers::Form(_) => None, - RootWidgetMembers::Inline(_) => None, - RootWidgetMembers::List(widget) => { - let RootState { focused_item, .. } = self.root_state(widget.__id__); - - ComponentWidgets::list_focused_item_id(focused_item, widget) - } - RootWidgetMembers::Grid(widget) => { - let RootState { focused_item, .. } = self.root_state(widget.__id__); - - ComponentWidgets::grid_focused_item_id(focused_item, widget) - } - } - } - - pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Task { - let TextFieldState { text_input_id, .. } = self.text_field_state(widget_id); - - text_input::focus(text_input_id.clone()) - } - - fn grid_section_sizes(grid_widget: &GridWidget) -> Vec { - let mut amount_per_section: Vec = vec![]; - let mut pending_section_size = 0; - - let mut cumulative_item_index = 0; - let mut cumulative_row_index = 0; - - let mut cumulative_item_index_at_start = cumulative_item_index; - let mut cumulative_row_index_at_start = cumulative_row_index; - - for members in &grid_widget.content.ordered_members { - match &members { - GridWidgetOrderedMembers::GridItem(_) => { - pending_section_size = pending_section_size + 1; - } - GridWidgetOrderedMembers::GridSection(widget) => { - if pending_section_size > 0 { - let width = grid_width(&grid_widget.columns); - amount_per_section.push(GridSectionData { - start_index: cumulative_item_index_at_start, - start_row_index: cumulative_row_index_at_start, - amount_in_section: pending_section_size, - width, - }); - - cumulative_item_index = cumulative_item_index + pending_section_size; - cumulative_row_index = - cumulative_row_index_at_start + (usize::div_ceil(pending_section_size, width)); - - cumulative_item_index_at_start = cumulative_item_index; - cumulative_row_index_at_start = cumulative_row_index; - - pending_section_size = 0; - } - - let section_amount = widget - .content - .ordered_members - .iter() - .filter(|members| matches!(members, GridSectionWidgetOrderedMembers::GridItem(_))) - .count(); - - let width = grid_width(&widget.columns); - amount_per_section.push(GridSectionData { - start_index: cumulative_item_index_at_start, - start_row_index: cumulative_row_index_at_start, - amount_in_section: section_amount, - width, - }); - - cumulative_item_index = cumulative_item_index + section_amount; - cumulative_row_index = cumulative_row_index_at_start + (usize::div_ceil(section_amount, width)); - - cumulative_item_index_at_start = cumulative_item_index; - cumulative_row_index_at_start = cumulative_row_index; - } - } - } - - if pending_section_size > 0 { - amount_per_section.push(GridSectionData { - start_index: cumulative_item_index_at_start, - start_row_index: cumulative_row_index_at_start, - amount_in_section: pending_section_size, - width: grid_width(&grid_widget.columns), - }); - } - - amount_per_section - } -} - -impl<'b> ComponentWidgetsMut<'b> { - pub fn append_text(&mut self, text: &str) -> Task { - let Some(root_widget) = &self.root_widget else { - return Task::none(); - }; - - let Some(content) = &root_widget.content else { - return Task::none(); - }; - - let widget_id = match content { - RootWidgetMembers::List(widget) => { - match &widget.content.search_bar { - None => return Task::none(), - Some(widget) => widget.__id__, - } - } - RootWidgetMembers::Grid(widget) => { - match &widget.content.search_bar { - None => return Task::none(), - Some(widget) => widget.__id__, - } - } - _ => return Task::none(), - }; - - let TextFieldState { - text_input_id, - state_value, - } = ComponentWidgetsMut::text_field_state_mut_on_state(&mut self.state, widget_id); - - if let Some(value) = text.chars().next().filter(|c| !c.is_control()) { - *state_value = format!("{}{}", state_value, value); - - text_input::focus(text_input_id.clone()) - } else { - Task::none() - } - } - - pub fn backspace_text(&mut self) -> Task { - let Some(root_widget) = &self.root_widget else { - return Task::none(); - }; - - let Some(content) = &root_widget.content else { - return Task::none(); - }; - - let widget_id = match content { - RootWidgetMembers::List(widget) => { - match &widget.content.search_bar { - None => return Task::none(), - Some(widget) => widget.__id__, - } - } - RootWidgetMembers::Grid(widget) => { - match &widget.content.search_bar { - None => return Task::none(), - Some(widget) => widget.__id__, - } - } - _ => return Task::none(), - }; - - let TextFieldState { - text_input_id, - state_value, - } = ComponentWidgetsMut::text_field_state_mut_on_state(&mut self.state, widget_id); - - let mut chars = state_value.chars(); - chars.next_back(); - *state_value = chars.as_str().to_owned(); - - text_input::focus(text_input_id.clone()) - } - - pub fn focus_up(&mut self) -> Task { - let Some(root_widget) = &self.root_widget else { - return Task::none(); - }; - - let Some(content) = &root_widget.content else { - return Task::none(); - }; - - match content { - RootWidgetMembers::Detail(_) => Task::none(), - RootWidgetMembers::Form(_) => Task::none(), - RootWidgetMembers::Inline(_) => Task::none(), - RootWidgetMembers::List(list_widget) => { - let RootState { focused_item, .. } = - ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, list_widget.__id__); - - let focus_task = focused_item.focus_previous().unwrap_or_else(|| Task::none()); - - let item_focus_event = - ComponentWidgets::list_item_focus_event(self.plugin_id.clone(), focused_item, list_widget); - - Task::batch([item_focus_event, focus_task]) - } - RootWidgetMembers::Grid(grid_widget) => { - let RootState { focused_item, .. } = - ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); - - let Some(current_index) = &focused_item.index else { - return Task::none(); - }; - - let amount_per_section_total = ComponentWidgets::grid_section_sizes(grid_widget); - - let focus_task = match grid_up_offset(*current_index, amount_per_section_total) { - None => Task::none(), - Some(data) => { - match focused_item.focus_previous_in(data.offset) { - None => Task::none(), - Some(_) => focused_item.scroll_to(data.row_index), - } - } - }; - - let item_focus_event = - ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); - - Task::batch([item_focus_event, focus_task]) - } - } - } - - pub fn focus_down(&mut self) -> Task { - let Some(root_widget) = &self.root_widget else { - return Task::none(); - }; - - let Some(content) = &root_widget.content else { - return Task::none(); - }; - - match content { - RootWidgetMembers::Detail(_) => Task::none(), - RootWidgetMembers::Form(_) => Task::none(), - RootWidgetMembers::Inline(_) => Task::none(), - RootWidgetMembers::List(widget) => { - let RootState { focused_item, .. } = - ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, widget.__id__); - - let total = widget - .content - .ordered_members - .iter() - .flat_map(|members| { - match members { - ListWidgetOrderedMembers::ListItem(widget) => vec![widget], - ListWidgetOrderedMembers::ListSection(widget) => { - widget - .content - .ordered_members - .iter() - .map(|members| { - match members { - ListSectionWidgetOrderedMembers::ListItem(widget) => widget, - } - }) - .collect() - } - } - }) - .count(); - - let focus_task = focused_item.focus_next(total).unwrap_or_else(|| Task::none()); - - let item_focus_event = - ComponentWidgets::list_item_focus_event(self.plugin_id.clone(), focused_item, widget); - - Task::batch([item_focus_event, focus_task]) - } - RootWidgetMembers::Grid(grid_widget) => { - let RootState { focused_item, .. } = - ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); - - let amount_per_section_total = ComponentWidgets::grid_section_sizes(grid_widget); - - let total = amount_per_section_total.iter().map(|data| data.amount_in_section).sum(); - - let Some(current_index) = &focused_item.index else { - let unfocus = match &grid_widget.content.search_bar { - None => Task::none(), - Some(_) => { - // there doesn't seem to be an unfocus command but focusing non-existing input will unfocus all - text_input::focus(text_input::Id::unique()) - } - }; - - let _ = focused_item.focus_next(total); - - let item_focus_event = - ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); - - return Task::batch([unfocus, focused_item.scroll_to(0), item_focus_event]); - }; - - let focus_task = match grid_down_offset(*current_index, amount_per_section_total) { - None => Task::none(), - Some(data) => { - match focused_item.focus_next_in(total, data.offset) { - None => Task::none(), - Some(_) => focused_item.scroll_to(data.row_index), - } - } - }; - - let item_focus_event = - ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); - - Task::batch([item_focus_event, focus_task]) - } - } - } - - pub fn focus_left(&mut self) -> Task { - let Some(root_widget) = &self.root_widget else { - return Task::none(); - }; - - let Some(content) = &root_widget.content else { - return Task::none(); - }; - - match content { - RootWidgetMembers::Detail(_) => Task::none(), - RootWidgetMembers::Form(_) => Task::none(), - RootWidgetMembers::Inline(_) => Task::none(), - RootWidgetMembers::List(_) => Task::none(), - RootWidgetMembers::Grid(grid_widget) => { - let RootState { focused_item, .. } = - ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); - - let _ = focused_item.focus_previous(); - - // focused_item.scroll_to(0) - - ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget) - } - } - } - - pub fn focus_right(&mut self) -> Task { - let Some(root_widget) = &self.root_widget else { - return Task::none(); - }; - - let Some(content) = &root_widget.content else { - return Task::none(); - }; - - match content { - RootWidgetMembers::Detail(_) => Task::none(), - RootWidgetMembers::Form(_) => Task::none(), - RootWidgetMembers::Inline(_) => Task::none(), - RootWidgetMembers::List(_) => Task::none(), - RootWidgetMembers::Grid(grid_widget) => { - let RootState { focused_item, .. } = - ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); - - let total = grid_widget - .content - .ordered_members - .iter() - .flat_map(|members| { - match members { - GridWidgetOrderedMembers::GridItem(widget) => vec![widget], - GridWidgetOrderedMembers::GridSection(widget) => { - widget - .content - .ordered_members - .iter() - .map(|members| { - match members { - GridSectionWidgetOrderedMembers::GridItem(widget) => widget, - } - }) - .collect() - } - } - }) - .count(); - - let _ = focused_item.focus_next(total); - - // focused_item.scroll_to(0) - - ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget) - } - } - } -} - -impl<'b> ComponentWidgets<'b> { - pub fn first_open(&self) -> AppMsg { - let Some(root_widget) = &self.root_widget else { - return AppMsg::Noop; - }; - - let Some(content) = &root_widget.content else { - return AppMsg::Noop; - }; - - let widget_id = match content { - RootWidgetMembers::List(widget) => { - match &widget.content.search_bar { - None => return AppMsg::Noop, - Some(widget) => widget.__id__, - } - } - RootWidgetMembers::Grid(widget) => { - match &widget.content.search_bar { - None => return AppMsg::Noop, - Some(widget) => widget.__id__, - } - } - _ => return AppMsg::Noop, - }; - - AppMsg::FocusPluginViewSearchBar { widget_id } - } - - fn list_focused_item_id(focused_item: &ScrollHandle, widget: &ListWidget) -> Option { - let mut items = vec![]; - - for members in &widget.content.ordered_members { - match &members { - ListWidgetOrderedMembers::ListItem(item) => { - items.push(&item.id); - } - ListWidgetOrderedMembers::ListSection(section) => { - for members in §ion.content.ordered_members { - match &members { - ListSectionWidgetOrderedMembers::ListItem(item) => { - items.push(&item.id); - } - } - } - } - } - } - - match focused_item.get(&items) { - None => None, - Some(item_id) => Some(item_id.to_string()), - } - } - - fn list_item_focus_event(plugin_id: PluginId, focused_item: &ScrollHandle, widget: &ListWidget) -> Task { - let widget_event = match ComponentWidgets::list_focused_item_id(focused_item, widget) { - None => { - ComponentWidgetEvent::FocusListItem { - list_widget_id: widget.__id__, - item_id: None, - } - } - Some(item_id) => { - ComponentWidgetEvent::FocusListItem { - list_widget_id: widget.__id__, - item_id: Some(item_id), - } - } - }; - - Task::done(AppMsg::WidgetEvent { - plugin_id, - render_location: UiRenderLocation::View, - widget_event, - }) - } - - fn grid_focused_item_id(focused_item: &ScrollHandle, widget: &GridWidget) -> Option { - let mut items = vec![]; - - for members in &widget.content.ordered_members { - match &members { - GridWidgetOrderedMembers::GridItem(item) => { - items.push(&item.id); - } - GridWidgetOrderedMembers::GridSection(section) => { - for members in §ion.content.ordered_members { - match &members { - GridSectionWidgetOrderedMembers::GridItem(item) => { - items.push(&item.id); - } - } - } - } - } - } - - match focused_item.get(&items) { - None => None, - Some(item_id) => Some(item_id.to_string()), - } - } - - fn grid_item_focus_event(plugin_id: PluginId, focused_item: &ScrollHandle, widget: &GridWidget) -> Task { - let widget_event = match ComponentWidgets::grid_focused_item_id(focused_item, widget) { - None => { - ComponentWidgetEvent::FocusGridItem { - grid_widget_id: widget.__id__, - item_id: None, - } - } - Some(item_id) => { - ComponentWidgetEvent::FocusGridItem { - grid_widget_id: widget.__id__, - item_id: Some(item_id), - } - } - }; - - Task::done(AppMsg::WidgetEvent { - plugin_id, - render_location: UiRenderLocation::View, - widget_event, - }) - } - - pub fn get_action_panel(&self, action_shortcuts: &HashMap) -> Option { - let Some(root_widget) = &self.root_widget else { - return None; - }; - - let Some(content) = &root_widget.content else { - return None; - }; - - match content { - RootWidgetMembers::Detail(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), - RootWidgetMembers::Form(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), - RootWidgetMembers::Inline(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), - RootWidgetMembers::List(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), - RootWidgetMembers::Grid(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), - } - } - - fn render_text<'a>(&self, value: &[String], context: TextRenderType) -> Element<'a, ComponentWidgetEvent> { - let header = match context { - TextRenderType::None => None, - TextRenderType::H1 => Some(34), - TextRenderType::H2 => Some(30), - TextRenderType::H3 => Some(24), - TextRenderType::H4 => Some(20), - TextRenderType::H5 => Some(18), - TextRenderType::H6 => Some(16), - }; - - let mut text = text(value.join("")).shaping(Shaping::Advanced); - - if let Some(size) = header { - text = text.size(size).font(Font { - weight: Weight::Bold, - ..Font::DEFAULT - }) - } - - text.into() - } - - pub fn render_root_widget<'a>( - &self, - plugin_view_state: &PluginViewState, - entrypoint_name: Option<&String>, - action_shortcuts: &HashMap, - ) -> Element<'a, ComponentWidgetEvent> { - match &self.root_widget { - None => horizontal_space().into(), - Some(root) => { - match &root.content { - None => horizontal_space().into(), - Some(content) => { - let entrypoint_name = - entrypoint_name.expect("entrypoint name should always exist after render"); - - match content { - RootWidgetMembers::Detail(widget) => { - let RootState { show_action_panel, .. } = self.root_state(widget.__id__); - - let content = self.render_detail_widget(widget, false); - - self.render_plugin_root( - *show_action_panel, - widget.__id__, - None, - &None, - &widget.content.actions, - content, - widget.is_loading.unwrap_or(false), - plugin_view_state, - entrypoint_name, - action_shortcuts, - ) - } - RootWidgetMembers::Form(widget) => { - self.render_form_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts) - } - RootWidgetMembers::List(widget) => { - self.render_list_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts) - } - RootWidgetMembers::Grid(widget) => { - self.render_grid_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts) - } - _ => { - panic!("used inline widget in non-inline place") - } - } - } - } - } - } - } - - pub fn render_root_inline_widget<'a>( - &self, - plugin_name: Option<&String>, - entrypoint_name: Option<&String>, - ) -> Element<'a, ComponentWidgetEvent> { - match &self.root_widget { - None => horizontal_space().into(), - Some(root) => { - match &root.content { - None => horizontal_space().into(), - Some(content) => { - match content { - RootWidgetMembers::Inline(widget) => { - let entrypoint_name = - entrypoint_name.expect("entrypoint name should always exist after render"); - let plugin_name = - plugin_name.expect("entrypoint name should always exist after render"); - - self.render_inline_widget(widget, plugin_name, entrypoint_name) - } - _ => { - panic!("used non-inline widget in inline place") - } - } - } - } - } - } - } - - fn render_metadata_tag_item_widget<'a>(&self, widget: &MetadataTagItemWidget) -> Element<'a, ComponentWidgetEvent> { - let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); - - let tag: Element<_> = button(content) - .on_press(ComponentWidgetEvent::TagClick { - widget_id: widget.__id__, - }) - .themed(ButtonStyle::MetadataTagItem); - - container(tag).themed(ContainerStyle::MetadataTagItem) - } - - fn render_metadata_tag_list_widget<'a>( - &self, - widget: &MetadataTagListWidget, - is_in_list: bool, - ) -> Element<'a, ComponentWidgetEvent> { - let content: Vec> = widget - .content - .ordered_members - .iter() - .map(|members| { - match members { - MetadataTagListWidgetOrderedMembers::MetadataTagItem(content) => { - self.render_metadata_tag_item_widget(&content) - } - } - }) - .collect(); - - let value = row(content).wrap().into(); - - render_metadata_item(&widget.label, value, is_in_list).into() - } - - fn render_metadata_link_widget<'a>( - &self, - widget: &MetadataLinkWidget, - is_in_list: bool, - ) -> Element<'a, ComponentWidgetEvent> { - let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); - - let icon: Element<_> = value(Bootstrap::BoxArrowUpRight).font(BOOTSTRAP_FONT).size(16).into(); - - let icon = container(icon).themed(ContainerStyle::MetadataLinkIcon); - - let content: Element<_> = row([content, icon]).align_y(Alignment::Center).into(); - - let link: Element<_> = button(content) - .on_press(ComponentWidgetEvent::LinkClick { - widget_id: widget.__id__, - href: widget.href.to_owned(), - }) - .themed(ButtonStyle::MetadataLink); - - let content: Element<_> = if widget.href.is_empty() { - link - } else { - let href: Element<_> = text(widget.href.to_string()).shaping(Shaping::Advanced).into(); - - tooltip(link, href, Position::Top).themed(TooltipStyle::Tooltip) - }; - - render_metadata_item(&widget.label, content, is_in_list).into() - } - - fn render_metadata_value_widget<'a>( - &self, - widget: &MetadataValueWidget, - is_in_list: bool, - ) -> Element<'a, ComponentWidgetEvent> { - let value: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); - - render_metadata_item(&widget.label, value, is_in_list).into() - } - - fn render_metadata_icon_widget<'a>( - &self, - widget: &MetadataIconWidget, - is_in_list: bool, - ) -> Element<'a, ComponentWidgetEvent> { - let value = value(icon_to_bootstrap(&widget.icon)) - .font(BOOTSTRAP_FONT) - .size(26) - .into(); - - render_metadata_item(&widget.label, value, is_in_list).into() - } - - fn render_metadata_separator_widget<'a>( - &self, - _widget: &MetadataSeparatorWidget, - ) -> Element<'a, ComponentWidgetEvent> { - let separator: Element<_> = horizontal_rule(1).into(); - - container(separator) - .width(Length::Fill) - .themed(ContainerStyle::MetadataSeparator) - } - - fn render_metadata_widget<'a>( - &self, - widget: &MetadataWidget, - is_in_list: bool, - ) -> Element<'a, ComponentWidgetEvent> { - let content: Vec> = widget - .content - .ordered_members - .iter() - .map(|members| { - match members { - MetadataWidgetOrderedMembers::MetadataTagList(content) => { - self.render_metadata_tag_list_widget(content, is_in_list) - } - MetadataWidgetOrderedMembers::MetadataLink(content) => { - self.render_metadata_link_widget(content, is_in_list) - } - MetadataWidgetOrderedMembers::MetadataValue(content) => { - self.render_metadata_value_widget(content, is_in_list) - } - MetadataWidgetOrderedMembers::MetadataIcon(content) => { - self.render_metadata_icon_widget(content, is_in_list) - } - MetadataWidgetOrderedMembers::MetadataSeparator(content) => { - self.render_metadata_separator_widget(content) - } - } - }) - .collect(); - - let metadata: Element<_> = column(content).into(); - - let metadata = container(metadata) - .width(Length::Fill) - .themed(ContainerStyle::MetadataInner); - - scrollable(metadata).width(Length::Fill).into() - } - - fn render_paragraph_widget<'a>( - &self, - widget: &ParagraphWidget, - centered: bool, - ) -> Element<'a, ComponentWidgetEvent> { - let paragraph: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); - - let mut content = container(paragraph).width(Length::Fill); - - if centered { - content = content.align_x(Horizontal::Center) - } - - content.themed(ContainerStyle::ContentParagraph) - } - - fn render_image_widget<'a>(&self, widget: &ImageWidget, centered: bool) -> Element<'a, ComponentWidgetEvent> { - // TODO image size, height and width - let content: Element<_> = render_image(self.images, widget.__id__, &widget.source, None); - - let mut content = container(content).width(Length::Fill); - - if centered { - content = content.align_x(Horizontal::Center) - } - - content.themed(ContainerStyle::ContentImage) - } - - fn render_h1_widget<'a>(&self, widget: &H1Widget) -> Element<'a, ComponentWidgetEvent> { - self.render_text(&widget.content.text, TextRenderType::H1) - } - - fn render_h2_widget<'a>(&self, widget: &H2Widget) -> Element<'a, ComponentWidgetEvent> { - self.render_text(&widget.content.text, TextRenderType::H2) - } - - fn render_h3_widget<'a>(&self, widget: &H3Widget) -> Element<'a, ComponentWidgetEvent> { - self.render_text(&widget.content.text, TextRenderType::H3) - } - - fn render_h4_widget<'a>(&self, widget: &H4Widget) -> Element<'a, ComponentWidgetEvent> { - self.render_text(&widget.content.text, TextRenderType::H4) - } - - fn render_h5_widget<'a>(&self, widget: &H5Widget) -> Element<'a, ComponentWidgetEvent> { - self.render_text(&widget.content.text, TextRenderType::H5) - } - - fn render_h6_widget<'a>(&self, widget: &H6Widget) -> Element<'a, ComponentWidgetEvent> { - self.render_text(&widget.content.text, TextRenderType::H6) - } - - fn render_horizontal_break_widget<'a>(&self, _widget: &HorizontalBreakWidget) -> Element<'a, ComponentWidgetEvent> { - let separator: Element<_> = horizontal_rule(1).into(); - - container(separator) - .width(Length::Fill) - .themed(ContainerStyle::ContentHorizontalBreak) - } - - fn render_code_block_widget<'a>(&self, widget: &CodeBlockWidget) -> Element<'a, ComponentWidgetEvent> { - let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); - - let content = container(content) - .width(Length::Fill) - .themed(ContainerStyle::ContentCodeBlockText); - - container(content) - .width(Length::Fill) - .themed(ContainerStyle::ContentCodeBlock) - } - - fn render_content_widget<'a>(&self, widget: &ContentWidget, centered: bool) -> Element<'a, ComponentWidgetEvent> { - let content: Vec<_> = widget - .content - .ordered_members - .iter() - .map(|members| { - match members { - ContentWidgetOrderedMembers::Paragraph(widget) => self.render_paragraph_widget(widget, centered), - ContentWidgetOrderedMembers::Image(widget) => self.render_image_widget(widget, centered), - ContentWidgetOrderedMembers::H1(widget) => self.render_h1_widget(widget), - ContentWidgetOrderedMembers::H2(widget) => self.render_h2_widget(widget), - ContentWidgetOrderedMembers::H3(widget) => self.render_h3_widget(widget), - ContentWidgetOrderedMembers::H4(widget) => self.render_h4_widget(widget), - ContentWidgetOrderedMembers::H5(widget) => self.render_h5_widget(widget), - ContentWidgetOrderedMembers::H6(widget) => self.render_h6_widget(widget), - ContentWidgetOrderedMembers::HorizontalBreak(widget) => self.render_horizontal_break_widget(widget), - ContentWidgetOrderedMembers::CodeBlock(widget) => self.render_code_block_widget(widget), - } - }) - .collect(); - - let content: Element<_> = column(content).into(); - - if centered { - container(content) - .width(Length::Fill) - .height(Length::Fill) - .align_x(Horizontal::Center) - .align_y(Vertical::Center) - .into() - } else { - content - } - } - - fn render_detail_widget<'a>(&self, widget: &DetailWidget, is_in_list: bool) -> Element<'a, ComponentWidgetEvent> { - let metadata_element = widget.content.metadata.as_ref().map(|widget| { - let content = self.render_metadata_widget(widget, is_in_list); - - container(content) - .width( - if is_in_list { - Length::Fill - } else { - Length::FillPortion(2) - }, - ) - .height( - if is_in_list { - Length::FillPortion(3) - } else { - Length::Fill - }, - ) - .themed(ContainerStyle::DetailMetadata) - }); - - let content_element = widget.content.content.as_ref().map(|widget| { - let content_element: Element<_> = container(self.render_content_widget(widget, false)) - .width(Length::Fill) - .themed(ContainerStyle::DetailContentInner); - - let content_element: Element<_> = scrollable(content_element).width(Length::Fill).into(); - - let content_element: Element<_> = container(content_element) - .width( - if is_in_list { - Length::Fill - } else { - Length::FillPortion(3) - }, - ) - .height( - if is_in_list { - Length::FillPortion(3) - } else { - Length::Fill - }, - ) - .themed(ContainerStyle::DetailContent); - - content_element - }); - - let separator = if is_in_list { - horizontal_rule(1).into() - } else { - vertical_rule(1).into() - }; - - let list_fn = |vec| { - if is_in_list { - column(vec).into() - } else { - row(vec).into() - } - }; - - let content: Element<_> = match (content_element, metadata_element) { - (Some(content_element), Some(metadata_element)) => { - list_fn(vec![content_element, separator, metadata_element]) - } - (Some(content_element), None) => list_fn(vec![content_element]), - (None, Some(metadata_element)) => list_fn(vec![metadata_element]), - (None, None) => list_fn(vec![]), - }; - - content - } - - fn render_text_field_widget<'a>(&self, widget: &TextFieldWidget) -> Element<'a, ComponentWidgetEvent> { - let widget_id = widget.__id__; - let TextFieldState { state_value, .. } = self.text_field_state(widget.__id__); - - text_input("", state_value) - .on_input(move |value| ComponentWidgetEvent::OnChangeTextField { widget_id, value }) - .themed(TextInputStyle::FormInput) - } - - fn render_password_field_widget<'a>(&self, widget: &PasswordFieldWidget) -> Element<'a, ComponentWidgetEvent> { - let widget_id = widget.__id__; - let TextFieldState { state_value, .. } = self.text_field_state(widget_id); - - text_input("", state_value) - .secure(true) - .on_input(move |value| ComponentWidgetEvent::OnChangePasswordField { widget_id, value }) - .themed(TextInputStyle::FormInput) - } - - fn render_checkbox_widget<'a>(&self, widget: &CheckboxWidget) -> Element<'a, ComponentWidgetEvent> { - let widget_id = widget.__id__; - let CheckboxState { state_value } = self.checkbox_state(widget_id); - - checkbox(widget.title.as_deref().unwrap_or_default(), state_value.to_owned()) - .on_toggle(move |value| ComponentWidgetEvent::ToggleCheckbox { widget_id, value }) - .into() - } - - fn render_date_picker_widget<'a>(&self, widget: &DatePickerWidget) -> Element<'a, ComponentWidgetEvent> { - let widget_id = widget.__id__; - let DatePickerState { - state_value, - show_picker, - } = self.date_picker_state(widget.__id__); - - let button_text = text(state_value.to_string()).shaping(Shaping::Advanced); - - let button = button(button_text).on_press(ComponentWidgetEvent::ToggleDatePicker { - widget_id: widget.__id__, - }); - - // TODO unable to customize buttons here, split to separate button styles - // DatePickerUnderlay, - // DatePickerOverlay, - - date_picker( - show_picker.to_owned(), - state_value.to_owned(), - button, - ComponentWidgetEvent::CancelDatePicker { widget_id }, - move |date| { - ComponentWidgetEvent::SubmitDatePicker { - widget_id, - value: date.to_string(), - } - }, - ) - .themed(DatePickerStyle::Default) - } - - fn render_select_widget<'a>(&self, widget: &SelectWidget) -> Element<'a, ComponentWidgetEvent> { - let widget_id = widget.__id__; - let SelectState { state_value } = self.select_state(widget_id); - - let items: Vec<_> = widget - .content - .ordered_members - .iter() - .map(|members| { - match members { - SelectWidgetOrderedMembers::SelectItem(widget) => { - SelectItem { - value: widget.value.to_owned(), - label: widget.content.text.join(""), - } - } - } - }) - .collect(); - - let state_value = state_value - .clone() - .map(|value| items.iter().find(|item| item.value == value)) - .flatten() - .map(|value| value.clone()); - - pick_list(items, state_value, move |item| { - ComponentWidgetEvent::SelectPickList { - widget_id, - value: item.value, - } - }) - .themed(PickListStyle::Default) - } - - fn render_separator_widget<'a>(&self, _widget: &SeparatorWidget) -> Element<'a, ComponentWidgetEvent> { - horizontal_rule(1).into() - } - - fn render_form_widget<'a>( - &self, - widget: &FormWidget, - plugin_view_state: &PluginViewState, - entrypoint_name: &str, - action_shortcuts: &HashMap, - ) -> Element<'a, ComponentWidgetEvent> { - let widget_id = widget.__id__; - let RootState { show_action_panel, .. } = self.root_state(widget_id); - - let items: Vec> = widget - .content - .ordered_members - .iter() - .map(|members| { - fn render_field<'c, 'd>( - field: Element<'c, ComponentWidgetEvent>, - label: &'d Option, - ) -> Element<'c, ComponentWidgetEvent> { - let before_or_label: Element<_> = match label { - None => Space::with_width(Length::FillPortion(2)).into(), - Some(label) => { - let label: Element<_> = text(label.to_string()) - .shaping(Shaping::Advanced) - .align_x(Horizontal::Right) - .width(Length::Fill) - .into(); - - container(label) - .width(Length::FillPortion(2)) - .themed(ContainerStyle::FormInputLabel) - } - }; - - let form_input = container(field).width(Length::FillPortion(3)).into(); - - let after = Space::with_width(Length::FillPortion(2)).into(); - - let content = vec![before_or_label, form_input, after]; - - let row: Element<_> = row(content).align_y(Alignment::Center).themed(RowStyle::FormInput); - - row - } - - match members { - FormWidgetOrderedMembers::Separator(widget) => self.render_separator_widget(widget), - FormWidgetOrderedMembers::TextField(widget) => { - render_field(self.render_text_field_widget(widget), &widget.label) - } - FormWidgetOrderedMembers::PasswordField(widget) => { - render_field(self.render_password_field_widget(widget), &widget.label) - } - FormWidgetOrderedMembers::Checkbox(widget) => { - render_field(self.render_checkbox_widget(widget), &widget.label) - } - FormWidgetOrderedMembers::DatePicker(widget) => { - render_field(self.render_date_picker_widget(widget), &widget.label) - } - FormWidgetOrderedMembers::Select(widget) => { - render_field(self.render_select_widget(widget), &widget.label) - } - } - }) - .collect(); - - let content: Element<_> = column(items).into(); - - let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::FormInner); - - let content: Element<_> = scrollable(content).width(Length::Fill).into(); - - let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::Form); - - self.render_plugin_root( - *show_action_panel, - widget_id, - None, - &None, - &widget.content.actions, - content, - widget.is_loading.unwrap_or(false), - plugin_view_state, - entrypoint_name, - action_shortcuts, - ) - } - - fn render_inline_separator_widget<'a>(&self, widget: &InlineSeparatorWidget) -> Element<'a, ComponentWidgetEvent> { - match &widget.icon { - None => vertical_rule(1).into(), - Some(icon) => { - let top_rule: Element<_> = vertical_rule(1).into(); - - let top_rule = container(top_rule).align_x(Horizontal::Center).into(); - - let icon = value(icon_to_bootstrap(icon)) - .font(BOOTSTRAP_FONT) - .size(45) - .themed(TextStyle::InlineSeparator); - - let bot_rule: Element<_> = vertical_rule(1).into(); - - let bot_rule = container(bot_rule).align_x(Horizontal::Center).into(); - - column([top_rule, icon, bot_rule]).align_x(Alignment::Center).into() - } - } - } - - fn render_inline_widget<'a>( - &self, - widget: &InlineWidget, - plugin_name: &str, - entrypoint_name: &str, - ) -> Element<'a, ComponentWidgetEvent> { - let name: Element<_> = text(format!("{} - {}", plugin_name, entrypoint_name)) - .shaping(Shaping::Advanced) - .themed(TextStyle::InlineName); - - let name: Element<_> = container(name).themed(ContainerStyle::InlineName); - - let content: Vec> = widget - .content - .ordered_members - .iter() - .map(|members| { - match members { - InlineWidgetOrderedMembers::Content(widget) => { - let element = self.render_content_widget(widget, true); - - container(element).into() - } - InlineWidgetOrderedMembers::InlineSeparator(widget) => self.render_inline_separator_widget(widget), - } - }) - .collect(); - - let content: Element<_> = row(content).into(); - - let content: Element<_> = container(content).themed(ContainerStyle::InlineInner); - - let content: Element<_> = column(vec![name, content]).width(Length::Fill).into(); - - let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::Inline); - - content - } - - fn render_empty_view_widget<'a>(&self, widget: &EmptyViewWidget) -> Element<'a, ComponentWidgetEvent> { - let image: Option> = widget - .image - .as_ref() - .map(|image| render_image(self.images, widget.__id__, image, Some(TextStyle::EmptyViewSubtitle))); - - let title: Element<_> = text(widget.title.to_string()).shaping(Shaping::Advanced).into(); - - let subtitle: Element<_> = match &widget.description { - None => horizontal_space().into(), - Some(subtitle) => { - text(subtitle.to_string()) - .shaping(Shaping::Advanced) - .themed(TextStyle::EmptyViewSubtitle) - } - }; - - let mut content = vec![title, subtitle]; - if let Some(image) = image { - let image: Element<_> = container(image).themed(ContainerStyle::EmptyViewImage); - - content.insert(0, image) - } - - let content: Element<_> = column(content).align_x(Alignment::Center).into(); - - container(content) - .width(Length::Fill) - .height(Length::Fill) - .align_x(Horizontal::Center) - .align_y(Vertical::Center) - .into() - } - - fn render_search_bar_widget<'a>(&self, widget: &SearchBarWidget) -> Element<'a, ComponentWidgetEvent> { - let widget_id = widget.__id__; - let TextFieldState { - state_value, - text_input_id, - } = self.text_field_state(widget_id); - - text_input(widget.placeholder.as_deref().unwrap_or_default(), state_value) - .id(text_input_id.clone()) - .ignore_with_modifiers(true) - .on_input(move |value| ComponentWidgetEvent::OnChangeSearchBar { widget_id, value }) - .themed(TextInputStyle::PluginSearchBar) - } - - fn render_list_widget<'a>( - &self, - list_widget: &ListWidget, - plugin_view_state: &PluginViewState, - entrypoint_name: &str, - action_shortcuts: &HashMap, - ) -> Element<'a, ComponentWidgetEvent> { - let widget_id = list_widget.__id__; - let RootState { - show_action_panel, - focused_item, - } = self.root_state(widget_id); - - let mut pending: Vec<&ListItemWidget> = vec![]; - let mut items: Vec> = vec![]; - let index_counter = &Cell::new(0); - let mut first_section = true; - - for members in &list_widget.content.ordered_members { - match &members { - ListWidgetOrderedMembers::ListItem(widget) => { - first_section = false; - pending.push(widget) - } - ListWidgetOrderedMembers::ListSection(widget) => { - if !pending.is_empty() { - let content: Vec<_> = pending - .iter() - .map(|widget| self.render_list_item_widget(widget, focused_item.index, index_counter)) - .collect(); - - let content: Element<_> = column(content).into(); - - items.push(content); - - pending = vec![]; - } - - items.push(self.render_list_section_widget( - widget, - focused_item.index, - index_counter, - first_section, - )); - - first_section = false; - } - } - } - - if !pending.is_empty() { - let content: Vec<_> = pending - .iter() - .map(|widget| self.render_list_item_widget(widget, focused_item.index, index_counter)) - .collect(); - - let content: Element<_> = column(content).into(); - - items.push(content); - } - - let content = if items.is_empty() { - match &list_widget.content.empty_view { - Some(widget) => self.render_empty_view_widget(widget), - None => horizontal_space().into(), - } - } else { - let content: Element<_> = column(items).width(Length::Fill).into(); - - let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::ListInner); - - let content: Element<_> = scrollable(content) - .id(focused_item.scrollable_id.clone()) - .width(Length::Fill) - .into(); - - let content: Element<_> = container(content) - .width(Length::FillPortion(3)) - .themed(ContainerStyle::List); - - content - }; - - let mut elements = vec![content]; - - if let Some(detail) = &list_widget.content.detail { - let detail = self.render_detail_widget(detail, true); - - let detail: Element<_> = container(detail).width(Length::FillPortion(5)).into(); - - let separator: Element<_> = vertical_rule(1).into(); - - elements.push(separator); - - elements.push(detail); - } - - let content: Element<_> = row(elements).height(Length::Fill).into(); - - let focused_item_id = ComponentWidgets::list_focused_item_id(focused_item, list_widget); - - self.render_plugin_root( - *show_action_panel, - widget_id, - focused_item_id, - &list_widget.content.search_bar, - &list_widget.content.actions, - content, - list_widget.is_loading.unwrap_or(false), - plugin_view_state, - entrypoint_name, - action_shortcuts, - ) - } - - fn render_list_section_widget<'a>( - &self, - widget: &ListSectionWidget, - item_focus_index: Option, - index_counter: &Cell, - first_section: bool, - ) -> Element<'a, ComponentWidgetEvent> { - let content: Vec<_> = widget - .content - .ordered_members - .iter() - .map(|members| { - match members { - ListSectionWidgetOrderedMembers::ListItem(widget) => { - self.render_list_item_widget(widget, item_focus_index, index_counter) - } - } - }) - .collect(); - - let content = column(content).into(); - - let section_title_style = if first_section { - RowStyle::ListFirstSectionTitle - } else { - RowStyle::ListSectionTitle - }; - - render_section( - content, - Some(&widget.title), - &widget.subtitle, - section_title_style, - TextStyle::ListSectionTitle, - TextStyle::ListSectionSubtitle, - ) - } - - fn render_list_item_widget<'a>( - &self, - widget: &ListItemWidget, - item_focus_index: Option, - index_counter: &Cell, - ) -> Element<'a, ComponentWidgetEvent> { - let icon: Option> = widget - .icon - .as_ref() - .map(|icon| render_image(self.images, widget.__id__, icon, None)); - - let title: Element<_> = text(widget.title.to_string()).shaping(Shaping::Advanced).into(); - let title: Element<_> = container(title).themed(ContainerStyle::ListItemTitle); - - let mut content = vec![title]; - - if let Some(icon) = icon { - let icon: Element<_> = container(icon).themed(ContainerStyle::ListItemIcon); - - content.insert(0, icon) - } - - if let Some(subtitle) = &widget.subtitle { - let subtitle: Element<_> = text(subtitle.to_string()) - .shaping(Shaping::Advanced) - .themed(TextStyle::ListItemSubtitle); - let subtitle: Element<_> = container(subtitle).themed(ContainerStyle::ListItemSubtitle); - - content.push(subtitle) - } - - if widget.content.accessories.len() > 0 { - let accessories: Vec> = widget - .content - .accessories - .iter() - .map(|accessory| { - match accessory { - ListItemAccessories::_0(widget) => render_text_accessory(self.images, widget), - ListItemAccessories::_1(widget) => render_icon_accessory(self.images, widget), - } - }) - .collect(); - - let accessories: Element<_> = row(accessories).into(); - - let space = horizontal_space().into(); - - content.push(space); - content.push(accessories); - } - - let content: Element<_> = row(content).align_y(Alignment::Center).into(); - - let style = match item_focus_index { - None => ButtonStyle::ListItem, - Some(focused_index) => { - if focused_index == index_counter.get() { - ButtonStyle::ListItemFocused - } else { - ButtonStyle::ListItem - } - } - }; - - index_counter.set(index_counter.get() + 1); - - let action_ids = self.get_action_ids(); - let primary_action = action_ids.first(); - - let on_press_msg = match primary_action { - None => ComponentWidgetEvent::Noop, - Some(widget_id) => { - ComponentWidgetEvent::RunPrimaryAction { - widget_id: *widget_id, - id: Some(widget.id.clone()), - } - } - }; - - button(content).on_press(on_press_msg).width(Length::Fill).themed(style) - } - - fn render_grid_widget<'a>( - &self, - grid_widget: &GridWidget, - plugin_view_state: &PluginViewState, - entrypoint_name: &str, - action_shortcuts: &HashMap, - ) -> Element<'a, ComponentWidgetEvent> { - let RootState { - show_action_panel, - focused_item, - } = self.root_state(grid_widget.__id__); - - let content = if grid_widget.content.ordered_members.is_empty() { - match &grid_widget.content.empty_view { - Some(widget) => self.render_empty_view_widget(widget), - None => horizontal_space().into(), - } - } else { - let mut pending: Vec<&GridItemWidget> = vec![]; - let mut items: Vec> = vec![]; - let index_counter = &Cell::new(0); - let mut first_section = true; - - for members in &grid_widget.content.ordered_members { - match &members { - GridWidgetOrderedMembers::GridItem(widget) => { - first_section = false; - pending.push(widget) - } - GridWidgetOrderedMembers::GridSection(widget) => { - if !pending.is_empty() { - let content = - self.render_grid(&pending, &grid_widget.columns, focused_item.index, index_counter); - - items.push(content); - - pending = vec![]; - } - - items.push(self.render_grid_section_widget( - widget, - focused_item.index, - index_counter, - first_section, - )); - - first_section = false; - } - } - } - - if !pending.is_empty() { - let content = self.render_grid(&pending, &grid_widget.columns, focused_item.index, index_counter); - - items.push(content); - } - - let content: Element<_> = column(items).into(); - - let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::GridInner); - - let content: Element<_> = scrollable(content) - .id(focused_item.scrollable_id.clone()) - .width(Length::Fill) - .into(); - - let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::Grid); - - content - }; - - let focused_item_id = ComponentWidgets::grid_focused_item_id(focused_item, grid_widget); - - self.render_plugin_root( - *show_action_panel, - grid_widget.__id__, - focused_item_id, - &grid_widget.content.search_bar, - &grid_widget.content.actions, - content, - grid_widget.is_loading.unwrap_or(false), - plugin_view_state, - entrypoint_name, - action_shortcuts, - ) - } - - fn render_grid_section_widget<'a>( - &self, - widget: &GridSectionWidget, - item_focus_index: Option, - index_counter: &Cell, - first_section: bool, - ) -> Element<'a, ComponentWidgetEvent> { - let items: Vec<_> = widget - .content - .ordered_members - .iter() - .map(|members| { - match members { - GridSectionWidgetOrderedMembers::GridItem(widget) => widget, - } - }) - .collect(); - - let content = self.render_grid(&items, &widget.columns, item_focus_index, index_counter); - - let section_title_style = if first_section { - RowStyle::GridFirstSectionTitle - } else { - RowStyle::GridSectionTitle - }; - - render_section( - content, - Some(&widget.title), - &widget.subtitle, - section_title_style, - TextStyle::GridSectionTitle, - TextStyle::GridSectionSubtitle, - ) - } - - fn render_grid_item_widget<'a>( - &self, - widget: &GridItemWidget, - item_focus_index: Option, - index_counter: &Cell, - grid_width: usize, - ) -> Element<'a, ComponentWidgetEvent> { - let height = match grid_width { - ..4 => 130, - 4 => 150, - 5 => 130, - 6 => 110, - 7 => 90, - 8 => 70, - 8.. => 50, - }; - - let content: Element<_> = container(self.render_content_widget(&widget.content.content, true)) - .height(height) - .into(); - - let style = match item_focus_index { - None => ButtonStyle::GridItem, - Some(focused_index) => { - if focused_index == index_counter.get() { - ButtonStyle::GridItemFocused - } else { - ButtonStyle::GridItem - } - } - }; - - index_counter.set(index_counter.get() + 1); - - let action_ids = self.get_action_ids(); - let primary_action = action_ids.first(); - - let on_press_msg = match primary_action { - None => ComponentWidgetEvent::Noop, - Some(widget_id) => { - ComponentWidgetEvent::RunPrimaryAction { - widget_id: *widget_id, - id: Some(widget.id.clone()), - } - } - }; - - let content: Element<_> = button(content).on_press(on_press_msg).width(Length::Fill).themed(style); - - let mut sub_content_left = vec![]; - - if let Some(title) = &widget.title { - // TODO text truncation when iced supports it - let title = text(title.to_string()) - .shaping(Shaping::Advanced) - .themed(TextStyle::GridItemTitle); - - sub_content_left.push(title); - } - - if let Some(subtitle) = &widget.subtitle { - let subtitle = text(subtitle.to_string()) - .shaping(Shaping::Advanced) - .themed(TextStyle::GridItemSubTitle); - - sub_content_left.push(subtitle); - } - - let mut sub_content_right = vec![]; - if let Some(widget) = &widget.content.accessory { - sub_content_right.push(render_icon_accessory(self.images, widget)); - } - - let sub_content_left: Element<_> = column(sub_content_left).width(Length::Fill).into(); - - let sub_content_right: Element<_> = column(sub_content_right).width(Length::Shrink).into(); - - let sub_content: Element<_> = row(vec![sub_content_left, sub_content_right]).themed(RowStyle::GridItemTitle); - - let content: Element<_> = column(vec![content, sub_content]).width(Length::Fill).into(); - - content - } - - fn render_grid<'a>( - &self, - items: &[&GridItemWidget], - /*aspect_ratio: Option<&str>,*/ - columns: &Option, - item_focus_index: Option, - index_counter: &Cell, - ) -> Element<'a, ComponentWidgetEvent> { - // TODO - // let (width, height) = match aspect_ratio { - // None => (1, 1), - // Some("1") => (1, 1), - // Some("3/2") => (3, 2), - // Some("2/3") => (2, 3), - // Some("4/3") => (4, 3), - // Some("3/4") => (3, 4), - // Some("16/9") => (16, 9), - // Some("9/16") => (9, 16), - // Some(value) => panic!("unsupported aspect_ratio {:?}", value) - // }; - - let grid_width = grid_width(columns); - - let rows: Vec> = items - .iter() - .map(|widget| self.render_grid_item_widget(widget, item_focus_index, index_counter, grid_width)) - .chunks(grid_width) - .into_iter() - .map(|row_items| { - let mut row_items: Vec<_> = row_items.collect(); - row_items.resize_with(grid_width, || horizontal_space().into()); - - grid_row(row_items).into() - }) - .collect(); - - let grid: Element<_> = grid(rows) - .width(Length::Fill) - .vertical_alignment(Vertical::Top) - .themed(GridStyle::Default); - - grid - } - - fn render_top_panel<'a>(&self, search_bar: &Option) -> Element<'a, ComponentWidgetEvent> { - let icon = value(Bootstrap::ArrowLeft).font(BOOTSTRAP_FONT); - - let back_button: Element<_> = button(icon) - .on_press(ComponentWidgetEvent::PreviousView) - .themed(ButtonStyle::RootTopPanelBackButton); - - let search_bar_element = search_bar - .as_ref() - .map(|widget| self.render_search_bar_widget(widget)) - .unwrap_or_else(|| Space::with_width(Length::FillPortion(3)).into()); - - let top_panel: Element<_> = row(vec![back_button, search_bar_element]) - .align_y(Alignment::Center) - .themed(RowStyle::RootTopPanel); - - let top_panel: Element<_> = container(top_panel) - .width(Length::Fill) - .themed(ContainerStyle::RootTopPanel); - - top_panel - } - - fn render_plugin_root<'a>( - &self, - show_action_panel: bool, - root_widget_id: UiWidgetId, - focused_item_id: Option, - search_bar: &Option, - action_panel: &Option, - content: Element<'a, ComponentWidgetEvent>, - is_loading: bool, - plugin_view_state: &PluginViewState, - entrypoint_name: &str, - action_shortcuts: &HashMap, - ) -> Element<'a, ComponentWidgetEvent> { - let top_panel = self.render_top_panel(search_bar); - - let top_separator = if is_loading { - LoadingBar::new().into() - } else { - horizontal_rule(1).into() - }; - - let mut action_panel = convert_action_panel(action_panel, &action_shortcuts); - - let primary_action = - action_panel - .as_mut() - .map(|panel| panel.find_first()) - .flatten() - .map(|(label, widget_id)| { - let shortcut = PhysicalShortcut { - physical_key: PhysicalKey::Enter, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: false, - }; - - (label.to_string(), widget_id, shortcut) - }); - - match plugin_view_state { - PluginViewState::None => { - render_root( - show_action_panel, - top_panel, - top_separator, - None, - content, - primary_action, - action_panel, - None::<&ScrollHandle>, - entrypoint_name, - || { - ComponentWidgetEvent::ToggleActionPanel { - widget_id: root_widget_id, - } - }, - |widget_id| { - ComponentWidgetEvent::RunPrimaryAction { - widget_id, - id: focused_item_id.clone(), - } - }, - |widget_id| { - ComponentWidgetEvent::ActionClick { - widget_id, - id: focused_item_id.clone(), - } - }, - || ComponentWidgetEvent::Noop, - ) - } - PluginViewState::ActionPanel { focused_action_item } => { - render_root( - show_action_panel, - top_panel, - top_separator, - None, - content, - primary_action, - action_panel, - Some(&focused_action_item), - entrypoint_name, - || { - ComponentWidgetEvent::ToggleActionPanel { - widget_id: root_widget_id, - } - }, - |widget_id| { - ComponentWidgetEvent::RunPrimaryAction { - widget_id, - id: focused_item_id.clone(), - } - }, - |widget_id| { - ComponentWidgetEvent::ActionClick { - widget_id, - id: focused_item_id.clone(), - } - }, - || ComponentWidgetEvent::Noop, - ) - } - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct SelectItem { - value: String, - label: String, -} - -impl Display for SelectItem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.label) - } -} - -fn render_metadata_item<'a>( - label: &str, - value: Element<'a, ComponentWidgetEvent>, - is_in_list: bool, -) -> Element<'a, ComponentWidgetEvent> { - let label: Element<_> = text(label.to_string()) - .shaping(Shaping::Advanced) - .themed(TextStyle::MetadataItemLabel); - - let label = container(label).themed(ContainerStyle::MetadataItemLabel); - - if is_in_list { - let space = horizontal_space().into(); - - let value = container(value).themed(ContainerStyle::MetadataItemValueInList); - - row(vec![label, space, value]).width(Length::Fill).into() - } else { - let value = container(value).themed(ContainerStyle::MetadataItemValue); - - column(vec![label, value]).into() - } -} - -fn grid_width(columns: &Option) -> usize { - columns.map(|value| value.trunc() as usize).unwrap_or(5) -} - -fn render_section<'a>( - content: Element<'a, ComponentWidgetEvent>, - title: Option<&str>, - subtitle: &Option, - theme_kind_title: RowStyle, - theme_kind_title_text: TextStyle, - theme_kind_subtitle_text: TextStyle, -) -> Element<'a, ComponentWidgetEvent> { - let mut title_content = vec![]; - - if let Some(title) = title { - let title: Element<_> = text(title.to_string()) - .shaping(Shaping::Advanced) - .size(15) - .themed(theme_kind_title_text); - - title_content.push(title) - } - - if let Some(subtitle) = subtitle { - let subtitle: Element<_> = text(subtitle.to_string()) - .shaping(Shaping::Advanced) - .size(15) - .themed(theme_kind_subtitle_text); - - title_content.push(subtitle) - } - - if title_content.is_empty() { - let space: Element<_> = horizontal_space().height(40).into(); - - title_content.push(space) - } - - let title_content = row(title_content).themed(theme_kind_title); - - column([title_content, content]).into() -} - -#[derive(Debug)] -pub struct ActionPanel { - pub title: Option, - pub items: Vec, -} - -impl ActionPanel { - pub fn action_count(&self) -> usize { - self.items.iter().map(|item| item.action_count()).sum() - } - - pub fn find_first(&self) -> Option<(String, UiWidgetId)> { - ActionPanelItem::find_first(&self.items) - } -} - -#[derive(Debug)] -pub enum ActionPanelItem { - Action { - label: String, - widget_id: UiWidgetId, - physical_shortcut: Option, - }, - ActionSection { - title: Option, - items: Vec, - }, -} - -impl ActionPanelItem { - fn action_count(&self) -> usize { - match self { - ActionPanelItem::Action { .. } => 1, - ActionPanelItem::ActionSection { items, .. } => items.iter().map(|item| item.action_count()).sum(), - } - } - - fn find_first(items: &[ActionPanelItem]) -> Option<(String, UiWidgetId)> { - for item in items { - match item { - ActionPanelItem::Action { label, widget_id, .. } => return Some((label.to_string(), *widget_id)), - ActionPanelItem::ActionSection { items, .. } => { - if let Some(item) = Self::find_first(items) { - return Some(item); - } - } - } - } - - None - } -} - -fn convert_action_panel( - action_panel: &Option, - action_shortcuts: &HashMap, -) -> Option { - match action_panel { - Some(ActionPanelWidget { content, title, .. }) => { - fn action_widget_to_action( - ActionWidget { __id__, id, label }: &ActionWidget, - action_shortcuts: &HashMap, - ) -> ActionPanelItem { - let physical_shortcut: Option = - id.as_ref().map(|id| action_shortcuts.get(id)).flatten().cloned(); - - ActionPanelItem::Action { - label: label.clone(), - widget_id: *__id__, - physical_shortcut, - } - } - - let items = content - .ordered_members - .iter() - .map(|members| { - match members { - ActionPanelWidgetOrderedMembers::Action(widget) => { - action_widget_to_action(widget, action_shortcuts) - } - ActionPanelWidgetOrderedMembers::ActionPanelSection(ActionPanelSectionWidget { - content, - title, - .. - }) => { - let section_items = content - .ordered_members - .iter() - .map(|members| { - match members { - ActionPanelSectionWidgetOrderedMembers::Action(widget) => { - action_widget_to_action(widget, action_shortcuts) - } - } - }) - .collect(); - - ActionPanelItem::ActionSection { - title: title.clone(), - items: section_items, - } - } - } - }) - .collect(); - - Some(ActionPanel { - title: title.clone(), - items, - }) - } - _ => None, - } -} - -fn render_action_panel_items<'a, T: 'a + Clone>( - root: bool, - title: Option, - items: Vec, - action_panel_focus_index: Option, - on_action_click: &dyn Fn(UiWidgetId) -> T, - index_counter: &Cell, -) -> Vec> { - let mut columns = vec![]; - - if let Some(title) = title { - let text: Element<_> = text(title) - .shaping(Shaping::Advanced) - .font(Font { - weight: Weight::Bold, - ..Font::DEFAULT - }) - .themed(TextStyle::ActionSectionTitle); - - let text = container(text).themed( - if root { - ContainerStyle::ActionPanelTitle - } else { - ContainerStyle::ActionSectionTitle - }, - ); - - columns.push(text) - } else { - if !root { - let separator: Element<_> = horizontal_rule(1).themed(RuleStyle::ActionPanel); - - columns.push(separator); - } - } - - for item in items { - match item { - ActionPanelItem::Action { - label, - widget_id, - physical_shortcut, - } => { - let physical_shortcut = match index_counter.get() { - 0 => { - Some(PhysicalShortcut { - // primary - physical_key: PhysicalKey::Enter, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: false, - }) - } - 1 => { - Some(PhysicalShortcut { - // secondary - physical_key: PhysicalKey::Enter, - modifier_shift: true, - modifier_control: false, - modifier_alt: false, - modifier_meta: false, - }) - } - _ => physical_shortcut, - }; - - let shortcut_element: Option> = - physical_shortcut.as_ref().map(|shortcut| render_shortcut(shortcut)); - - let content: Element<_> = if let Some(shortcut_element) = shortcut_element { - let text: Element<_> = text(label).shaping(Shaping::Advanced).into(); - - let space: Element<_> = horizontal_space().into(); - - row([text, space, shortcut_element]).align_y(Alignment::Center).into() - } else { - text(label).shaping(Shaping::Advanced).into() - }; - - let style = match action_panel_focus_index { - None => ButtonStyle::Action, - Some(focused_index) => { - if focused_index == index_counter.get() { - ButtonStyle::ActionFocused - } else { - ButtonStyle::Action - } - } - }; - - index_counter.set(index_counter.get() + 1); - - let content = button(content) - .on_press(on_action_click(widget_id)) - .width(Length::Fill) - .themed(style); - - columns.push(content); - } - ActionPanelItem::ActionSection { title, items } => { - let content = render_action_panel_items( - false, - title, - items, - action_panel_focus_index, - on_action_click, - index_counter, - ); - - for content in content { - columns.push(content); - } - } - }; - } - - columns -} - -fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T>( - action_panel: ActionPanel, - on_action_click: F, - action_panel_scroll_handle: &ScrollHandle, -) -> Element<'a, T> { - let columns = render_action_panel_items( - true, - action_panel.title, - action_panel.items, - action_panel_scroll_handle.index, - &on_action_click, - &Cell::new(0), - ); - - let actions: Element<_> = column(columns).into(); - - let actions: Element<_> = scrollable(actions) - .id(action_panel_scroll_handle.scrollable_id.clone()) - .width(Length::Fill) - .into(); - - container(actions).themed(ContainerStyle::ActionPanel) -} - -pub fn render_root<'a, T: 'a + Clone>( - show_action_panel: bool, - top_panel: Element<'a, T>, - top_separator: Element<'a, T>, - toast_text: Option<&str>, - content: Element<'a, T>, - primary_action: Option<(String, UiWidgetId, PhysicalShortcut)>, - action_panel: Option, - action_panel_scroll_handle: Option<&ScrollHandle>, - entrypoint_name: &str, - on_panel_toggle_click: impl Fn() -> T, - on_panel_primary_click: impl Fn(UiWidgetId) -> T, - on_action_click: impl Fn(UiWidgetId) -> T, - noop_msg: impl Fn() -> T, -) -> Element<'a, T> { - let entrypoint_name: Element<_> = text(entrypoint_name.to_string()).shaping(Shaping::Advanced).into(); - - let panel_height = 16 + 8 + 2; // TODO get value from theme - - let primary_action = match primary_action { - Some((label, widget_id, shortcut)) => { - let label: Element<_> = text(label) - .shaping(Shaping::Advanced) - .themed(TextStyle::RootBottomPanelPrimaryActionText); - - let label: Element<_> = container(label).themed(ContainerStyle::RootBottomPanelPrimaryActionText); - - let shortcut = render_shortcut(&shortcut); - - let content: Element<_> = row(vec![label, shortcut]).into(); - - let content: Element<_> = button(content) - .on_press(on_panel_primary_click(widget_id)) - .themed(ButtonStyle::RootBottomPanelPrimaryActionButton); - - let content: Element<_> = container(content).themed(ContainerStyle::RootBottomPanelPrimaryActionButton); - - Some(content) - } - None => None, - }; - - let (hide_action_panel, action_panel, bottom_panel) = match action_panel { - Some(action_panel) => { - let actions_text: Element<_> = text("Actions").themed(TextStyle::RootBottomPanelActionToggleText); - - let actions_text: Element<_> = - container(actions_text).themed(ContainerStyle::RootBottomPanelActionToggleText); - - let shortcut = render_shortcut(&PhysicalShortcut { - physical_key: PhysicalKey::KeyK, - modifier_shift: false, - modifier_control: false, - modifier_alt: true, - modifier_meta: false, - }); - - let mut bottom_panel_content = vec![entrypoint_name]; - - if let Some(toast_text) = toast_text { - let toast_text = text(toast_text.to_string()).into(); - - bottom_panel_content.push(toast_text); - } - - let space = horizontal_space().into(); - - bottom_panel_content.push(space); - - if let Some(primary_action) = primary_action { - bottom_panel_content.push(primary_action); - - let rule: Element<_> = vertical_rule(1).class(RuleStyle::PrimaryActionSeparator).into(); - - let rule: Element<_> = container(rule) - .width(Length::Shrink) - .height(panel_height) - .max_height(panel_height) - .into(); - - bottom_panel_content.push(rule); - } - - let action_panel_toggle_content: Element<_> = row(vec![actions_text, shortcut]).into(); - - let action_panel_toggle: Element<_> = button(action_panel_toggle_content) - .on_press(on_panel_toggle_click()) - .themed(ButtonStyle::RootBottomPanelActionToggleButton); - - bottom_panel_content.push(action_panel_toggle); - - let bottom_panel: Element<_> = row(bottom_panel_content) - .align_y(Alignment::Center) - .themed(RowStyle::RootBottomPanel); - - (!show_action_panel, Some(action_panel), bottom_panel) - } - None => { - let space: Element<_> = Space::new(Length::Fill, panel_height).into(); - - let mut bottom_panel_content = vec![]; - - if let Some(toast_text) = toast_text { - let toast_text = text(toast_text.to_string()).into(); - - bottom_panel_content.push(toast_text); - } else { - bottom_panel_content.push(entrypoint_name); - } - - bottom_panel_content.push(space); - - if let Some(primary_action) = primary_action { - bottom_panel_content.push(primary_action); - } - - let bottom_panel: Element<_> = row(bottom_panel_content) - .align_y(Alignment::Center) - .themed(RowStyle::RootBottomPanel); - - (true, None, bottom_panel) - } - }; - - let bottom_panel: Element<_> = container(bottom_panel) - .width(Length::Fill) - .themed(ContainerStyle::RootBottomPanel); - - let content: Element<_> = container(content) - .width(Length::Fill) - .height(Length::Fill) - .themed(ContainerStyle::RootInner); - - let content: Element<_> = column(vec![top_panel, top_separator, content, bottom_panel]).into(); - - let content: Element<_> = mouse_area(content) - .on_press( - if hide_action_panel { - noop_msg() - } else { - on_panel_toggle_click() - }, - ) - .into(); - - let mut content = vec![content]; - - if let (Some(action_panel), Some(action_panel_scroll_handle)) = (action_panel, action_panel_scroll_handle) { - if !hide_action_panel { - let action_panel = render_action_panel(action_panel, on_action_click, action_panel_scroll_handle); - - let action_panel: Element<_> = container(action_panel) - .padding(gauntlet_common_ui::padding(0.0, 8.0, 48.0, 0.0)) - .align_right(Length::Fill) - .align_bottom(Length::Fill) - .into(); - - content.push(action_panel); - } - }; - - stack(content).into() -} - -fn render_shortcut<'a, T: 'a>(shortcut: &PhysicalShortcut) -> Element<'a, T> { - let mut result = vec![]; - - let (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) = - shortcut_to_text(shortcut); - - fn apply_modifier<'result, 'element, T: 'element>( - result: &'result mut Vec>, - modifier: Option>, - ) { - if let Some(modifier) = modifier { - let modifier: Element<_> = container(modifier).themed(ContainerStyle::ActionShortcutModifier); - - let modifier: Element<_> = container(modifier).themed(ContainerStyle::ActionShortcutModifiersInit); - - result.push(modifier); - } - } - - apply_modifier(&mut result, meta_modifier_text); - apply_modifier(&mut result, control_modifier_text); - apply_modifier(&mut result, shift_modifier_text); - apply_modifier(&mut result, alt_modifier_text); - - let key_name: Element<_> = container(key_name).themed(ContainerStyle::ActionShortcutModifier); - - result.push(key_name); - - row(result).themed(RowStyle::ActionShortcut) -} - -fn render_image<'a, T: 'a + Clone>( - images: &HashMap>, - widget_id: UiWidgetId, - image_data: &ImageLike, - icon_style: Option, -) -> Element<'a, T> { - match image_data { - ImageLike::ImageSource(_) => { - match images.get(&widget_id) { - Some(bytes) => image(Handle::from_bytes(bytes.clone())).into(), - None => horizontal_space().into(), - } - } - ImageLike::Icons(icon) => { - match icon_style { - None => value(icon_to_bootstrap(icon)).font(BOOTSTRAP_FONT).into(), - Some(icon_style) => value(icon_to_bootstrap(icon)).font(BOOTSTRAP_FONT).themed(icon_style), - } - } - } -} - -pub fn render_icon_accessory<'a, T: 'a + Clone>( - images: &HashMap>, - widget: &IconAccessoryWidget, -) -> Element<'a, T> { - let icon = render_image(images, widget.__id__, &widget.icon, Some(TextStyle::IconAccessory)); - - let content = container(icon) - .align_x(Horizontal::Center) - .align_y(Vertical::Center) - .themed(ContainerStyle::IconAccessory); - - match widget.tooltip.as_ref() { - None => content, - Some(tooltip_text) => { - let tooltip_text: Element<_> = text(tooltip_text.to_string()).shaping(Shaping::Advanced).into(); - - tooltip(content, tooltip_text, Position::Top).themed(TooltipStyle::Tooltip) - } - } -} - -pub fn render_text_accessory<'a, T: 'a + Clone>( - images: &HashMap>, - widget: &TextAccessoryWidget, -) -> Element<'a, T> { - let icon: Option> = widget - .icon - .as_ref() - .map(|icon| render_image(images, widget.__id__, icon, Some(TextStyle::TextAccessory))); - - let text_content: Element<_> = text(widget.text.to_string()) - .shaping(Shaping::Advanced) - .themed(TextStyle::TextAccessory); - - let mut content: Vec> = vec![]; - - if let Some(icon) = icon { - let icon: Element<_> = container(icon).themed(ContainerStyle::TextAccessoryIcon); - - content.push(icon) - } - - content.push(text_content); - - let content: Element<_> = row(content).align_y(Alignment::Center).into(); - - let content = container(content) - .align_x(Horizontal::Center) - .align_y(Vertical::Center) - .themed(ContainerStyle::TextAccessory); - - match widget.tooltip.as_ref() { - None => content, - Some(tooltip_text) => { - let tooltip_text: Element<_> = text(tooltip_text.to_string()).shaping(Shaping::Advanced).into(); - - tooltip(content, tooltip_text, Position::Top).themed(TooltipStyle::Tooltip) - } - } -} - -#[derive(Clone, Debug)] -pub enum ComponentWidgetEvent { - LinkClick { - widget_id: UiWidgetId, - href: String, - }, - TagClick { - widget_id: UiWidgetId, - }, - ActionClick { - widget_id: UiWidgetId, - id: Option, - }, - RunAction { - widget_id: UiWidgetId, - id: Option, - }, - ToggleDatePicker { - widget_id: UiWidgetId, - }, - OnChangeTextField { - widget_id: UiWidgetId, - value: String, - }, - OnChangePasswordField { - widget_id: UiWidgetId, - value: String, - }, - OnChangeSearchBar { - widget_id: UiWidgetId, - value: String, - }, - SubmitDatePicker { - widget_id: UiWidgetId, - value: String, - }, - CancelDatePicker { - widget_id: UiWidgetId, - }, - ToggleCheckbox { - widget_id: UiWidgetId, - value: bool, - }, - SelectPickList { - widget_id: UiWidgetId, - value: String, - }, - ToggleActionPanel { - widget_id: UiWidgetId, - }, - FocusListItem { - list_widget_id: UiWidgetId, - item_id: Option, - }, - FocusGridItem { - grid_widget_id: UiWidgetId, - item_id: Option, - }, - PreviousView, - RunPrimaryAction { - widget_id: UiWidgetId, - id: Option, - }, - Noop, -} - -include!(concat!(env!("OUT_DIR"), "/components.rs")); - -impl ComponentWidgetEvent { - pub fn handle(self, _plugin_id: PluginId, state: Option<&mut ComponentWidgetState>) -> Option { - match self { - ComponentWidgetEvent::LinkClick { widget_id: _, href } => Some(UiViewEvent::Open { href }), - ComponentWidgetEvent::TagClick { widget_id } => Some(create_metadata_tag_item_on_click_event(widget_id)), - ComponentWidgetEvent::RunAction { widget_id, id } | ComponentWidgetEvent::ActionClick { widget_id, id } => { - Some(create_action_on_action_event(widget_id, id)) - } - ComponentWidgetEvent::ToggleDatePicker { widget_id } => { - let state = state.expect("state should always exist for "); - - let ComponentWidgetState::DatePicker(DatePickerState { - state_value: _, - show_picker, - }) = state - else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; - - *show_picker = !*show_picker; - None - } - ComponentWidgetEvent::CancelDatePicker { widget_id } => { - let state = state.expect("state should always exist for "); - - let ComponentWidgetState::DatePicker(DatePickerState { - state_value: _, - show_picker, - }) = state - else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; - - *show_picker = false; - None - } - ComponentWidgetEvent::SubmitDatePicker { widget_id, value } => { - let state = state.expect("state should always exist for "); - - { - let ComponentWidgetState::DatePicker(DatePickerState { - state_value: _, - show_picker, - }) = state - else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; - - *show_picker = false; - } - - Some(create_date_picker_on_change_event(widget_id, Some(value))) - } - ComponentWidgetEvent::ToggleCheckbox { widget_id, value } => { - let state = state.expect("state should always exist for "); - - { - let ComponentWidgetState::Checkbox(CheckboxState { state_value }) = state else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; - - *state_value = !*state_value; - } - - Some(create_checkbox_on_change_event(widget_id, value)) - } - ComponentWidgetEvent::SelectPickList { widget_id, value } => { - let state = state.expect("state should always exist for "); - - { - let ComponentWidgetState::Select(SelectState { state_value }) = state else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; - - *state_value = Some(value.clone()); - } - - Some(create_select_on_change_event(widget_id, Some(value))) - } - ComponentWidgetEvent::OnChangeTextField { widget_id, value } => { - let state = state.expect("state should always exist for "); - - { - let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; - - *state_value = value.clone(); - } - - Some(create_text_field_on_change_event(widget_id, Some(value))) - } - ComponentWidgetEvent::OnChangePasswordField { widget_id, value } => { - let state = state.expect("state should always exist for "); - - { - let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; - - *state_value = value.clone(); - } - - Some(create_password_field_on_change_event(widget_id, Some(value))) - } - ComponentWidgetEvent::OnChangeSearchBar { widget_id, value } => { - let state = state.expect("state should always exist for "); - - { - let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; - - *state_value = value.clone(); - } - - Some(create_search_bar_on_change_event(widget_id, Some(value))) - } - ComponentWidgetEvent::ToggleActionPanel { .. } => { - Some(UiViewEvent::AppEvent { - event: AppMsg::ToggleActionPanel { keyboard: false }, - }) - } - ComponentWidgetEvent::FocusListItem { - list_widget_id, - item_id, - } => Some(create_list_on_item_focus_change_event(list_widget_id, item_id)), - ComponentWidgetEvent::FocusGridItem { - grid_widget_id, - item_id, - } => Some(create_grid_on_item_focus_change_event(grid_widget_id, item_id)), - ComponentWidgetEvent::Noop | ComponentWidgetEvent::PreviousView => { - panic!("widget_id on these events is not supposed to be called") - } - ComponentWidgetEvent::RunPrimaryAction { widget_id, id } => { - Some(UiViewEvent::AppEvent { - event: AppMsg::OnAnyActionPluginViewAnyPanel { widget_id, id }, - }) - } - } - } - - pub fn widget_id(&self) -> UiWidgetId { - match self { - ComponentWidgetEvent::LinkClick { widget_id, .. } => widget_id, - ComponentWidgetEvent::ActionClick { widget_id, .. } => widget_id, - ComponentWidgetEvent::RunAction { widget_id, .. } => widget_id, - ComponentWidgetEvent::TagClick { widget_id, .. } => widget_id, - ComponentWidgetEvent::ToggleDatePicker { widget_id, .. } => widget_id, - ComponentWidgetEvent::SubmitDatePicker { widget_id, .. } => widget_id, - ComponentWidgetEvent::CancelDatePicker { widget_id, .. } => widget_id, - ComponentWidgetEvent::ToggleCheckbox { widget_id, .. } => widget_id, - ComponentWidgetEvent::SelectPickList { widget_id, .. } => widget_id, - ComponentWidgetEvent::OnChangeTextField { widget_id, .. } => widget_id, - ComponentWidgetEvent::OnChangePasswordField { widget_id, .. } => widget_id, - ComponentWidgetEvent::OnChangeSearchBar { widget_id, .. } => widget_id, - ComponentWidgetEvent::ToggleActionPanel { widget_id } => widget_id, - ComponentWidgetEvent::FocusListItem { list_widget_id, .. } => list_widget_id, - ComponentWidgetEvent::FocusGridItem { grid_widget_id, .. } => grid_widget_id, - ComponentWidgetEvent::RunPrimaryAction { widget_id, .. } => widget_id, - ComponentWidgetEvent::Noop | ComponentWidgetEvent::PreviousView => { - panic!("widget_id on these events is not supposed to be called") - } - } - .to_owned() - } -} - -pub fn parse_date(value: &str) -> Option<(i32, u32, u32)> { - let ymd: Vec<_> = value.split("-").collect(); - - match ymd[..] { - [year, month, day] => { - let year = year.parse::(); - let month = month.parse::(); - let day = day.parse::(); - - match (year, month, day) { - (Ok(year), Ok(month), Ok(day)) => Some((year, month, day)), - _ => None, - } - } - _ => None, - } -} - -fn icon_to_bootstrap(icon: &Icons) -> Bootstrap { - match icon { - Icons::Airplane => Bootstrap::Airplane, - Icons::Alarm => Bootstrap::Alarm, - Icons::AlignCentre => Bootstrap::AlignCenter, - Icons::AlignLeft => Bootstrap::AlignStart, - Icons::AlignRight => Bootstrap::AlignEnd, - // Icons::Anchor => Bootstrap::, - Icons::ArrowClockwise => Bootstrap::ArrowClockwise, - Icons::ArrowCounterClockwise => Bootstrap::ArrowCounterclockwise, - Icons::ArrowDown => Bootstrap::ArrowDown, - Icons::ArrowLeft => Bootstrap::ArrowLeft, - Icons::ArrowRight => Bootstrap::ArrowRight, - Icons::ArrowUp => Bootstrap::ArrowUp, - Icons::ArrowLeftRight => Bootstrap::ArrowLeftRight, - Icons::ArrowsContract => Bootstrap::ArrowsAngleContract, - Icons::ArrowsExpand => Bootstrap::ArrowsAngleExpand, - Icons::AtSymbol => Bootstrap::At, - // Icons::BandAid => Bootstrap::Bandaid, - Icons::Cash => Bootstrap::Cash, - // Icons::BarChart => Bootstrap::BarChart, - // Icons::BarCode => Bootstrap::, - Icons::Battery => Bootstrap::Battery, - Icons::BatteryCharging => Bootstrap::BatteryCharging, - // Icons::BatteryDisabled => Bootstrap::, - Icons::Bell => Bootstrap::Bell, - Icons::BellDisabled => Bootstrap::BellSlash, - // Icons::Bike => Bootstrap::Bicycle, - // Icons::Binoculars => Bootstrap::Binoculars, - // Icons::Bird => Bootstrap::, - Icons::Bluetooth => Bootstrap::Bluetooth, - // Icons::Boat => Bootstrap::, - Icons::Bold => Bootstrap::TypeBold, - // Icons::Bolt => Bootstrap::, - // Icons::BoltDisabled => Bootstrap::, - Icons::Book => Bootstrap::Book, - Icons::Bookmark => Bootstrap::Bookmark, - Icons::Box => Bootstrap::Box, - // Icons::Brush => Bootstrap::Brush, - Icons::Bug => Bootstrap::Bug, - Icons::Building => Bootstrap::Building, - Icons::BulletPoints => Bootstrap::ListUl, - Icons::Calculator => Bootstrap::Calculator, - Icons::Calendar => Bootstrap::Calendar, - Icons::Camera => Bootstrap::Camera, - Icons::Car => Bootstrap::CarFront, - Icons::Cart => Bootstrap::Cart, - // Icons::Cd => Bootstrap::, - // Icons::Center => Bootstrap::, - Icons::Checkmark => Bootstrap::Checktwo, - // Icons::ChessPiece => Bootstrap::, - Icons::ChevronDown => Bootstrap::ChevronDown, - Icons::ChevronLeft => Bootstrap::ChevronLeft, - Icons::ChevronRight => Bootstrap::ChevronRight, - Icons::ChevronUp => Bootstrap::ChevronUp, - Icons::ChevronExpand => Bootstrap::ChevronExpand, - Icons::Circle => Bootstrap::Circle, - // Icons::CircleProgress100 => Bootstrap::, - // Icons::CircleProgress25 => Bootstrap::, - // Icons::CircleProgress50 => Bootstrap::, - // Icons::CircleProgress75 => Bootstrap::, - // Icons::ClearFormatting => Bootstrap::, - Icons::Clipboard => Bootstrap::Clipboard, - Icons::Clock => Bootstrap::Clock, - Icons::Cloud => Bootstrap::Cloud, - Icons::CloudLightning => Bootstrap::CloudLightning, - Icons::CloudRain => Bootstrap::CloudRain, - Icons::CloudSnow => Bootstrap::CloudSnow, - Icons::CloudSun => Bootstrap::CloudSun, - Icons::Code => Bootstrap::Code, - Icons::Gear => Bootstrap::Gear, - Icons::Coin => Bootstrap::Coin, - Icons::Command => Bootstrap::Command, - Icons::Compass => Bootstrap::Compass, - // Icons::ComputerChip => Bootstrap::, - // Icons::Contrast => Bootstrap::, - Icons::CreditCard => Bootstrap::CreditCard, - Icons::Crop => Bootstrap::Crop, - // Icons::Crown => Bootstrap::, - Icons::Document => Bootstrap::FileEarmark, - Icons::DocumentAdd => Bootstrap::FileEarmarkPlus, - Icons::DocumentDelete => Bootstrap::FileEarmarkX, - Icons::Dot => Bootstrap::Dot, - Icons::Download => Bootstrap::Download, - // Icons::Duplicate => Bootstrap::, - Icons::Eject => Bootstrap::Eject, - Icons::ThreeDots => Bootstrap::ThreeDots, - Icons::Envelope => Bootstrap::Envelope, - Icons::Eraser => Bootstrap::Eraser, - Icons::ExclamationMark => Bootstrap::ExclamationLg, - Icons::Eye => Bootstrap::Eye, - Icons::EyeDisabled => Bootstrap::EyeSlash, - Icons::EyeDropper => Bootstrap::Eyedropper, - Icons::Female => Bootstrap::GenderFemale, - Icons::Film => Bootstrap::Film, - Icons::Filter => Bootstrap::Filter, - Icons::Fingerprint => Bootstrap::Fingerprint, - Icons::Flag => Bootstrap::Flag, - Icons::Folder => Bootstrap::Folder, - Icons::FolderAdd => Bootstrap::FolderPlus, - Icons::FolderDelete => Bootstrap::FolderMinus, - Icons::Forward => Bootstrap::Forward, - Icons::GameController => Bootstrap::Controller, - Icons::Virus => Bootstrap::Virus, - Icons::Gift => Bootstrap::Gift, - Icons::Glasses => Bootstrap::Eyeglasses, - Icons::Globe => Bootstrap::Globe, - Icons::Hammer => Bootstrap::Hammer, - Icons::HardDrive => Bootstrap::DeviceHdd, - Icons::Headphones => Bootstrap::Headphones, - Icons::Heart => Bootstrap::Heart, - // Icons::HeartDisabled => Bootstrap::, - Icons::Heartbeat => Bootstrap::Activity, - Icons::Hourglass => Bootstrap::Hourglass, - Icons::House => Bootstrap::House, - Icons::Image => Bootstrap::Image, - Icons::Info => Bootstrap::InfoLg, - Icons::Italics => Bootstrap::TypeItalic, - Icons::Key => Bootstrap::Key, - Icons::Keyboard => Bootstrap::Keyboard, - Icons::Layers => Bootstrap::Layers, - // Icons::Leaf => Bootstrap::, - Icons::LightBulb => Bootstrap::Lightbulb, - Icons::LightBulbDisabled => Bootstrap::LightbulbOff, - Icons::Link => Bootstrap::LinkFourfivedeg, - Icons::List => Bootstrap::List, - Icons::Lock => Bootstrap::Lock, - // Icons::LockDisabled => Bootstrap::, - Icons::LockUnlocked => Bootstrap::Unlock, - // Icons::Logout => Bootstrap::, - // Icons::Lowercase => Bootstrap::, - // Icons::MagnifyingGlass => Bootstrap::, - Icons::Male => Bootstrap::GenderMale, - Icons::Map => Bootstrap::Map, - Icons::Maximize => Bootstrap::Fullscreen, - Icons::Megaphone => Bootstrap::Megaphone, - Icons::MemoryModule => Bootstrap::Memory, - Icons::MemoryStick => Bootstrap::UsbDrive, - Icons::Message => Bootstrap::Chat, - Icons::Microphone => Bootstrap::Mic, - Icons::MicrophoneDisabled => Bootstrap::MicMute, - Icons::Minimize => Bootstrap::FullscreenExit, - Icons::Minus => Bootstrap::Dash, - Icons::Mobile => Bootstrap::Phone, - // Icons::Monitor => Bootstrap::, - Icons::Moon => Bootstrap::Moon, - // Icons::Mountain => Bootstrap::, - Icons::Mouse => Bootstrap::Mouse, - Icons::Multiply => Bootstrap::X, - Icons::Music => Bootstrap::MusicNoteBeamed, - Icons::Network => Bootstrap::BroadcastPin, - Icons::Paperclip => Bootstrap::Paperclip, - Icons::Paragraph => Bootstrap::TextParagraph, - Icons::Pause => Bootstrap::Pause, - Icons::Pencil => Bootstrap::Pencil, - Icons::Person => Bootstrap::Person, - Icons::PersonAdd => Bootstrap::PersonAdd, - Icons::PersonRemove => Bootstrap::PersonDash, - Icons::Phone => Bootstrap::Telephone, - // Icons::PhoneRinging => Bootstrap::, - Icons::PieChart => Bootstrap::PieChart, - Icons::Capsule => Bootstrap::Capsule, - // Icons::Pin => Bootstrap::, - // Icons::PinDisabled => Bootstrap::, - Icons::Play => Bootstrap::Play, - Icons::Plug => Bootstrap::Plug, - Icons::Plus => Bootstrap::Plus, - // Icons::PlusMinusDivideMultiply => Bootstrap::, - Icons::Power => Bootstrap::Power, - Icons::Printer => Bootstrap::Printer, - Icons::QuestionMark => Bootstrap::QuestionLg, - Icons::Quotes => Bootstrap::Quote, - Icons::Receipt => Bootstrap::Receipt, - Icons::Repeat => Bootstrap::Repeat, - Icons::Reply => Bootstrap::Reply, - Icons::Rewind => Bootstrap::Rewind, - Icons::Rocket => Bootstrap::Rocket, - // Icons::Ruler => Bootstrap::, - Icons::Shield => Bootstrap::Shield, - Icons::Shuffle => Bootstrap::Shuffle, - Icons::Snippets => Bootstrap::BodyText, - Icons::Snowflake => Bootstrap::Snow, - // Icons::VolumeHigh => Bootstrap::VolumeUp, - // Icons::VolumeLow => Bootstrap::VolumeDown, - // Icons::VolumeOff => Bootstrap::VolumeOff, - // Icons::VolumeOn => Bootstrap::, - Icons::Star => Bootstrap::Star, - // Icons::StarDisabled => Bootstrap::, - Icons::Stop => Bootstrap::Stop, - Icons::Stopwatch => Bootstrap::Stopwatch, - Icons::StrikeThrough => Bootstrap::TypeStrikethrough, - Icons::Sun => Bootstrap::SunFill, // TODO why Sun isn't in iced_aw? - Icons::Scissors => Bootstrap::Scissors, - // Icons::Syringe => Bootstrap::, - Icons::Tag => Bootstrap::Tag, - Icons::Thermometer => Bootstrap::Thermometer, - Icons::Terminal => Bootstrap::Terminal, - Icons::Text => Bootstrap::Fonts, - Icons::TextCursor => Bootstrap::CursorText, - // Icons::TextSelection => Bootstrap::, - // Icons::Torch => Bootstrap::, - // Icons::Train => Bootstrap::, - Icons::Trash => Bootstrap::Trash, - Icons::Tree => Bootstrap::Tree, - Icons::Trophy => Bootstrap::Trophy, - Icons::People => Bootstrap::People, - Icons::Umbrella => Bootstrap::Umbrella, - Icons::Underline => Bootstrap::TypeUnderline, - Icons::Upload => Bootstrap::Upload, - // Icons::Uppercase => Bootstrap::, - Icons::Wallet => Bootstrap::Wallet, - Icons::Wand => Bootstrap::Magic, - // Icons::Warning => Bootstrap::, - // Icons::Weights => Bootstrap::, - Icons::Wifi => Bootstrap::Wifi, - Icons::WifiDisabled => Bootstrap::WifiOff, - Icons::Window => Bootstrap::Window, - Icons::Tools => Bootstrap::Tools, - Icons::Watch => Bootstrap::Watch, - Icons::XMark => Bootstrap::XLg, - Icons::Indent => Bootstrap::Indent, - Icons::Unindent => Bootstrap::Unindent, - } -} diff --git a/rust/client/src/ui/widget/accessories.rs b/rust/client/src/ui/widget/accessories.rs new file mode 100644 index 0000000..101506d --- /dev/null +++ b/rust/client/src/ui/widget/accessories.rs @@ -0,0 +1,82 @@ +use std::collections::HashMap; + +use gauntlet_common::model::IconAccessoryWidget; +use gauntlet_common::model::TextAccessoryWidget; +use gauntlet_common::model::UiWidgetId; +use iced::advanced::text::Shaping; +use iced::alignment::Horizontal; +use iced::alignment::Vertical; +use iced::widget::container; +use iced::widget::row; +use iced::widget::text; +use iced::widget::tooltip; +use iced::widget::tooltip::Position; +use iced::Alignment; + +use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::text::TextStyle; +use crate::ui::theme::tooltip::TooltipStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::images::render_image; + +pub fn render_icon_accessory<'a, T: 'a + Clone>( + images: &HashMap>, + widget: &IconAccessoryWidget, +) -> Element<'a, T> { + let icon = render_image(images, widget.__id__, &widget.icon, Some(TextStyle::IconAccessory)); + + let content = container(icon) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .themed(ContainerStyle::IconAccessory); + + match widget.tooltip.as_ref() { + None => content, + Some(tooltip_text) => { + let tooltip_text: Element<_> = text(tooltip_text.to_string()).shaping(Shaping::Advanced).into(); + + tooltip(content, tooltip_text, Position::Top).themed(TooltipStyle::Tooltip) + } + } +} + +pub fn render_text_accessory<'a, T: 'a + Clone>( + images: &HashMap>, + widget: &TextAccessoryWidget, +) -> Element<'a, T> { + let icon: Option> = widget + .icon + .as_ref() + .map(|icon| render_image(images, widget.__id__, icon, Some(TextStyle::TextAccessory))); + + let text_content: Element<_> = text(widget.text.to_string()) + .shaping(Shaping::Advanced) + .themed(TextStyle::TextAccessory); + + let mut content: Vec> = vec![]; + + if let Some(icon) = icon { + let icon: Element<_> = container(icon).themed(ContainerStyle::TextAccessoryIcon); + + content.push(icon) + } + + content.push(text_content); + + let content: Element<_> = row(content).align_y(Alignment::Center).into(); + + let content = container(content) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .themed(ContainerStyle::TextAccessory); + + match widget.tooltip.as_ref() { + None => content, + Some(tooltip_text) => { + let tooltip_text: Element<_> = text(tooltip_text.to_string()).shaping(Shaping::Advanced).into(); + + tooltip(content, tooltip_text, Position::Top).themed(TooltipStyle::Tooltip) + } + } +} diff --git a/rust/client/src/ui/widget/action_panel.rs b/rust/client/src/ui/widget/action_panel.rs new file mode 100644 index 0000000..996a2cf --- /dev/null +++ b/rust/client/src/ui/widget/action_panel.rs @@ -0,0 +1,325 @@ +use std::cell::Cell; +use std::collections::HashMap; + +use gauntlet_common::model::ActionPanelSectionWidget; +use gauntlet_common::model::ActionPanelSectionWidgetOrderedMembers; +use gauntlet_common::model::ActionPanelWidget; +use gauntlet_common::model::ActionPanelWidgetOrderedMembers; +use gauntlet_common::model::ActionWidget; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::UiWidgetId; +use gauntlet_common_ui::shortcut_to_text; +use iced::advanced::text::Shaping; +use iced::font::Weight; +use iced::widget::button; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_rule; +use iced::widget::horizontal_space; +use iced::widget::row; +use iced::widget::scrollable; +use iced::widget::text; +use iced::Alignment; +use iced::Font; +use iced::Length; + +use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::theme::button::ButtonStyle; +use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::row::RowStyle; +use crate::ui::theme::rule::RuleStyle; +use crate::ui::theme::text::TextStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; + +#[derive(Debug)] +pub struct ActionPanel { + pub title: Option, + pub items: Vec, +} + +impl ActionPanel { + pub fn action_count(&self) -> usize { + self.items.iter().map(|item| item.action_count()).sum() + } + + pub fn find_first(&self) -> Option<(String, UiWidgetId)> { + ActionPanelItem::find_first(&self.items) + } +} + +#[derive(Debug)] +pub enum ActionPanelItem { + Action { + label: String, + widget_id: UiWidgetId, + physical_shortcut: Option, + }, + ActionSection { + title: Option, + items: Vec, + }, +} + +impl ActionPanelItem { + fn action_count(&self) -> usize { + match self { + ActionPanelItem::Action { .. } => 1, + ActionPanelItem::ActionSection { items, .. } => items.iter().map(|item| item.action_count()).sum(), + } + } + + fn find_first(items: &[ActionPanelItem]) -> Option<(String, UiWidgetId)> { + for item in items { + match item { + ActionPanelItem::Action { label, widget_id, .. } => return Some((label.to_string(), *widget_id)), + ActionPanelItem::ActionSection { items, .. } => { + if let Some(item) = Self::find_first(items) { + return Some(item); + } + } + } + } + + None + } +} + +pub fn convert_action_panel( + action_panel: &Option, + action_shortcuts: &HashMap, +) -> Option { + match action_panel { + Some(ActionPanelWidget { content, title, .. }) => { + fn action_widget_to_action( + ActionWidget { __id__, id, label }: &ActionWidget, + action_shortcuts: &HashMap, + ) -> ActionPanelItem { + let physical_shortcut: Option = + id.as_ref().map(|id| action_shortcuts.get(id)).flatten().cloned(); + + ActionPanelItem::Action { + label: label.clone(), + widget_id: *__id__, + physical_shortcut, + } + } + + let items = content + .ordered_members + .iter() + .map(|members| { + match members { + ActionPanelWidgetOrderedMembers::Action(widget) => { + action_widget_to_action(widget, action_shortcuts) + } + ActionPanelWidgetOrderedMembers::ActionPanelSection(ActionPanelSectionWidget { + content, + title, + .. + }) => { + let section_items = content + .ordered_members + .iter() + .map(|members| { + match members { + ActionPanelSectionWidgetOrderedMembers::Action(widget) => { + action_widget_to_action(widget, action_shortcuts) + } + } + }) + .collect(); + + ActionPanelItem::ActionSection { + title: title.clone(), + items: section_items, + } + } + } + }) + .collect(); + + Some(ActionPanel { + title: title.clone(), + items, + }) + } + _ => None, + } +} + +fn render_action_panel_items<'a, T: 'a + Clone>( + root: bool, + title: Option, + items: Vec, + action_panel_focus_index: Option, + on_action_click: &dyn Fn(UiWidgetId) -> T, + index_counter: &Cell, +) -> Vec> { + let mut columns = vec![]; + + if let Some(title) = title { + let text: Element<_> = text(title) + .shaping(Shaping::Advanced) + .font(Font { + weight: Weight::Bold, + ..Font::DEFAULT + }) + .themed(TextStyle::ActionSectionTitle); + + let text = container(text).themed( + if root { + ContainerStyle::ActionPanelTitle + } else { + ContainerStyle::ActionSectionTitle + }, + ); + + columns.push(text) + } else { + if !root { + let separator: Element<_> = horizontal_rule(1).themed(RuleStyle::ActionPanel); + + columns.push(separator); + } + } + + for item in items { + match item { + ActionPanelItem::Action { + label, + widget_id, + physical_shortcut, + } => { + let physical_shortcut = match index_counter.get() { + 0 => { + Some(PhysicalShortcut { + // primary + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }) + } + 1 => { + Some(PhysicalShortcut { + // secondary + physical_key: PhysicalKey::Enter, + modifier_shift: true, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }) + } + _ => physical_shortcut, + }; + + let shortcut_element: Option> = + physical_shortcut.as_ref().map(|shortcut| render_shortcut(shortcut)); + + let content: Element<_> = if let Some(shortcut_element) = shortcut_element { + let text: Element<_> = text(label).shaping(Shaping::Advanced).into(); + + let space: Element<_> = horizontal_space().into(); + + row([text, space, shortcut_element]).align_y(Alignment::Center).into() + } else { + text(label).shaping(Shaping::Advanced).into() + }; + + let style = match action_panel_focus_index { + None => ButtonStyle::Action, + Some(focused_index) => { + if focused_index == index_counter.get() { + ButtonStyle::ActionFocused + } else { + ButtonStyle::Action + } + } + }; + + index_counter.set(index_counter.get() + 1); + + let content = button(content) + .on_press(on_action_click(widget_id)) + .width(Length::Fill) + .themed(style); + + columns.push(content); + } + ActionPanelItem::ActionSection { title, items } => { + let content = render_action_panel_items( + false, + title, + items, + action_panel_focus_index, + on_action_click, + index_counter, + ); + + for content in content { + columns.push(content); + } + } + }; + } + + columns +} + +pub fn render_action_panel<'a, T: 'a + Clone, F: Fn(UiWidgetId) -> T>( + action_panel: ActionPanel, + on_action_click: F, + action_panel_scroll_handle: &ScrollHandle, +) -> Element<'a, T> { + let columns = render_action_panel_items( + true, + action_panel.title, + action_panel.items, + action_panel_scroll_handle.index, + &on_action_click, + &Cell::new(0), + ); + + let actions: Element<_> = column(columns).into(); + + let actions: Element<_> = scrollable(actions) + .id(action_panel_scroll_handle.scrollable_id.clone()) + .width(Length::Fill) + .into(); + + container(actions).themed(ContainerStyle::ActionPanel) +} + +pub fn render_shortcut<'a, T: 'a>(shortcut: &PhysicalShortcut) -> Element<'a, T> { + let mut result = vec![]; + + let (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) = + shortcut_to_text(shortcut); + + fn apply_modifier<'result, 'element, T: 'element>( + result: &'result mut Vec>, + modifier: Option>, + ) { + if let Some(modifier) = modifier { + let modifier: Element<_> = container(modifier).themed(ContainerStyle::ActionShortcutModifier); + + let modifier: Element<_> = container(modifier).themed(ContainerStyle::ActionShortcutModifiersInit); + + result.push(modifier); + } + } + + apply_modifier(&mut result, meta_modifier_text); + apply_modifier(&mut result, control_modifier_text); + apply_modifier(&mut result, shift_modifier_text); + apply_modifier(&mut result, alt_modifier_text); + + let key_name: Element<_> = container(key_name).themed(ContainerStyle::ActionShortcutModifier); + + result.push(key_name); + + row(result).themed(RowStyle::ActionShortcut) +} diff --git a/rust/client/src/ui/widget/content.rs b/rust/client/src/ui/widget/content.rs new file mode 100644 index 0000000..bd55544 --- /dev/null +++ b/rust/client/src/ui/widget/content.rs @@ -0,0 +1,140 @@ +use gauntlet_common::model::CodeBlockWidget; +use gauntlet_common::model::ContentWidget; +use gauntlet_common::model::ContentWidgetOrderedMembers; +use gauntlet_common::model::H1Widget; +use gauntlet_common::model::H2Widget; +use gauntlet_common::model::H3Widget; +use gauntlet_common::model::H4Widget; +use gauntlet_common::model::H5Widget; +use gauntlet_common::model::H6Widget; +use gauntlet_common::model::HorizontalBreakWidget; +use gauntlet_common::model::ImageWidget; +use gauntlet_common::model::ParagraphWidget; +use iced::alignment::Horizontal; +use iced::alignment::Vertical; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_rule; +use iced::Length; + +use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::events::ComponentWidgetEvent; +use crate::ui::widget::images::render_image; +use crate::ui::widget::text::TextRenderType; + +impl<'b> ComponentWidgets<'b> { + fn render_paragraph_widget<'a>( + &self, + widget: &ParagraphWidget, + centered: bool, + ) -> Element<'a, ComponentWidgetEvent> { + let paragraph: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); + + let mut content = container(paragraph).width(Length::Fill); + + if centered { + content = content.align_x(Horizontal::Center) + } + + content.themed(ContainerStyle::ContentParagraph) + } + + fn render_image_widget<'a>(&self, widget: &ImageWidget, centered: bool) -> Element<'a, ComponentWidgetEvent> { + // TODO image size, height and width + let content: Element<_> = render_image(self.images, widget.__id__, &widget.source, None); + + let mut content = container(content).width(Length::Fill); + + if centered { + content = content.align_x(Horizontal::Center) + } + + content.themed(ContainerStyle::ContentImage) + } + + fn render_h1_widget<'a>(&self, widget: &H1Widget) -> Element<'a, ComponentWidgetEvent> { + self.render_text(&widget.content.text, TextRenderType::H1) + } + + fn render_h2_widget<'a>(&self, widget: &H2Widget) -> Element<'a, ComponentWidgetEvent> { + self.render_text(&widget.content.text, TextRenderType::H2) + } + + fn render_h3_widget<'a>(&self, widget: &H3Widget) -> Element<'a, ComponentWidgetEvent> { + self.render_text(&widget.content.text, TextRenderType::H3) + } + + fn render_h4_widget<'a>(&self, widget: &H4Widget) -> Element<'a, ComponentWidgetEvent> { + self.render_text(&widget.content.text, TextRenderType::H4) + } + + fn render_h5_widget<'a>(&self, widget: &H5Widget) -> Element<'a, ComponentWidgetEvent> { + self.render_text(&widget.content.text, TextRenderType::H5) + } + + fn render_h6_widget<'a>(&self, widget: &H6Widget) -> Element<'a, ComponentWidgetEvent> { + self.render_text(&widget.content.text, TextRenderType::H6) + } + + fn render_horizontal_break_widget<'a>(&self, _widget: &HorizontalBreakWidget) -> Element<'a, ComponentWidgetEvent> { + let separator: Element<_> = horizontal_rule(1).into(); + + container(separator) + .width(Length::Fill) + .themed(ContainerStyle::ContentHorizontalBreak) + } + + fn render_code_block_widget<'a>(&self, widget: &CodeBlockWidget) -> Element<'a, ComponentWidgetEvent> { + let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); + + let content = container(content) + .width(Length::Fill) + .themed(ContainerStyle::ContentCodeBlockText); + + container(content) + .width(Length::Fill) + .themed(ContainerStyle::ContentCodeBlock) + } + + pub fn render_content_widget<'a>( + &self, + widget: &ContentWidget, + centered: bool, + ) -> Element<'a, ComponentWidgetEvent> { + let content: Vec<_> = widget + .content + .ordered_members + .iter() + .map(|members| { + match members { + ContentWidgetOrderedMembers::Paragraph(widget) => self.render_paragraph_widget(widget, centered), + ContentWidgetOrderedMembers::Image(widget) => self.render_image_widget(widget, centered), + ContentWidgetOrderedMembers::H1(widget) => self.render_h1_widget(widget), + ContentWidgetOrderedMembers::H2(widget) => self.render_h2_widget(widget), + ContentWidgetOrderedMembers::H3(widget) => self.render_h3_widget(widget), + ContentWidgetOrderedMembers::H4(widget) => self.render_h4_widget(widget), + ContentWidgetOrderedMembers::H5(widget) => self.render_h5_widget(widget), + ContentWidgetOrderedMembers::H6(widget) => self.render_h6_widget(widget), + ContentWidgetOrderedMembers::HorizontalBreak(widget) => self.render_horizontal_break_widget(widget), + ContentWidgetOrderedMembers::CodeBlock(widget) => self.render_code_block_widget(widget), + } + }) + .collect(); + + let content: Element<_> = column(content).into(); + + if centered { + container(content) + .width(Length::Fill) + .height(Length::Fill) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .into() + } else { + content + } + } +} diff --git a/rust/client/src/ui/widget/data.rs b/rust/client/src/ui/widget/data.rs new file mode 100644 index 0000000..40bc90a --- /dev/null +++ b/rust/client/src/ui/widget/data.rs @@ -0,0 +1,417 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use gauntlet_common::model::ActionPanelSectionWidgetOrderedMembers; +use gauntlet_common::model::ActionPanelWidgetOrderedMembers; +use gauntlet_common::model::GridSectionWidgetOrderedMembers; +use gauntlet_common::model::GridWidget; +use gauntlet_common::model::GridWidgetOrderedMembers; +use gauntlet_common::model::ListSectionWidgetOrderedMembers; +use gauntlet_common::model::ListWidget; +use gauntlet_common::model::ListWidgetOrderedMembers; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::RootWidget; +use gauntlet_common::model::RootWidgetMembers; +use gauntlet_common::model::UiRenderLocation; +use gauntlet_common::model::UiWidgetId; +use iced::widget::text_input; +use iced::Task; + +use crate::ui::grid_navigation::GridSectionData; +use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::widget::action_panel::convert_action_panel; +use crate::ui::widget::action_panel::ActionPanel; +use crate::ui::widget::events::ComponentWidgetEvent; +use crate::ui::widget::grid::grid_width; +use crate::ui::widget::state::CheckboxState; +use crate::ui::widget::state::ComponentWidgetState; +use crate::ui::widget::state::DatePickerState; +use crate::ui::widget::state::RootState; +use crate::ui::widget::state::SelectState; +use crate::ui::widget::state::TextFieldState; +use crate::ui::AppMsg; + +#[derive(Debug)] +pub struct ComponentWidgets<'b> { + pub root_widget: &'b Option>, + pub state: &'b HashMap, + pub plugin_id: PluginId, + pub images: &'b HashMap>, +} + +impl<'b> ComponentWidgets<'b> { + pub fn new( + root_widget: &'b Option>, + state: &'b HashMap, + plugin_id: PluginId, + images: &'b HashMap>, + ) -> ComponentWidgets<'b> { + Self { + root_widget, + state, + plugin_id, + images, + } + } + + pub fn text_field_state(&self, widget_id: UiWidgetId) -> &TextFieldState { + let state = self.state.get(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); + + match state { + ComponentWidgetState::TextField(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state), + } + } + + pub fn checkbox_state(&self, widget_id: UiWidgetId) -> &CheckboxState { + let state = self.state.get(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); + + match state { + ComponentWidgetState::Checkbox(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state), + } + } + + pub fn date_picker_state(&self, widget_id: UiWidgetId) -> &DatePickerState { + let state = self.state.get(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); + + match state { + ComponentWidgetState::DatePicker(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state), + } + } + + pub fn select_state(&self, widget_id: UiWidgetId) -> &SelectState { + let state = self.state.get(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); + + match state { + ComponentWidgetState::Select(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state), + } + } + + pub fn root_state(&self, widget_id: UiWidgetId) -> &RootState { + let state = self.state.get(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); + + match state { + ComponentWidgetState::Root(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state), + } + } +} + +impl<'b> ComponentWidgets<'b> { + pub fn get_action_ids(&self) -> Vec { + let Some(root_widget) = &self.root_widget else { + return vec![]; + }; + + let Some(content) = &root_widget.content else { + return vec![]; + }; + + let actions = match content { + RootWidgetMembers::Detail(widget) => &widget.content.actions, + RootWidgetMembers::Form(widget) => &widget.content.actions, + RootWidgetMembers::Inline(widget) => &widget.content.actions, + RootWidgetMembers::List(widget) => &widget.content.actions, + RootWidgetMembers::Grid(widget) => &widget.content.actions, + }; + + let mut result = vec![]; + match actions { + None => {} + Some(widget) => { + for members in &widget.content.ordered_members { + match members { + ActionPanelWidgetOrderedMembers::Action(widget) => result.push(widget.__id__), + ActionPanelWidgetOrderedMembers::ActionPanelSection(widget) => { + for members in &widget.content.ordered_members { + match members { + ActionPanelSectionWidgetOrderedMembers::Action(widget) => { + result.push(widget.__id__) + } + } + } + } + } + } + } + } + + result + } + + pub fn get_focused_item_id(&self) -> Option { + let Some(root_widget) = &self.root_widget else { + return None; + }; + + let Some(content) = &root_widget.content else { + return None; + }; + + match content { + RootWidgetMembers::Detail(_) => None, + RootWidgetMembers::Form(_) => None, + RootWidgetMembers::Inline(_) => None, + RootWidgetMembers::List(widget) => { + let RootState { focused_item, .. } = self.root_state(widget.__id__); + + ComponentWidgets::list_focused_item_id(focused_item, widget) + } + RootWidgetMembers::Grid(widget) => { + let RootState { focused_item, .. } = self.root_state(widget.__id__); + + ComponentWidgets::grid_focused_item_id(focused_item, widget) + } + } + } + + pub fn focus_search_bar(&self, widget_id: UiWidgetId) -> Task { + let TextFieldState { text_input_id, .. } = self.text_field_state(widget_id); + + text_input::focus(text_input_id.clone()) + } + + pub fn grid_section_sizes(grid_widget: &GridWidget) -> Vec { + let mut amount_per_section: Vec = vec![]; + let mut pending_section_size = 0; + + let mut cumulative_item_index = 0; + let mut cumulative_row_index = 0; + + let mut cumulative_item_index_at_start = cumulative_item_index; + let mut cumulative_row_index_at_start = cumulative_row_index; + + for members in &grid_widget.content.ordered_members { + match &members { + GridWidgetOrderedMembers::GridItem(_) => { + pending_section_size = pending_section_size + 1; + } + GridWidgetOrderedMembers::GridSection(widget) => { + if pending_section_size > 0 { + let width = grid_width(&grid_widget.columns); + amount_per_section.push(GridSectionData { + start_index: cumulative_item_index_at_start, + start_row_index: cumulative_row_index_at_start, + amount_in_section: pending_section_size, + width, + }); + + cumulative_item_index = cumulative_item_index + pending_section_size; + cumulative_row_index = + cumulative_row_index_at_start + (usize::div_ceil(pending_section_size, width)); + + cumulative_item_index_at_start = cumulative_item_index; + cumulative_row_index_at_start = cumulative_row_index; + + pending_section_size = 0; + } + + let section_amount = widget + .content + .ordered_members + .iter() + .filter(|members| matches!(members, GridSectionWidgetOrderedMembers::GridItem(_))) + .count(); + + let width = grid_width(&widget.columns); + amount_per_section.push(GridSectionData { + start_index: cumulative_item_index_at_start, + start_row_index: cumulative_row_index_at_start, + amount_in_section: section_amount, + width, + }); + + cumulative_item_index = cumulative_item_index + section_amount; + cumulative_row_index = cumulative_row_index_at_start + (usize::div_ceil(section_amount, width)); + + cumulative_item_index_at_start = cumulative_item_index; + cumulative_row_index_at_start = cumulative_row_index; + } + } + } + + if pending_section_size > 0 { + amount_per_section.push(GridSectionData { + start_index: cumulative_item_index_at_start, + start_row_index: cumulative_row_index_at_start, + amount_in_section: pending_section_size, + width: grid_width(&grid_widget.columns), + }); + } + + amount_per_section + } +} + +impl<'b> ComponentWidgets<'b> { + pub fn first_open(&self) -> AppMsg { + let Some(root_widget) = &self.root_widget else { + return AppMsg::Noop; + }; + + let Some(content) = &root_widget.content else { + return AppMsg::Noop; + }; + + let widget_id = match content { + RootWidgetMembers::List(widget) => { + match &widget.content.search_bar { + None => return AppMsg::Noop, + Some(widget) => widget.__id__, + } + } + RootWidgetMembers::Grid(widget) => { + match &widget.content.search_bar { + None => return AppMsg::Noop, + Some(widget) => widget.__id__, + } + } + _ => return AppMsg::Noop, + }; + + AppMsg::FocusPluginViewSearchBar { widget_id } + } + + pub fn list_focused_item_id(focused_item: &ScrollHandle, widget: &ListWidget) -> Option { + let mut items = vec![]; + + for members in &widget.content.ordered_members { + match &members { + ListWidgetOrderedMembers::ListItem(item) => { + items.push(&item.id); + } + ListWidgetOrderedMembers::ListSection(section) => { + for members in §ion.content.ordered_members { + match &members { + ListSectionWidgetOrderedMembers::ListItem(item) => { + items.push(&item.id); + } + } + } + } + } + } + + match focused_item.get(&items) { + None => None, + Some(item_id) => Some(item_id.to_string()), + } + } + + pub fn list_item_focus_event( + plugin_id: PluginId, + focused_item: &ScrollHandle, + widget: &ListWidget, + ) -> Task { + let widget_event = match ComponentWidgets::list_focused_item_id(focused_item, widget) { + None => { + ComponentWidgetEvent::FocusListItem { + list_widget_id: widget.__id__, + item_id: None, + } + } + Some(item_id) => { + ComponentWidgetEvent::FocusListItem { + list_widget_id: widget.__id__, + item_id: Some(item_id), + } + } + }; + + Task::done(AppMsg::WidgetEvent { + plugin_id, + render_location: UiRenderLocation::View, + widget_event, + }) + } + + pub fn grid_focused_item_id(focused_item: &ScrollHandle, widget: &GridWidget) -> Option { + let mut items = vec![]; + + for members in &widget.content.ordered_members { + match &members { + GridWidgetOrderedMembers::GridItem(item) => { + items.push(&item.id); + } + GridWidgetOrderedMembers::GridSection(section) => { + for members in §ion.content.ordered_members { + match &members { + GridSectionWidgetOrderedMembers::GridItem(item) => { + items.push(&item.id); + } + } + } + } + } + } + + match focused_item.get(&items) { + None => None, + Some(item_id) => Some(item_id.to_string()), + } + } + + pub fn grid_item_focus_event( + plugin_id: PluginId, + focused_item: &ScrollHandle, + widget: &GridWidget, + ) -> Task { + let widget_event = match ComponentWidgets::grid_focused_item_id(focused_item, widget) { + None => { + ComponentWidgetEvent::FocusGridItem { + grid_widget_id: widget.__id__, + item_id: None, + } + } + Some(item_id) => { + ComponentWidgetEvent::FocusGridItem { + grid_widget_id: widget.__id__, + item_id: Some(item_id), + } + } + }; + + Task::done(AppMsg::WidgetEvent { + plugin_id, + render_location: UiRenderLocation::View, + widget_event, + }) + } + + pub fn get_action_panel(&self, action_shortcuts: &HashMap) -> Option { + let Some(root_widget) = &self.root_widget else { + return None; + }; + + let Some(content) = &root_widget.content else { + return None; + }; + + match content { + RootWidgetMembers::Detail(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), + RootWidgetMembers::Form(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), + RootWidgetMembers::Inline(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), + RootWidgetMembers::List(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), + RootWidgetMembers::Grid(widget) => convert_action_panel(&widget.content.actions, action_shortcuts), + } + } +} diff --git a/rust/client/src/ui/widget/data_mut.rs b/rust/client/src/ui/widget/data_mut.rs new file mode 100644 index 0000000..22f59f4 --- /dev/null +++ b/rust/client/src/ui/widget/data_mut.rs @@ -0,0 +1,402 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use gauntlet_common::model::GridSectionWidgetOrderedMembers; +use gauntlet_common::model::GridWidgetOrderedMembers; +use gauntlet_common::model::ListSectionWidgetOrderedMembers; +use gauntlet_common::model::ListWidgetOrderedMembers; +use gauntlet_common::model::PluginId; +use gauntlet_common::model::RootWidget; +use gauntlet_common::model::RootWidgetMembers; +use gauntlet_common::model::UiWidgetId; +use iced::widget::text_input; +use iced::Task; + +use crate::ui::grid_navigation::grid_down_offset; +use crate::ui::grid_navigation::grid_up_offset; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::state::ComponentWidgetState; +use crate::ui::widget::state::RootState; +use crate::ui::widget::state::TextFieldState; +use crate::ui::AppMsg; + +#[derive(Debug)] +pub struct ComponentWidgetsMut<'b> { + pub root_widget: &'b mut Option>, + pub state: &'b mut HashMap, + pub plugin_id: PluginId, + pub images: &'b HashMap>, +} + +impl<'b> ComponentWidgetsMut<'b> { + pub fn new( + root_widget: &'b mut Option>, + state: &'b mut HashMap, + plugin_id: PluginId, + images: &'b HashMap>, + ) -> ComponentWidgetsMut<'b> { + Self { + root_widget, + state, + plugin_id, + images, + } + } + + pub fn text_field_state_mut(&mut self, widget_id: UiWidgetId) -> &mut TextFieldState { + Self::text_field_state_mut_on_state(&mut self.state, widget_id) + } + + pub fn text_field_state_mut_on_state( + state: &mut HashMap, + widget_id: UiWidgetId, + ) -> &mut TextFieldState { + let state = state.get_mut(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); + + match state { + ComponentWidgetState::TextField(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state), + } + } + + pub fn root_state_mut(&mut self, widget_id: UiWidgetId) -> &mut RootState { + Self::root_state_mut_on_field(&mut self.state, widget_id) + } + + pub fn root_state_mut_on_field( + state: &mut HashMap, + widget_id: UiWidgetId, + ) -> &mut RootState { + let state = state.get_mut(&widget_id).expect(&format!( + "requested state should always be present for id: {}", + widget_id + )); + + match state { + ComponentWidgetState::Root(state) => state, + _ => panic!("TextFieldState expected, {:?} found", state), + } + } +} + +impl<'b> ComponentWidgetsMut<'b> { + pub fn toggle_action_panel(&mut self) { + let Some(root_widget) = &self.root_widget else { + return; + }; + + let Some(content) = &root_widget.content else { + return; + }; + + let widget_id = match content { + RootWidgetMembers::Detail(widget) => widget.__id__, + RootWidgetMembers::Form(widget) => widget.__id__, + RootWidgetMembers::Inline(widget) => widget.__id__, + RootWidgetMembers::List(widget) => widget.__id__, + RootWidgetMembers::Grid(widget) => widget.__id__, + }; + + let state = self.root_state_mut(widget_id); + + state.show_action_panel = !state.show_action_panel; + } + + pub fn append_text(&mut self, text: &str) -> Task { + let Some(root_widget) = &self.root_widget else { + return Task::none(); + }; + + let Some(content) = &root_widget.content else { + return Task::none(); + }; + + let widget_id = match content { + RootWidgetMembers::List(widget) => { + match &widget.content.search_bar { + None => return Task::none(), + Some(widget) => widget.__id__, + } + } + RootWidgetMembers::Grid(widget) => { + match &widget.content.search_bar { + None => return Task::none(), + Some(widget) => widget.__id__, + } + } + _ => return Task::none(), + }; + + let TextFieldState { + text_input_id, + state_value, + } = ComponentWidgetsMut::text_field_state_mut_on_state(&mut self.state, widget_id); + + if let Some(value) = text.chars().next().filter(|c| !c.is_control()) { + *state_value = format!("{}{}", state_value, value); + + text_input::focus(text_input_id.clone()) + } else { + Task::none() + } + } + + pub fn backspace_text(&mut self) -> Task { + let Some(root_widget) = &self.root_widget else { + return Task::none(); + }; + + let Some(content) = &root_widget.content else { + return Task::none(); + }; + + let widget_id = match content { + RootWidgetMembers::List(widget) => { + match &widget.content.search_bar { + None => return Task::none(), + Some(widget) => widget.__id__, + } + } + RootWidgetMembers::Grid(widget) => { + match &widget.content.search_bar { + None => return Task::none(), + Some(widget) => widget.__id__, + } + } + _ => return Task::none(), + }; + + let TextFieldState { + text_input_id, + state_value, + } = ComponentWidgetsMut::text_field_state_mut_on_state(&mut self.state, widget_id); + + let mut chars = state_value.chars(); + chars.next_back(); + *state_value = chars.as_str().to_owned(); + + text_input::focus(text_input_id.clone()) + } + + pub fn focus_up(&mut self) -> Task { + let Some(root_widget) = &self.root_widget else { + return Task::none(); + }; + + let Some(content) = &root_widget.content else { + return Task::none(); + }; + + match content { + RootWidgetMembers::Detail(_) => Task::none(), + RootWidgetMembers::Form(_) => Task::none(), + RootWidgetMembers::Inline(_) => Task::none(), + RootWidgetMembers::List(list_widget) => { + let RootState { focused_item, .. } = + ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, list_widget.__id__); + + let focus_task = focused_item.focus_previous().unwrap_or_else(|| Task::none()); + + let item_focus_event = + ComponentWidgets::list_item_focus_event(self.plugin_id.clone(), focused_item, list_widget); + + Task::batch([item_focus_event, focus_task]) + } + RootWidgetMembers::Grid(grid_widget) => { + let RootState { focused_item, .. } = + ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); + + let Some(current_index) = &focused_item.index else { + return Task::none(); + }; + + let amount_per_section_total = ComponentWidgets::grid_section_sizes(grid_widget); + + let focus_task = match grid_up_offset(*current_index, amount_per_section_total) { + None => Task::none(), + Some(data) => { + match focused_item.focus_previous_in(data.offset) { + None => Task::none(), + Some(_) => focused_item.scroll_to(data.row_index), + } + } + }; + + let item_focus_event = + ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); + + Task::batch([item_focus_event, focus_task]) + } + } + } + + pub fn focus_down(&mut self) -> Task { + let Some(root_widget) = &self.root_widget else { + return Task::none(); + }; + + let Some(content) = &root_widget.content else { + return Task::none(); + }; + + match content { + RootWidgetMembers::Detail(_) => Task::none(), + RootWidgetMembers::Form(_) => Task::none(), + RootWidgetMembers::Inline(_) => Task::none(), + RootWidgetMembers::List(widget) => { + let RootState { focused_item, .. } = + ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, widget.__id__); + + let total = widget + .content + .ordered_members + .iter() + .flat_map(|members| { + match members { + ListWidgetOrderedMembers::ListItem(widget) => vec![widget], + ListWidgetOrderedMembers::ListSection(widget) => { + widget + .content + .ordered_members + .iter() + .map(|members| { + match members { + ListSectionWidgetOrderedMembers::ListItem(widget) => widget, + } + }) + .collect() + } + } + }) + .count(); + + let focus_task = focused_item.focus_next(total).unwrap_or_else(|| Task::none()); + + let item_focus_event = + ComponentWidgets::list_item_focus_event(self.plugin_id.clone(), focused_item, widget); + + Task::batch([item_focus_event, focus_task]) + } + RootWidgetMembers::Grid(grid_widget) => { + let RootState { focused_item, .. } = + ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); + + let amount_per_section_total = ComponentWidgets::grid_section_sizes(grid_widget); + + let total = amount_per_section_total.iter().map(|data| data.amount_in_section).sum(); + + let Some(current_index) = &focused_item.index else { + let unfocus = match &grid_widget.content.search_bar { + None => Task::none(), + Some(_) => { + // there doesn't seem to be an unfocus command but focusing non-existing input will unfocus all + text_input::focus(text_input::Id::unique()) + } + }; + + let _ = focused_item.focus_next(total); + + let item_focus_event = + ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); + + return Task::batch([unfocus, focused_item.scroll_to(0), item_focus_event]); + }; + + let focus_task = match grid_down_offset(*current_index, amount_per_section_total) { + None => Task::none(), + Some(data) => { + match focused_item.focus_next_in(total, data.offset) { + None => Task::none(), + Some(_) => focused_item.scroll_to(data.row_index), + } + } + }; + + let item_focus_event = + ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget); + + Task::batch([item_focus_event, focus_task]) + } + } + } + + pub fn focus_left(&mut self) -> Task { + let Some(root_widget) = &self.root_widget else { + return Task::none(); + }; + + let Some(content) = &root_widget.content else { + return Task::none(); + }; + + match content { + RootWidgetMembers::Detail(_) => Task::none(), + RootWidgetMembers::Form(_) => Task::none(), + RootWidgetMembers::Inline(_) => Task::none(), + RootWidgetMembers::List(_) => Task::none(), + RootWidgetMembers::Grid(grid_widget) => { + let RootState { focused_item, .. } = + ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); + + let _ = focused_item.focus_previous(); + + // focused_item.scroll_to(0) + + ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget) + } + } + } + + pub fn focus_right(&mut self) -> Task { + let Some(root_widget) = &self.root_widget else { + return Task::none(); + }; + + let Some(content) = &root_widget.content else { + return Task::none(); + }; + + match content { + RootWidgetMembers::Detail(_) => Task::none(), + RootWidgetMembers::Form(_) => Task::none(), + RootWidgetMembers::Inline(_) => Task::none(), + RootWidgetMembers::List(_) => Task::none(), + RootWidgetMembers::Grid(grid_widget) => { + let RootState { focused_item, .. } = + ComponentWidgetsMut::root_state_mut_on_field(&mut self.state, grid_widget.__id__); + + let total = grid_widget + .content + .ordered_members + .iter() + .flat_map(|members| { + match members { + GridWidgetOrderedMembers::GridItem(widget) => vec![widget], + GridWidgetOrderedMembers::GridSection(widget) => { + widget + .content + .ordered_members + .iter() + .map(|members| { + match members { + GridSectionWidgetOrderedMembers::GridItem(widget) => widget, + } + }) + .collect() + } + } + }) + .count(); + + let _ = focused_item.focus_next(total); + + // focused_item.scroll_to(0) + + ComponentWidgets::grid_item_focus_event(self.plugin_id.clone(), focused_item, grid_widget) + } + } + } +} diff --git a/rust/client/src/ui/widget/detail.rs b/rust/client/src/ui/widget/detail.rs new file mode 100644 index 0000000..20d4a07 --- /dev/null +++ b/rust/client/src/ui/widget/detail.rs @@ -0,0 +1,95 @@ +use gauntlet_common::model::DetailWidget; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_rule; +use iced::widget::row; +use iced::widget::scrollable; +use iced::widget::vertical_rule; +use iced::Length; + +use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::events::ComponentWidgetEvent; + +impl<'b> ComponentWidgets<'b> { + pub fn render_detail_widget<'a>( + &self, + widget: &DetailWidget, + is_in_list: bool, + ) -> Element<'a, ComponentWidgetEvent> { + let metadata_element = widget.content.metadata.as_ref().map(|widget| { + let content = self.render_metadata_widget(widget, is_in_list); + + container(content) + .width( + if is_in_list { + Length::Fill + } else { + Length::FillPortion(2) + }, + ) + .height( + if is_in_list { + Length::FillPortion(3) + } else { + Length::Fill + }, + ) + .themed(ContainerStyle::DetailMetadata) + }); + + let content_element = widget.content.content.as_ref().map(|widget| { + let content_element: Element<_> = container(self.render_content_widget(widget, false)) + .width(Length::Fill) + .themed(ContainerStyle::DetailContentInner); + + let content_element: Element<_> = scrollable(content_element).width(Length::Fill).into(); + + let content_element: Element<_> = container(content_element) + .width( + if is_in_list { + Length::Fill + } else { + Length::FillPortion(3) + }, + ) + .height( + if is_in_list { + Length::FillPortion(3) + } else { + Length::Fill + }, + ) + .themed(ContainerStyle::DetailContent); + + content_element + }); + + let separator = if is_in_list { + horizontal_rule(1).into() + } else { + vertical_rule(1).into() + }; + + let list_fn = |vec| { + if is_in_list { + column(vec).into() + } else { + row(vec).into() + } + }; + + let content: Element<_> = match (content_element, metadata_element) { + (Some(content_element), Some(metadata_element)) => { + list_fn(vec![content_element, separator, metadata_element]) + } + (Some(content_element), None) => list_fn(vec![content_element]), + (None, Some(metadata_element)) => list_fn(vec![metadata_element]), + (None, None) => list_fn(vec![]), + }; + + content + } +} diff --git a/rust/client/src/ui/widget/empty_view.rs b/rust/client/src/ui/widget/empty_view.rs new file mode 100644 index 0000000..e688239 --- /dev/null +++ b/rust/client/src/ui/widget/empty_view.rs @@ -0,0 +1,54 @@ +use gauntlet_common::model::EmptyViewWidget; +use iced::advanced::text::Shaping; +use iced::alignment::Horizontal; +use iced::alignment::Vertical; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_space; +use iced::widget::text; +use iced::Alignment; +use iced::Length; + +use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::text::TextStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::events::ComponentWidgetEvent; +use crate::ui::widget::images::render_image; + +impl<'b> ComponentWidgets<'b> { + pub fn render_empty_view_widget<'a>(&self, widget: &EmptyViewWidget) -> Element<'a, ComponentWidgetEvent> { + let image: Option> = widget + .image + .as_ref() + .map(|image| render_image(self.images, widget.__id__, image, Some(TextStyle::EmptyViewSubtitle))); + + let title: Element<_> = text(widget.title.to_string()).shaping(Shaping::Advanced).into(); + + let subtitle: Element<_> = match &widget.description { + None => horizontal_space().into(), + Some(subtitle) => { + text(subtitle.to_string()) + .shaping(Shaping::Advanced) + .themed(TextStyle::EmptyViewSubtitle) + } + }; + + let mut content = vec![title, subtitle]; + if let Some(image) = image { + let image: Element<_> = container(image).themed(ContainerStyle::EmptyViewImage); + + content.insert(0, image) + } + + let content: Element<_> = column(content).align_x(Alignment::Center).into(); + + container(content) + .width(Length::Fill) + .height(Length::Fill) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .into() + } +} diff --git a/rust/client/src/ui/widget/events.rs b/rust/client/src/ui/widget/events.rs new file mode 100644 index 0000000..3fb8d91 --- /dev/null +++ b/rust/client/src/ui/widget/events.rs @@ -0,0 +1,246 @@ +use gauntlet_common::model::PluginId; +use gauntlet_common::model::UiWidgetId; + +use crate::model::UiViewEvent; +use crate::ui::widget::state::CheckboxState; +use crate::ui::widget::state::ComponentWidgetState; +use crate::ui::widget::state::DatePickerState; +use crate::ui::widget::state::SelectState; +use crate::ui::widget::state::TextFieldState; +use crate::ui::AppMsg; + +include!(concat!(env!("OUT_DIR"), "/components.rs")); + +#[derive(Clone, Debug)] +pub enum ComponentWidgetEvent { + LinkClick { + widget_id: UiWidgetId, + href: String, + }, + TagClick { + widget_id: UiWidgetId, + }, + ActionClick { + widget_id: UiWidgetId, + id: Option, + }, + RunAction { + widget_id: UiWidgetId, + id: Option, + }, + ToggleDatePicker { + widget_id: UiWidgetId, + }, + OnChangeTextField { + widget_id: UiWidgetId, + value: String, + }, + OnChangePasswordField { + widget_id: UiWidgetId, + value: String, + }, + OnChangeSearchBar { + widget_id: UiWidgetId, + value: String, + }, + SubmitDatePicker { + widget_id: UiWidgetId, + value: String, + }, + CancelDatePicker { + widget_id: UiWidgetId, + }, + ToggleCheckbox { + widget_id: UiWidgetId, + value: bool, + }, + SelectPickList { + widget_id: UiWidgetId, + value: String, + }, + ToggleActionPanel { + widget_id: UiWidgetId, + }, + FocusListItem { + list_widget_id: UiWidgetId, + item_id: Option, + }, + FocusGridItem { + grid_widget_id: UiWidgetId, + item_id: Option, + }, + PreviousView, + RunPrimaryAction { + widget_id: UiWidgetId, + id: Option, + }, + Noop, +} + +impl ComponentWidgetEvent { + pub fn handle(self, _plugin_id: PluginId, state: Option<&mut ComponentWidgetState>) -> Option { + match self { + ComponentWidgetEvent::LinkClick { widget_id: _, href } => Some(UiViewEvent::Open { href }), + ComponentWidgetEvent::TagClick { widget_id } => Some(create_metadata_tag_item_on_click_event(widget_id)), + ComponentWidgetEvent::RunAction { widget_id, id } | ComponentWidgetEvent::ActionClick { widget_id, id } => { + Some(create_action_on_action_event(widget_id, id)) + } + ComponentWidgetEvent::ToggleDatePicker { widget_id } => { + let state = state.expect("state should always exist for "); + + let ComponentWidgetState::DatePicker(DatePickerState { + state_value: _, + show_picker, + }) = state + else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; + + *show_picker = !*show_picker; + None + } + ComponentWidgetEvent::CancelDatePicker { widget_id } => { + let state = state.expect("state should always exist for "); + + let ComponentWidgetState::DatePicker(DatePickerState { + state_value: _, + show_picker, + }) = state + else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; + + *show_picker = false; + None + } + ComponentWidgetEvent::SubmitDatePicker { widget_id, value } => { + let state = state.expect("state should always exist for "); + + { + let ComponentWidgetState::DatePicker(DatePickerState { + state_value: _, + show_picker, + }) = state + else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; + + *show_picker = false; + } + + Some(create_date_picker_on_change_event(widget_id, Some(value))) + } + ComponentWidgetEvent::ToggleCheckbox { widget_id, value } => { + let state = state.expect("state should always exist for "); + + { + let ComponentWidgetState::Checkbox(CheckboxState { state_value }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; + + *state_value = !*state_value; + } + + Some(create_checkbox_on_change_event(widget_id, value)) + } + ComponentWidgetEvent::SelectPickList { widget_id, value } => { + let state = state.expect("state should always exist for "); + + { + let ComponentWidgetState::Select(SelectState { state_value }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; + + *state_value = Some(value.clone()); + } + + Some(create_select_on_change_event(widget_id, Some(value))) + } + ComponentWidgetEvent::OnChangeTextField { widget_id, value } => { + let state = state.expect("state should always exist for "); + + { + let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; + + *state_value = value.clone(); + } + + Some(create_text_field_on_change_event(widget_id, Some(value))) + } + ComponentWidgetEvent::OnChangePasswordField { widget_id, value } => { + let state = state.expect("state should always exist for "); + + { + let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; + + *state_value = value.clone(); + } + + Some(create_password_field_on_change_event(widget_id, Some(value))) + } + ComponentWidgetEvent::OnChangeSearchBar { widget_id, value } => { + let state = state.expect("state should always exist for "); + + { + let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; + + *state_value = value.clone(); + } + + Some(create_search_bar_on_change_event(widget_id, Some(value))) + } + ComponentWidgetEvent::ToggleActionPanel { .. } => { + Some(UiViewEvent::AppEvent { + event: AppMsg::ToggleActionPanel { keyboard: false }, + }) + } + ComponentWidgetEvent::FocusListItem { + list_widget_id, + item_id, + } => Some(create_list_on_item_focus_change_event(list_widget_id, item_id)), + ComponentWidgetEvent::FocusGridItem { + grid_widget_id, + item_id, + } => Some(create_grid_on_item_focus_change_event(grid_widget_id, item_id)), + ComponentWidgetEvent::Noop | ComponentWidgetEvent::PreviousView => { + panic!("widget_id on these events is not supposed to be called") + } + ComponentWidgetEvent::RunPrimaryAction { widget_id, id } => { + Some(UiViewEvent::AppEvent { + event: AppMsg::OnAnyActionPluginViewAnyPanel { widget_id, id }, + }) + } + } + } + + pub fn widget_id(&self) -> UiWidgetId { + match self { + ComponentWidgetEvent::LinkClick { widget_id, .. } => widget_id, + ComponentWidgetEvent::ActionClick { widget_id, .. } => widget_id, + ComponentWidgetEvent::RunAction { widget_id, .. } => widget_id, + ComponentWidgetEvent::TagClick { widget_id, .. } => widget_id, + ComponentWidgetEvent::ToggleDatePicker { widget_id, .. } => widget_id, + ComponentWidgetEvent::SubmitDatePicker { widget_id, .. } => widget_id, + ComponentWidgetEvent::CancelDatePicker { widget_id, .. } => widget_id, + ComponentWidgetEvent::ToggleCheckbox { widget_id, .. } => widget_id, + ComponentWidgetEvent::SelectPickList { widget_id, .. } => widget_id, + ComponentWidgetEvent::OnChangeTextField { widget_id, .. } => widget_id, + ComponentWidgetEvent::OnChangePasswordField { widget_id, .. } => widget_id, + ComponentWidgetEvent::OnChangeSearchBar { widget_id, .. } => widget_id, + ComponentWidgetEvent::ToggleActionPanel { widget_id } => widget_id, + ComponentWidgetEvent::FocusListItem { list_widget_id, .. } => list_widget_id, + ComponentWidgetEvent::FocusGridItem { grid_widget_id, .. } => grid_widget_id, + ComponentWidgetEvent::RunPrimaryAction { widget_id, .. } => widget_id, + ComponentWidgetEvent::Noop | ComponentWidgetEvent::PreviousView => { + panic!("widget_id on these events is not supposed to be called") + } + } + .to_owned() + } +} diff --git a/rust/client/src/ui/widget/form.rs b/rust/client/src/ui/widget/form.rs new file mode 100644 index 0000000..a6f372e --- /dev/null +++ b/rust/client/src/ui/widget/form.rs @@ -0,0 +1,246 @@ +use std::collections::HashMap; +use std::fmt::Display; + +use gauntlet_common::model::CheckboxWidget; +use gauntlet_common::model::DatePickerWidget; +use gauntlet_common::model::FormWidget; +use gauntlet_common::model::FormWidgetOrderedMembers; +use gauntlet_common::model::PasswordFieldWidget; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::SelectWidget; +use gauntlet_common::model::SelectWidgetOrderedMembers; +use gauntlet_common::model::SeparatorWidget; +use gauntlet_common::model::TextFieldWidget; +use iced::advanced::text::Shaping; +use iced::alignment::Horizontal; +use iced::widget::button; +use iced::widget::checkbox; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_rule; +use iced::widget::pick_list; +use iced::widget::row; +use iced::widget::scrollable; +use iced::widget::text; +use iced::widget::text_input; +use iced::widget::Space; +use iced::Alignment; +use iced::Length; +use iced_aw::date_picker; + +use crate::ui::state::PluginViewState; +use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::date_picker::DatePickerStyle; +use crate::ui::theme::pick_list::PickListStyle; +use crate::ui::theme::row::RowStyle; +use crate::ui::theme::text_input::TextInputStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::events::ComponentWidgetEvent; +use crate::ui::widget::state::CheckboxState; +use crate::ui::widget::state::DatePickerState; +use crate::ui::widget::state::RootState; +use crate::ui::widget::state::SelectState; +use crate::ui::widget::state::TextFieldState; + +impl<'b> ComponentWidgets<'b> { + fn render_text_field_widget<'a>(&self, widget: &TextFieldWidget) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let TextFieldState { state_value, .. } = self.text_field_state(widget.__id__); + + text_input("", state_value) + .on_input(move |value| ComponentWidgetEvent::OnChangeTextField { widget_id, value }) + .themed(TextInputStyle::FormInput) + } + + fn render_password_field_widget<'a>(&self, widget: &PasswordFieldWidget) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let TextFieldState { state_value, .. } = self.text_field_state(widget_id); + + text_input("", state_value) + .secure(true) + .on_input(move |value| ComponentWidgetEvent::OnChangePasswordField { widget_id, value }) + .themed(TextInputStyle::FormInput) + } + + fn render_checkbox_widget<'a>(&self, widget: &CheckboxWidget) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let CheckboxState { state_value } = self.checkbox_state(widget_id); + + checkbox(widget.title.as_deref().unwrap_or_default(), state_value.to_owned()) + .on_toggle(move |value| ComponentWidgetEvent::ToggleCheckbox { widget_id, value }) + .into() + } + + fn render_date_picker_widget<'a>(&self, widget: &DatePickerWidget) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let DatePickerState { + state_value, + show_picker, + } = self.date_picker_state(widget.__id__); + + let button_text = text(state_value.to_string()).shaping(Shaping::Advanced); + + let button = button(button_text).on_press(ComponentWidgetEvent::ToggleDatePicker { + widget_id: widget.__id__, + }); + + // TODO unable to customize buttons here, split to separate button styles + // DatePickerUnderlay, + // DatePickerOverlay, + + date_picker( + show_picker.to_owned(), + state_value.to_owned(), + button, + ComponentWidgetEvent::CancelDatePicker { widget_id }, + move |date| { + ComponentWidgetEvent::SubmitDatePicker { + widget_id, + value: date.to_string(), + } + }, + ) + .themed(DatePickerStyle::Default) + } + + fn render_select_widget<'a>(&self, widget: &SelectWidget) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let SelectState { state_value } = self.select_state(widget_id); + + let items: Vec<_> = widget + .content + .ordered_members + .iter() + .map(|members| { + match members { + SelectWidgetOrderedMembers::SelectItem(widget) => { + SelectItem { + value: widget.value.to_owned(), + label: widget.content.text.join(""), + } + } + } + }) + .collect(); + + let state_value = state_value + .clone() + .map(|value| items.iter().find(|item| item.value == value)) + .flatten() + .map(|value| value.clone()); + + pick_list(items, state_value, move |item| { + ComponentWidgetEvent::SelectPickList { + widget_id, + value: item.value, + } + }) + .themed(PickListStyle::Default) + } + + fn render_separator_widget<'a>(&self, _widget: &SeparatorWidget) -> Element<'a, ComponentWidgetEvent> { + horizontal_rule(1).into() + } + + pub fn render_form_widget<'a>( + &self, + widget: &FormWidget, + plugin_view_state: &PluginViewState, + entrypoint_name: &str, + action_shortcuts: &HashMap, + ) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let RootState { show_action_panel, .. } = self.root_state(widget_id); + + let items: Vec> = widget + .content + .ordered_members + .iter() + .map(|members| { + fn render_field<'c, 'd>( + field: Element<'c, ComponentWidgetEvent>, + label: &'d Option, + ) -> Element<'c, ComponentWidgetEvent> { + let before_or_label: Element<_> = match label { + None => Space::with_width(Length::FillPortion(2)).into(), + Some(label) => { + let label: Element<_> = text(label.to_string()) + .shaping(Shaping::Advanced) + .align_x(Horizontal::Right) + .width(Length::Fill) + .into(); + + container(label) + .width(Length::FillPortion(2)) + .themed(ContainerStyle::FormInputLabel) + } + }; + + let form_input = container(field).width(Length::FillPortion(3)).into(); + + let after = Space::with_width(Length::FillPortion(2)).into(); + + let content = vec![before_or_label, form_input, after]; + + let row: Element<_> = row(content).align_y(Alignment::Center).themed(RowStyle::FormInput); + + row + } + + match members { + FormWidgetOrderedMembers::Separator(widget) => self.render_separator_widget(widget), + FormWidgetOrderedMembers::TextField(widget) => { + render_field(self.render_text_field_widget(widget), &widget.label) + } + FormWidgetOrderedMembers::PasswordField(widget) => { + render_field(self.render_password_field_widget(widget), &widget.label) + } + FormWidgetOrderedMembers::Checkbox(widget) => { + render_field(self.render_checkbox_widget(widget), &widget.label) + } + FormWidgetOrderedMembers::DatePicker(widget) => { + render_field(self.render_date_picker_widget(widget), &widget.label) + } + FormWidgetOrderedMembers::Select(widget) => { + render_field(self.render_select_widget(widget), &widget.label) + } + } + }) + .collect(); + + let content: Element<_> = column(items).into(); + + let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::FormInner); + + let content: Element<_> = scrollable(content).width(Length::Fill).into(); + + let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::Form); + + self.render_plugin_root( + *show_action_panel, + widget_id, + None, + &None, + &widget.content.actions, + content, + widget.is_loading.unwrap_or(false), + plugin_view_state, + entrypoint_name, + action_shortcuts, + ) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct SelectItem { + value: String, + label: String, +} + +impl Display for SelectItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.label) + } +} diff --git a/rust/client/src/ui/widget/grid.rs b/rust/client/src/ui/widget/grid.rs new file mode 100644 index 0000000..eedf57c --- /dev/null +++ b/rust/client/src/ui/widget/grid.rs @@ -0,0 +1,332 @@ +use std::cell::Cell; +use std::collections::HashMap; + +use gauntlet_common::model::GridItemWidget; +use gauntlet_common::model::GridSectionWidget; +use gauntlet_common::model::GridSectionWidgetOrderedMembers; +use gauntlet_common::model::GridWidget; +use gauntlet_common::model::GridWidgetOrderedMembers; +use gauntlet_common::model::PhysicalShortcut; +use iced::advanced::text::Shaping; +use iced::alignment::Vertical; +use iced::widget::button; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_space; +use iced::widget::row; +use iced::widget::scrollable; +use iced::widget::text; +use iced::Length; +use iced_aw::grid; +use iced_aw::grid_row; +use iced_aw::GridRow; +use itertools::Itertools; + +use crate::ui::state::PluginViewState; +use crate::ui::theme::button::ButtonStyle; +use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::grid::GridStyle; +use crate::ui::theme::row::RowStyle; +use crate::ui::theme::text::TextStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::accessories::render_icon_accessory; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::events::ComponentWidgetEvent; +use crate::ui::widget::state::RootState; + +impl<'b> ComponentWidgets<'b> { + pub fn render_grid_widget<'a>( + &self, + grid_widget: &GridWidget, + plugin_view_state: &PluginViewState, + entrypoint_name: &str, + action_shortcuts: &HashMap, + ) -> Element<'a, ComponentWidgetEvent> { + let RootState { + show_action_panel, + focused_item, + } = self.root_state(grid_widget.__id__); + + let content = if grid_widget.content.ordered_members.is_empty() { + match &grid_widget.content.empty_view { + Some(widget) => self.render_empty_view_widget(widget), + None => horizontal_space().into(), + } + } else { + let mut pending: Vec<&GridItemWidget> = vec![]; + let mut items: Vec> = vec![]; + let index_counter = &Cell::new(0); + let mut first_section = true; + + for members in &grid_widget.content.ordered_members { + match &members { + GridWidgetOrderedMembers::GridItem(widget) => { + first_section = false; + pending.push(widget) + } + GridWidgetOrderedMembers::GridSection(widget) => { + if !pending.is_empty() { + let content = + self.render_grid(&pending, &grid_widget.columns, focused_item.index, index_counter); + + items.push(content); + + pending = vec![]; + } + + items.push(self.render_grid_section_widget( + widget, + focused_item.index, + index_counter, + first_section, + )); + + first_section = false; + } + } + } + + if !pending.is_empty() { + let content = self.render_grid(&pending, &grid_widget.columns, focused_item.index, index_counter); + + items.push(content); + } + + let content: Element<_> = column(items).into(); + + let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::GridInner); + + let content: Element<_> = scrollable(content) + .id(focused_item.scrollable_id.clone()) + .width(Length::Fill) + .into(); + + let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::Grid); + + content + }; + + let focused_item_id = ComponentWidgets::grid_focused_item_id(focused_item, grid_widget); + + self.render_plugin_root( + *show_action_panel, + grid_widget.__id__, + focused_item_id, + &grid_widget.content.search_bar, + &grid_widget.content.actions, + content, + grid_widget.is_loading.unwrap_or(false), + plugin_view_state, + entrypoint_name, + action_shortcuts, + ) + } + + fn render_grid_section_widget<'a>( + &self, + widget: &GridSectionWidget, + item_focus_index: Option, + index_counter: &Cell, + first_section: bool, + ) -> Element<'a, ComponentWidgetEvent> { + let items: Vec<_> = widget + .content + .ordered_members + .iter() + .map(|members| { + match members { + GridSectionWidgetOrderedMembers::GridItem(widget) => widget, + } + }) + .collect(); + + let content = self.render_grid(&items, &widget.columns, item_focus_index, index_counter); + + let section_title_style = if first_section { + RowStyle::GridFirstSectionTitle + } else { + RowStyle::GridSectionTitle + }; + + render_section( + content, + Some(&widget.title), + &widget.subtitle, + section_title_style, + TextStyle::GridSectionTitle, + TextStyle::GridSectionSubtitle, + ) + } + + fn render_grid_item_widget<'a>( + &self, + widget: &GridItemWidget, + item_focus_index: Option, + index_counter: &Cell, + grid_width: usize, + ) -> Element<'a, ComponentWidgetEvent> { + let height = match grid_width { + ..4 => 130, + 4 => 150, + 5 => 130, + 6 => 110, + 7 => 90, + 8 => 70, + 8.. => 50, + }; + + let content: Element<_> = container(self.render_content_widget(&widget.content.content, true)) + .height(height) + .into(); + + let style = match item_focus_index { + None => ButtonStyle::GridItem, + Some(focused_index) => { + if focused_index == index_counter.get() { + ButtonStyle::GridItemFocused + } else { + ButtonStyle::GridItem + } + } + }; + + index_counter.set(index_counter.get() + 1); + + let action_ids = self.get_action_ids(); + let primary_action = action_ids.first(); + + let on_press_msg = match primary_action { + None => ComponentWidgetEvent::Noop, + Some(widget_id) => { + ComponentWidgetEvent::RunPrimaryAction { + widget_id: *widget_id, + id: Some(widget.id.clone()), + } + } + }; + + let content: Element<_> = button(content).on_press(on_press_msg).width(Length::Fill).themed(style); + + let mut sub_content_left = vec![]; + + if let Some(title) = &widget.title { + // TODO text truncation when iced supports it + let title = text(title.to_string()) + .shaping(Shaping::Advanced) + .themed(TextStyle::GridItemTitle); + + sub_content_left.push(title); + } + + if let Some(subtitle) = &widget.subtitle { + let subtitle = text(subtitle.to_string()) + .shaping(Shaping::Advanced) + .themed(TextStyle::GridItemSubTitle); + + sub_content_left.push(subtitle); + } + + let mut sub_content_right = vec![]; + if let Some(widget) = &widget.content.accessory { + sub_content_right.push(render_icon_accessory(self.images, widget)); + } + + let sub_content_left: Element<_> = column(sub_content_left).width(Length::Fill).into(); + + let sub_content_right: Element<_> = column(sub_content_right).width(Length::Shrink).into(); + + let sub_content: Element<_> = row(vec![sub_content_left, sub_content_right]).themed(RowStyle::GridItemTitle); + + let content: Element<_> = column(vec![content, sub_content]).width(Length::Fill).into(); + + content + } + + fn render_grid<'a>( + &self, + items: &[&GridItemWidget], + /*aspect_ratio: Option<&str>,*/ + columns: &Option, + item_focus_index: Option, + index_counter: &Cell, + ) -> Element<'a, ComponentWidgetEvent> { + // TODO + // let (width, height) = match aspect_ratio { + // None => (1, 1), + // Some("1") => (1, 1), + // Some("3/2") => (3, 2), + // Some("2/3") => (2, 3), + // Some("4/3") => (4, 3), + // Some("3/4") => (3, 4), + // Some("16/9") => (16, 9), + // Some("9/16") => (9, 16), + // Some(value) => panic!("unsupported aspect_ratio {:?}", value) + // }; + + let grid_width = grid_width(columns); + + let rows: Vec> = items + .iter() + .map(|widget| self.render_grid_item_widget(widget, item_focus_index, index_counter, grid_width)) + .chunks(grid_width) + .into_iter() + .map(|row_items| { + let mut row_items: Vec<_> = row_items.collect(); + row_items.resize_with(grid_width, || horizontal_space().into()); + + grid_row(row_items).into() + }) + .collect(); + + let grid: Element<_> = grid(rows) + .width(Length::Fill) + .vertical_alignment(Vertical::Top) + .themed(GridStyle::Default); + + grid + } +} + +pub fn grid_width(columns: &Option) -> usize { + columns.map(|value| value.trunc() as usize).unwrap_or(5) +} + +pub fn render_section<'a>( + content: Element<'a, ComponentWidgetEvent>, + title: Option<&str>, + subtitle: &Option, + theme_kind_title: RowStyle, + theme_kind_title_text: TextStyle, + theme_kind_subtitle_text: TextStyle, +) -> Element<'a, ComponentWidgetEvent> { + let mut title_content = vec![]; + + if let Some(title) = title { + let title: Element<_> = text(title.to_string()) + .shaping(Shaping::Advanced) + .size(15) + .themed(theme_kind_title_text); + + title_content.push(title) + } + + if let Some(subtitle) = subtitle { + let subtitle: Element<_> = text(subtitle.to_string()) + .shaping(Shaping::Advanced) + .size(15) + .themed(theme_kind_subtitle_text); + + title_content.push(subtitle) + } + + if title_content.is_empty() { + let space: Element<_> = horizontal_space().height(40).into(); + + title_content.push(space) + } + + let title_content = row(title_content).themed(theme_kind_title); + + column([title_content, content]).into() +} diff --git a/rust/client/src/ui/widget/images.rs b/rust/client/src/ui/widget/images.rs new file mode 100644 index 0000000..8d63e2f --- /dev/null +++ b/rust/client/src/ui/widget/images.rs @@ -0,0 +1,262 @@ +use std::collections::HashMap; + +use gauntlet_common::model::Icons; +use gauntlet_common::model::ImageLike; +use gauntlet_common::model::UiWidgetId; +use iced::advanced::image::Handle; +use iced::widget::horizontal_space; +use iced::widget::image; +use iced::widget::value; +use iced_fonts::Bootstrap; +use iced_fonts::BOOTSTRAP_FONT; + +use crate::ui::theme::text::TextStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; + +pub fn render_image<'a, T: 'a + Clone>( + images: &HashMap>, + widget_id: UiWidgetId, + image_data: &ImageLike, + icon_style: Option, +) -> Element<'a, T> { + match image_data { + ImageLike::ImageSource(_) => { + match images.get(&widget_id) { + Some(bytes) => image(Handle::from_bytes(bytes.clone())).into(), + None => horizontal_space().into(), + } + } + ImageLike::Icons(icon) => { + match icon_style { + None => value(icon_to_bootstrap(icon)).font(BOOTSTRAP_FONT).into(), + Some(icon_style) => value(icon_to_bootstrap(icon)).font(BOOTSTRAP_FONT).themed(icon_style), + } + } + } +} + +pub fn icon_to_bootstrap(icon: &Icons) -> Bootstrap { + match icon { + Icons::Airplane => Bootstrap::Airplane, + Icons::Alarm => Bootstrap::Alarm, + Icons::AlignCentre => Bootstrap::AlignCenter, + Icons::AlignLeft => Bootstrap::AlignStart, + Icons::AlignRight => Bootstrap::AlignEnd, + // Icons::Anchor => Bootstrap::, + Icons::ArrowClockwise => Bootstrap::ArrowClockwise, + Icons::ArrowCounterClockwise => Bootstrap::ArrowCounterclockwise, + Icons::ArrowDown => Bootstrap::ArrowDown, + Icons::ArrowLeft => Bootstrap::ArrowLeft, + Icons::ArrowRight => Bootstrap::ArrowRight, + Icons::ArrowUp => Bootstrap::ArrowUp, + Icons::ArrowLeftRight => Bootstrap::ArrowLeftRight, + Icons::ArrowsContract => Bootstrap::ArrowsAngleContract, + Icons::ArrowsExpand => Bootstrap::ArrowsAngleExpand, + Icons::AtSymbol => Bootstrap::At, + // Icons::BandAid => Bootstrap::Bandaid, + Icons::Cash => Bootstrap::Cash, + // Icons::BarChart => Bootstrap::BarChart, + // Icons::BarCode => Bootstrap::, + Icons::Battery => Bootstrap::Battery, + Icons::BatteryCharging => Bootstrap::BatteryCharging, + // Icons::BatteryDisabled => Bootstrap::, + Icons::Bell => Bootstrap::Bell, + Icons::BellDisabled => Bootstrap::BellSlash, + // Icons::Bike => Bootstrap::Bicycle, + // Icons::Binoculars => Bootstrap::Binoculars, + // Icons::Bird => Bootstrap::, + Icons::Bluetooth => Bootstrap::Bluetooth, + // Icons::Boat => Bootstrap::, + Icons::Bold => Bootstrap::TypeBold, + // Icons::Bolt => Bootstrap::, + // Icons::BoltDisabled => Bootstrap::, + Icons::Book => Bootstrap::Book, + Icons::Bookmark => Bootstrap::Bookmark, + Icons::Box => Bootstrap::Box, + // Icons::Brush => Bootstrap::Brush, + Icons::Bug => Bootstrap::Bug, + Icons::Building => Bootstrap::Building, + Icons::BulletPoints => Bootstrap::ListUl, + Icons::Calculator => Bootstrap::Calculator, + Icons::Calendar => Bootstrap::Calendar, + Icons::Camera => Bootstrap::Camera, + Icons::Car => Bootstrap::CarFront, + Icons::Cart => Bootstrap::Cart, + // Icons::Cd => Bootstrap::, + // Icons::Center => Bootstrap::, + Icons::Checkmark => Bootstrap::Checktwo, + // Icons::ChessPiece => Bootstrap::, + Icons::ChevronDown => Bootstrap::ChevronDown, + Icons::ChevronLeft => Bootstrap::ChevronLeft, + Icons::ChevronRight => Bootstrap::ChevronRight, + Icons::ChevronUp => Bootstrap::ChevronUp, + Icons::ChevronExpand => Bootstrap::ChevronExpand, + Icons::Circle => Bootstrap::Circle, + // Icons::CircleProgress100 => Bootstrap::, + // Icons::CircleProgress25 => Bootstrap::, + // Icons::CircleProgress50 => Bootstrap::, + // Icons::CircleProgress75 => Bootstrap::, + // Icons::ClearFormatting => Bootstrap::, + Icons::Clipboard => Bootstrap::Clipboard, + Icons::Clock => Bootstrap::Clock, + Icons::Cloud => Bootstrap::Cloud, + Icons::CloudLightning => Bootstrap::CloudLightning, + Icons::CloudRain => Bootstrap::CloudRain, + Icons::CloudSnow => Bootstrap::CloudSnow, + Icons::CloudSun => Bootstrap::CloudSun, + Icons::Code => Bootstrap::Code, + Icons::Gear => Bootstrap::Gear, + Icons::Coin => Bootstrap::Coin, + Icons::Command => Bootstrap::Command, + Icons::Compass => Bootstrap::Compass, + // Icons::ComputerChip => Bootstrap::, + // Icons::Contrast => Bootstrap::, + Icons::CreditCard => Bootstrap::CreditCard, + Icons::Crop => Bootstrap::Crop, + // Icons::Crown => Bootstrap::, + Icons::Document => Bootstrap::FileEarmark, + Icons::DocumentAdd => Bootstrap::FileEarmarkPlus, + Icons::DocumentDelete => Bootstrap::FileEarmarkX, + Icons::Dot => Bootstrap::Dot, + Icons::Download => Bootstrap::Download, + // Icons::Duplicate => Bootstrap::, + Icons::Eject => Bootstrap::Eject, + Icons::ThreeDots => Bootstrap::ThreeDots, + Icons::Envelope => Bootstrap::Envelope, + Icons::Eraser => Bootstrap::Eraser, + Icons::ExclamationMark => Bootstrap::ExclamationLg, + Icons::Eye => Bootstrap::Eye, + Icons::EyeDisabled => Bootstrap::EyeSlash, + Icons::EyeDropper => Bootstrap::Eyedropper, + Icons::Female => Bootstrap::GenderFemale, + Icons::Film => Bootstrap::Film, + Icons::Filter => Bootstrap::Filter, + Icons::Fingerprint => Bootstrap::Fingerprint, + Icons::Flag => Bootstrap::Flag, + Icons::Folder => Bootstrap::Folder, + Icons::FolderAdd => Bootstrap::FolderPlus, + Icons::FolderDelete => Bootstrap::FolderMinus, + Icons::Forward => Bootstrap::Forward, + Icons::GameController => Bootstrap::Controller, + Icons::Virus => Bootstrap::Virus, + Icons::Gift => Bootstrap::Gift, + Icons::Glasses => Bootstrap::Eyeglasses, + Icons::Globe => Bootstrap::Globe, + Icons::Hammer => Bootstrap::Hammer, + Icons::HardDrive => Bootstrap::DeviceHdd, + Icons::Headphones => Bootstrap::Headphones, + Icons::Heart => Bootstrap::Heart, + // Icons::HeartDisabled => Bootstrap::, + Icons::Heartbeat => Bootstrap::Activity, + Icons::Hourglass => Bootstrap::Hourglass, + Icons::House => Bootstrap::House, + Icons::Image => Bootstrap::Image, + Icons::Info => Bootstrap::InfoLg, + Icons::Italics => Bootstrap::TypeItalic, + Icons::Key => Bootstrap::Key, + Icons::Keyboard => Bootstrap::Keyboard, + Icons::Layers => Bootstrap::Layers, + // Icons::Leaf => Bootstrap::, + Icons::LightBulb => Bootstrap::Lightbulb, + Icons::LightBulbDisabled => Bootstrap::LightbulbOff, + Icons::Link => Bootstrap::LinkFourfivedeg, + Icons::List => Bootstrap::List, + Icons::Lock => Bootstrap::Lock, + // Icons::LockDisabled => Bootstrap::, + Icons::LockUnlocked => Bootstrap::Unlock, + // Icons::Logout => Bootstrap::, + // Icons::Lowercase => Bootstrap::, + // Icons::MagnifyingGlass => Bootstrap::, + Icons::Male => Bootstrap::GenderMale, + Icons::Map => Bootstrap::Map, + Icons::Maximize => Bootstrap::Fullscreen, + Icons::Megaphone => Bootstrap::Megaphone, + Icons::MemoryModule => Bootstrap::Memory, + Icons::MemoryStick => Bootstrap::UsbDrive, + Icons::Message => Bootstrap::Chat, + Icons::Microphone => Bootstrap::Mic, + Icons::MicrophoneDisabled => Bootstrap::MicMute, + Icons::Minimize => Bootstrap::FullscreenExit, + Icons::Minus => Bootstrap::Dash, + Icons::Mobile => Bootstrap::Phone, + // Icons::Monitor => Bootstrap::, + Icons::Moon => Bootstrap::Moon, + // Icons::Mountain => Bootstrap::, + Icons::Mouse => Bootstrap::Mouse, + Icons::Multiply => Bootstrap::X, + Icons::Music => Bootstrap::MusicNoteBeamed, + Icons::Network => Bootstrap::BroadcastPin, + Icons::Paperclip => Bootstrap::Paperclip, + Icons::Paragraph => Bootstrap::TextParagraph, + Icons::Pause => Bootstrap::Pause, + Icons::Pencil => Bootstrap::Pencil, + Icons::Person => Bootstrap::Person, + Icons::PersonAdd => Bootstrap::PersonAdd, + Icons::PersonRemove => Bootstrap::PersonDash, + Icons::Phone => Bootstrap::Telephone, + // Icons::PhoneRinging => Bootstrap::, + Icons::PieChart => Bootstrap::PieChart, + Icons::Capsule => Bootstrap::Capsule, + // Icons::Pin => Bootstrap::, + // Icons::PinDisabled => Bootstrap::, + Icons::Play => Bootstrap::Play, + Icons::Plug => Bootstrap::Plug, + Icons::Plus => Bootstrap::Plus, + // Icons::PlusMinusDivideMultiply => Bootstrap::, + Icons::Power => Bootstrap::Power, + Icons::Printer => Bootstrap::Printer, + Icons::QuestionMark => Bootstrap::QuestionLg, + Icons::Quotes => Bootstrap::Quote, + Icons::Receipt => Bootstrap::Receipt, + Icons::Repeat => Bootstrap::Repeat, + Icons::Reply => Bootstrap::Reply, + Icons::Rewind => Bootstrap::Rewind, + Icons::Rocket => Bootstrap::Rocket, + // Icons::Ruler => Bootstrap::, + Icons::Shield => Bootstrap::Shield, + Icons::Shuffle => Bootstrap::Shuffle, + Icons::Snippets => Bootstrap::BodyText, + Icons::Snowflake => Bootstrap::Snow, + // Icons::VolumeHigh => Bootstrap::VolumeUp, + // Icons::VolumeLow => Bootstrap::VolumeDown, + // Icons::VolumeOff => Bootstrap::VolumeOff, + // Icons::VolumeOn => Bootstrap::, + Icons::Star => Bootstrap::Star, + // Icons::StarDisabled => Bootstrap::, + Icons::Stop => Bootstrap::Stop, + Icons::Stopwatch => Bootstrap::Stopwatch, + Icons::StrikeThrough => Bootstrap::TypeStrikethrough, + Icons::Sun => Bootstrap::SunFill, // TODO why Sun isn't in iced_aw? + Icons::Scissors => Bootstrap::Scissors, + // Icons::Syringe => Bootstrap::, + Icons::Tag => Bootstrap::Tag, + Icons::Thermometer => Bootstrap::Thermometer, + Icons::Terminal => Bootstrap::Terminal, + Icons::Text => Bootstrap::Fonts, + Icons::TextCursor => Bootstrap::CursorText, + // Icons::TextSelection => Bootstrap::, + // Icons::Torch => Bootstrap::, + // Icons::Train => Bootstrap::, + Icons::Trash => Bootstrap::Trash, + Icons::Tree => Bootstrap::Tree, + Icons::Trophy => Bootstrap::Trophy, + Icons::People => Bootstrap::People, + Icons::Umbrella => Bootstrap::Umbrella, + Icons::Underline => Bootstrap::TypeUnderline, + Icons::Upload => Bootstrap::Upload, + // Icons::Uppercase => Bootstrap::, + Icons::Wallet => Bootstrap::Wallet, + Icons::Wand => Bootstrap::Magic, + // Icons::Warning => Bootstrap::, + // Icons::Weights => Bootstrap::, + Icons::Wifi => Bootstrap::Wifi, + Icons::WifiDisabled => Bootstrap::WifiOff, + Icons::Window => Bootstrap::Window, + Icons::Tools => Bootstrap::Tools, + Icons::Watch => Bootstrap::Watch, + Icons::XMark => Bootstrap::XLg, + Icons::Indent => Bootstrap::Indent, + Icons::Unindent => Bootstrap::Unindent, + } +} diff --git a/rust/client/src/ui/widget/inline.rs b/rust/client/src/ui/widget/inline.rs new file mode 100644 index 0000000..3c9e45a --- /dev/null +++ b/rust/client/src/ui/widget/inline.rs @@ -0,0 +1,85 @@ +use gauntlet_common::model::InlineSeparatorWidget; +use gauntlet_common::model::InlineWidget; +use gauntlet_common::model::InlineWidgetOrderedMembers; +use iced::advanced::text::Shaping; +use iced::alignment::Horizontal; +use iced::widget::column; +use iced::widget::container; +use iced::widget::row; +use iced::widget::text; +use iced::widget::value; +use iced::widget::vertical_rule; +use iced::Alignment; +use iced::Length; +use iced_fonts::BOOTSTRAP_FONT; + +use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::text::TextStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::events::ComponentWidgetEvent; +use crate::ui::widget::images::icon_to_bootstrap; + +impl<'b> ComponentWidgets<'b> { + fn render_inline_separator_widget<'a>(&self, widget: &InlineSeparatorWidget) -> Element<'a, ComponentWidgetEvent> { + match &widget.icon { + None => vertical_rule(1).into(), + Some(icon) => { + let top_rule: Element<_> = vertical_rule(1).into(); + + let top_rule = container(top_rule).align_x(Horizontal::Center).into(); + + let icon = value(icon_to_bootstrap(icon)) + .font(BOOTSTRAP_FONT) + .size(45) + .themed(TextStyle::InlineSeparator); + + let bot_rule: Element<_> = vertical_rule(1).into(); + + let bot_rule = container(bot_rule).align_x(Horizontal::Center).into(); + + column([top_rule, icon, bot_rule]).align_x(Alignment::Center).into() + } + } + } + + pub fn render_inline_widget<'a>( + &self, + widget: &InlineWidget, + plugin_name: &str, + entrypoint_name: &str, + ) -> Element<'a, ComponentWidgetEvent> { + let name: Element<_> = text(format!("{} - {}", plugin_name, entrypoint_name)) + .shaping(Shaping::Advanced) + .themed(TextStyle::InlineName); + + let name: Element<_> = container(name).themed(ContainerStyle::InlineName); + + let content: Vec> = widget + .content + .ordered_members + .iter() + .map(|members| { + match members { + InlineWidgetOrderedMembers::Content(widget) => { + let element = self.render_content_widget(widget, true); + + container(element).into() + } + InlineWidgetOrderedMembers::InlineSeparator(widget) => self.render_inline_separator_widget(widget), + } + }) + .collect(); + + let content: Element<_> = row(content).into(); + + let content: Element<_> = container(content).themed(ContainerStyle::InlineInner); + + let content: Element<_> = column(vec![name, content]).width(Length::Fill).into(); + + let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::Inline); + + content + } +} diff --git a/rust/client/src/ui/widget/list.rs b/rust/client/src/ui/widget/list.rs new file mode 100644 index 0000000..3d8f345 --- /dev/null +++ b/rust/client/src/ui/widget/list.rs @@ -0,0 +1,274 @@ +use std::cell::Cell; +use std::collections::HashMap; + +use gauntlet_common::model::ListItemAccessories; +use gauntlet_common::model::ListItemWidget; +use gauntlet_common::model::ListSectionWidget; +use gauntlet_common::model::ListSectionWidgetOrderedMembers; +use gauntlet_common::model::ListWidget; +use gauntlet_common::model::ListWidgetOrderedMembers; +use gauntlet_common::model::PhysicalShortcut; +use iced::advanced::text::Shaping; +use iced::widget::button; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_space; +use iced::widget::row; +use iced::widget::scrollable; +use iced::widget::text; +use iced::widget::vertical_rule; +use iced::Alignment; +use iced::Length; + +use crate::ui::state::PluginViewState; +use crate::ui::theme::button::ButtonStyle; +use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::row::RowStyle; +use crate::ui::theme::text::TextStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::accessories::render_icon_accessory; +use crate::ui::widget::accessories::render_text_accessory; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::events::ComponentWidgetEvent; +use crate::ui::widget::grid::render_section; +use crate::ui::widget::images::render_image; +use crate::ui::widget::state::RootState; + +impl<'b> ComponentWidgets<'b> { + pub fn render_list_widget<'a>( + &self, + list_widget: &ListWidget, + plugin_view_state: &PluginViewState, + entrypoint_name: &str, + action_shortcuts: &HashMap, + ) -> Element<'a, ComponentWidgetEvent> { + let widget_id = list_widget.__id__; + let RootState { + show_action_panel, + focused_item, + } = self.root_state(widget_id); + + let mut pending: Vec<&ListItemWidget> = vec![]; + let mut items: Vec> = vec![]; + let index_counter = &Cell::new(0); + let mut first_section = true; + + for members in &list_widget.content.ordered_members { + match &members { + ListWidgetOrderedMembers::ListItem(widget) => { + first_section = false; + pending.push(widget) + } + ListWidgetOrderedMembers::ListSection(widget) => { + if !pending.is_empty() { + let content: Vec<_> = pending + .iter() + .map(|widget| self.render_list_item_widget(widget, focused_item.index, index_counter)) + .collect(); + + let content: Element<_> = column(content).into(); + + items.push(content); + + pending = vec![]; + } + + items.push(self.render_list_section_widget( + widget, + focused_item.index, + index_counter, + first_section, + )); + + first_section = false; + } + } + } + + if !pending.is_empty() { + let content: Vec<_> = pending + .iter() + .map(|widget| self.render_list_item_widget(widget, focused_item.index, index_counter)) + .collect(); + + let content: Element<_> = column(content).into(); + + items.push(content); + } + + let content = if items.is_empty() { + match &list_widget.content.empty_view { + Some(widget) => self.render_empty_view_widget(widget), + None => horizontal_space().into(), + } + } else { + let content: Element<_> = column(items).width(Length::Fill).into(); + + let content: Element<_> = container(content).width(Length::Fill).themed(ContainerStyle::ListInner); + + let content: Element<_> = scrollable(content) + .id(focused_item.scrollable_id.clone()) + .width(Length::Fill) + .into(); + + let content: Element<_> = container(content) + .width(Length::FillPortion(3)) + .themed(ContainerStyle::List); + + content + }; + + let mut elements = vec![content]; + + if let Some(detail) = &list_widget.content.detail { + let detail = self.render_detail_widget(detail, true); + + let detail: Element<_> = container(detail).width(Length::FillPortion(5)).into(); + + let separator: Element<_> = vertical_rule(1).into(); + + elements.push(separator); + + elements.push(detail); + } + + let content: Element<_> = row(elements).height(Length::Fill).into(); + + let focused_item_id = ComponentWidgets::list_focused_item_id(focused_item, list_widget); + + self.render_plugin_root( + *show_action_panel, + widget_id, + focused_item_id, + &list_widget.content.search_bar, + &list_widget.content.actions, + content, + list_widget.is_loading.unwrap_or(false), + plugin_view_state, + entrypoint_name, + action_shortcuts, + ) + } + + fn render_list_section_widget<'a>( + &self, + widget: &ListSectionWidget, + item_focus_index: Option, + index_counter: &Cell, + first_section: bool, + ) -> Element<'a, ComponentWidgetEvent> { + let content: Vec<_> = widget + .content + .ordered_members + .iter() + .map(|members| { + match members { + ListSectionWidgetOrderedMembers::ListItem(widget) => { + self.render_list_item_widget(widget, item_focus_index, index_counter) + } + } + }) + .collect(); + + let content = column(content).into(); + + let section_title_style = if first_section { + RowStyle::ListFirstSectionTitle + } else { + RowStyle::ListSectionTitle + }; + + render_section( + content, + Some(&widget.title), + &widget.subtitle, + section_title_style, + TextStyle::ListSectionTitle, + TextStyle::ListSectionSubtitle, + ) + } + + fn render_list_item_widget<'a>( + &self, + widget: &ListItemWidget, + item_focus_index: Option, + index_counter: &Cell, + ) -> Element<'a, ComponentWidgetEvent> { + let icon: Option> = widget + .icon + .as_ref() + .map(|icon| render_image(self.images, widget.__id__, icon, None)); + + let title: Element<_> = text(widget.title.to_string()).shaping(Shaping::Advanced).into(); + let title: Element<_> = container(title).themed(ContainerStyle::ListItemTitle); + + let mut content = vec![title]; + + if let Some(icon) = icon { + let icon: Element<_> = container(icon).themed(ContainerStyle::ListItemIcon); + + content.insert(0, icon) + } + + if let Some(subtitle) = &widget.subtitle { + let subtitle: Element<_> = text(subtitle.to_string()) + .shaping(Shaping::Advanced) + .themed(TextStyle::ListItemSubtitle); + let subtitle: Element<_> = container(subtitle).themed(ContainerStyle::ListItemSubtitle); + + content.push(subtitle) + } + + if widget.content.accessories.len() > 0 { + let accessories: Vec> = widget + .content + .accessories + .iter() + .map(|accessory| { + match accessory { + ListItemAccessories::_0(widget) => render_text_accessory(self.images, widget), + ListItemAccessories::_1(widget) => render_icon_accessory(self.images, widget), + } + }) + .collect(); + + let accessories: Element<_> = row(accessories).into(); + + let space = horizontal_space().into(); + + content.push(space); + content.push(accessories); + } + + let content: Element<_> = row(content).align_y(Alignment::Center).into(); + + let style = match item_focus_index { + None => ButtonStyle::ListItem, + Some(focused_index) => { + if focused_index == index_counter.get() { + ButtonStyle::ListItemFocused + } else { + ButtonStyle::ListItem + } + } + }; + + index_counter.set(index_counter.get() + 1); + + let action_ids = self.get_action_ids(); + let primary_action = action_ids.first(); + + let on_press_msg = match primary_action { + None => ComponentWidgetEvent::Noop, + Some(widget_id) => { + ComponentWidgetEvent::RunPrimaryAction { + widget_id: *widget_id, + id: Some(widget.id.clone()), + } + } + }; + + button(content).on_press(on_press_msg).width(Length::Fill).themed(style) + } +} diff --git a/rust/client/src/ui/widget/metadata.rs b/rust/client/src/ui/widget/metadata.rs new file mode 100644 index 0000000..48a4ea4 --- /dev/null +++ b/rust/client/src/ui/widget/metadata.rs @@ -0,0 +1,201 @@ +use gauntlet_common::model::MetadataIconWidget; +use gauntlet_common::model::MetadataLinkWidget; +use gauntlet_common::model::MetadataSeparatorWidget; +use gauntlet_common::model::MetadataTagItemWidget; +use gauntlet_common::model::MetadataTagListWidget; +use gauntlet_common::model::MetadataTagListWidgetOrderedMembers; +use gauntlet_common::model::MetadataValueWidget; +use gauntlet_common::model::MetadataWidget; +use gauntlet_common::model::MetadataWidgetOrderedMembers; +use iced::advanced::text::Shaping; +use iced::widget::button; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_rule; +use iced::widget::horizontal_space; +use iced::widget::row; +use iced::widget::scrollable; +use iced::widget::text; +use iced::widget::tooltip; +use iced::widget::tooltip::Position; +use iced::widget::value; +use iced::Alignment; +use iced::Length; +use iced_fonts::Bootstrap; +use iced_fonts::BOOTSTRAP_FONT; + +use crate::ui::theme::button::ButtonStyle; +use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::text::TextStyle; +use crate::ui::theme::tooltip::TooltipStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::events::ComponentWidgetEvent; +use crate::ui::widget::images::icon_to_bootstrap; +use crate::ui::widget::text::TextRenderType; + +impl<'b> ComponentWidgets<'b> { + fn render_metadata_tag_item_widget<'a>(&self, widget: &MetadataTagItemWidget) -> Element<'a, ComponentWidgetEvent> { + let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); + + let tag: Element<_> = button(content) + .on_press(ComponentWidgetEvent::TagClick { + widget_id: widget.__id__, + }) + .themed(ButtonStyle::MetadataTagItem); + + container(tag).themed(ContainerStyle::MetadataTagItem) + } + + fn render_metadata_tag_list_widget<'a>( + &self, + widget: &MetadataTagListWidget, + is_in_list: bool, + ) -> Element<'a, ComponentWidgetEvent> { + let content: Vec> = widget + .content + .ordered_members + .iter() + .map(|members| { + match members { + MetadataTagListWidgetOrderedMembers::MetadataTagItem(content) => { + self.render_metadata_tag_item_widget(&content) + } + } + }) + .collect(); + + let value = row(content).wrap().into(); + + render_metadata_item(&widget.label, value, is_in_list).into() + } + + fn render_metadata_link_widget<'a>( + &self, + widget: &MetadataLinkWidget, + is_in_list: bool, + ) -> Element<'a, ComponentWidgetEvent> { + let content: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); + + let icon: Element<_> = value(Bootstrap::BoxArrowUpRight).font(BOOTSTRAP_FONT).size(16).into(); + + let icon = container(icon).themed(ContainerStyle::MetadataLinkIcon); + + let content: Element<_> = row([content, icon]).align_y(Alignment::Center).into(); + + let link: Element<_> = button(content) + .on_press(ComponentWidgetEvent::LinkClick { + widget_id: widget.__id__, + href: widget.href.to_owned(), + }) + .themed(ButtonStyle::MetadataLink); + + let content: Element<_> = if widget.href.is_empty() { + link + } else { + let href: Element<_> = text(widget.href.to_string()).shaping(Shaping::Advanced).into(); + + tooltip(link, href, Position::Top).themed(TooltipStyle::Tooltip) + }; + + render_metadata_item(&widget.label, content, is_in_list).into() + } + + fn render_metadata_value_widget<'a>( + &self, + widget: &MetadataValueWidget, + is_in_list: bool, + ) -> Element<'a, ComponentWidgetEvent> { + let value: Element<_> = self.render_text(&widget.content.text, TextRenderType::None); + + render_metadata_item(&widget.label, value, is_in_list).into() + } + + fn render_metadata_icon_widget<'a>( + &self, + widget: &MetadataIconWidget, + is_in_list: bool, + ) -> Element<'a, ComponentWidgetEvent> { + let value = value(icon_to_bootstrap(&widget.icon)) + .font(BOOTSTRAP_FONT) + .size(26) + .into(); + + render_metadata_item(&widget.label, value, is_in_list).into() + } + + fn render_metadata_separator_widget<'a>( + &self, + _widget: &MetadataSeparatorWidget, + ) -> Element<'a, ComponentWidgetEvent> { + let separator: Element<_> = horizontal_rule(1).into(); + + container(separator) + .width(Length::Fill) + .themed(ContainerStyle::MetadataSeparator) + } + + pub fn render_metadata_widget<'a>( + &self, + widget: &MetadataWidget, + is_in_list: bool, + ) -> Element<'a, ComponentWidgetEvent> { + let content: Vec> = widget + .content + .ordered_members + .iter() + .map(|members| { + match members { + MetadataWidgetOrderedMembers::MetadataTagList(content) => { + self.render_metadata_tag_list_widget(content, is_in_list) + } + MetadataWidgetOrderedMembers::MetadataLink(content) => { + self.render_metadata_link_widget(content, is_in_list) + } + MetadataWidgetOrderedMembers::MetadataValue(content) => { + self.render_metadata_value_widget(content, is_in_list) + } + MetadataWidgetOrderedMembers::MetadataIcon(content) => { + self.render_metadata_icon_widget(content, is_in_list) + } + MetadataWidgetOrderedMembers::MetadataSeparator(content) => { + self.render_metadata_separator_widget(content) + } + } + }) + .collect(); + + let metadata: Element<_> = column(content).into(); + + let metadata = container(metadata) + .width(Length::Fill) + .themed(ContainerStyle::MetadataInner); + + scrollable(metadata).width(Length::Fill).into() + } +} + +fn render_metadata_item<'a>( + label: &str, + value: Element<'a, ComponentWidgetEvent>, + is_in_list: bool, +) -> Element<'a, ComponentWidgetEvent> { + let label: Element<_> = text(label.to_string()) + .shaping(Shaping::Advanced) + .themed(TextStyle::MetadataItemLabel); + + let label = container(label).themed(ContainerStyle::MetadataItemLabel); + + if is_in_list { + let space = horizontal_space().into(); + + let value = container(value).themed(ContainerStyle::MetadataItemValueInList); + + row(vec![label, space, value]).width(Length::Fill).into() + } else { + let value = container(value).themed(ContainerStyle::MetadataItemValue); + + column(vec![label, value]).into() + } +} diff --git a/rust/client/src/ui/widget/mod.rs b/rust/client/src/ui/widget/mod.rs new file mode 100644 index 0000000..04f78f8 --- /dev/null +++ b/rust/client/src/ui/widget/mod.rs @@ -0,0 +1,18 @@ +pub mod accessories; +pub mod action_panel; +mod content; +pub mod data; +pub mod data_mut; +mod detail; +mod empty_view; +pub mod events; +mod form; +mod grid; +mod images; +mod inline; +mod list; +mod metadata; +pub mod root; +mod search_bar; +pub mod state; +mod text; diff --git a/rust/client/src/ui/widget/root.rs b/rust/client/src/ui/widget/root.rs new file mode 100644 index 0000000..6c84fb8 --- /dev/null +++ b/rust/client/src/ui/widget/root.rs @@ -0,0 +1,421 @@ +use std::collections::HashMap; + +use gauntlet_common::model::ActionPanelWidget; +use gauntlet_common::model::PhysicalKey; +use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::RootWidgetMembers; +use gauntlet_common::model::SearchBarWidget; +use gauntlet_common::model::UiWidgetId; +use iced::advanced::text::Shaping; +use iced::widget::button; +use iced::widget::column; +use iced::widget::container; +use iced::widget::horizontal_rule; +use iced::widget::horizontal_space; +use iced::widget::mouse_area; +use iced::widget::row; +use iced::widget::stack; +use iced::widget::text; +use iced::widget::value; +use iced::widget::vertical_rule; +use iced::widget::Space; +use iced::Alignment; +use iced::Length; +use iced_fonts::Bootstrap; +use iced_fonts::BOOTSTRAP_FONT; + +use crate::ui::custom_widgets::loading_bar::LoadingBar; +use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::state::PluginViewState; +use crate::ui::theme::button::ButtonStyle; +use crate::ui::theme::container::ContainerStyle; +use crate::ui::theme::row::RowStyle; +use crate::ui::theme::rule::RuleStyle; +use crate::ui::theme::text::TextStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::action_panel::convert_action_panel; +use crate::ui::widget::action_panel::render_action_panel; +use crate::ui::widget::action_panel::render_shortcut; +use crate::ui::widget::action_panel::ActionPanel; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::events::ComponentWidgetEvent; +use crate::ui::widget::state::RootState; + +impl<'b> ComponentWidgets<'b> { + pub fn render_root_widget<'a>( + &self, + plugin_view_state: &PluginViewState, + entrypoint_name: Option<&String>, + action_shortcuts: &HashMap, + ) -> Element<'a, ComponentWidgetEvent> { + match &self.root_widget { + None => horizontal_space().into(), + Some(root) => { + match &root.content { + None => horizontal_space().into(), + Some(content) => { + let entrypoint_name = + entrypoint_name.expect("entrypoint name should always exist after render"); + + match content { + RootWidgetMembers::Detail(widget) => { + let RootState { show_action_panel, .. } = self.root_state(widget.__id__); + + let content = self.render_detail_widget(widget, false); + + self.render_plugin_root( + *show_action_panel, + widget.__id__, + None, + &None, + &widget.content.actions, + content, + widget.is_loading.unwrap_or(false), + plugin_view_state, + entrypoint_name, + action_shortcuts, + ) + } + RootWidgetMembers::Form(widget) => { + self.render_form_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts) + } + RootWidgetMembers::List(widget) => { + self.render_list_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts) + } + RootWidgetMembers::Grid(widget) => { + self.render_grid_widget(widget, plugin_view_state, entrypoint_name, action_shortcuts) + } + _ => { + panic!("used inline widget in non-inline place") + } + } + } + } + } + } + } + + pub fn render_root_inline_widget<'a>( + &self, + plugin_name: Option<&String>, + entrypoint_name: Option<&String>, + ) -> Element<'a, ComponentWidgetEvent> { + match &self.root_widget { + None => horizontal_space().into(), + Some(root) => { + match &root.content { + None => horizontal_space().into(), + Some(content) => { + match content { + RootWidgetMembers::Inline(widget) => { + let entrypoint_name = + entrypoint_name.expect("entrypoint name should always exist after render"); + let plugin_name = + plugin_name.expect("entrypoint name should always exist after render"); + + self.render_inline_widget(widget, plugin_name, entrypoint_name) + } + _ => { + panic!("used non-inline widget in inline place") + } + } + } + } + } + } + } + + fn render_top_panel<'a>(&self, search_bar: &Option) -> Element<'a, ComponentWidgetEvent> { + let icon = value(Bootstrap::ArrowLeft).font(BOOTSTRAP_FONT); + + let back_button: Element<_> = button(icon) + .on_press(ComponentWidgetEvent::PreviousView) + .themed(ButtonStyle::RootTopPanelBackButton); + + let search_bar_element = search_bar + .as_ref() + .map(|widget| self.render_search_bar_widget(widget)) + .unwrap_or_else(|| Space::with_width(Length::FillPortion(3)).into()); + + let top_panel: Element<_> = row(vec![back_button, search_bar_element]) + .align_y(Alignment::Center) + .themed(RowStyle::RootTopPanel); + + let top_panel: Element<_> = container(top_panel) + .width(Length::Fill) + .themed(ContainerStyle::RootTopPanel); + + top_panel + } + + pub fn render_plugin_root<'a>( + &self, + show_action_panel: bool, + root_widget_id: UiWidgetId, + focused_item_id: Option, + search_bar: &Option, + action_panel: &Option, + content: Element<'a, ComponentWidgetEvent>, + is_loading: bool, + plugin_view_state: &PluginViewState, + entrypoint_name: &str, + action_shortcuts: &HashMap, + ) -> Element<'a, ComponentWidgetEvent> { + let top_panel = self.render_top_panel(search_bar); + + let top_separator = if is_loading { + LoadingBar::new().into() + } else { + horizontal_rule(1).into() + }; + + let mut action_panel = convert_action_panel(action_panel, &action_shortcuts); + + let primary_action = + action_panel + .as_mut() + .map(|panel| panel.find_first()) + .flatten() + .map(|(label, widget_id)| { + let shortcut = PhysicalShortcut { + physical_key: PhysicalKey::Enter, + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + }; + + (label.to_string(), widget_id, shortcut) + }); + + match plugin_view_state { + PluginViewState::None => { + render_root( + show_action_panel, + top_panel, + top_separator, + None, + content, + primary_action, + action_panel, + None::<&ScrollHandle>, + entrypoint_name, + || { + ComponentWidgetEvent::ToggleActionPanel { + widget_id: root_widget_id, + } + }, + |widget_id| { + ComponentWidgetEvent::RunPrimaryAction { + widget_id, + id: focused_item_id.clone(), + } + }, + |widget_id| { + ComponentWidgetEvent::ActionClick { + widget_id, + id: focused_item_id.clone(), + } + }, + || ComponentWidgetEvent::Noop, + ) + } + PluginViewState::ActionPanel { focused_action_item } => { + render_root( + show_action_panel, + top_panel, + top_separator, + None, + content, + primary_action, + action_panel, + Some(&focused_action_item), + entrypoint_name, + || { + ComponentWidgetEvent::ToggleActionPanel { + widget_id: root_widget_id, + } + }, + |widget_id| { + ComponentWidgetEvent::RunPrimaryAction { + widget_id, + id: focused_item_id.clone(), + } + }, + |widget_id| { + ComponentWidgetEvent::ActionClick { + widget_id, + id: focused_item_id.clone(), + } + }, + || ComponentWidgetEvent::Noop, + ) + } + } + } +} + +pub fn render_root<'a, T: 'a + Clone>( + show_action_panel: bool, + top_panel: Element<'a, T>, + top_separator: Element<'a, T>, + toast_text: Option<&str>, + content: Element<'a, T>, + primary_action: Option<(String, UiWidgetId, PhysicalShortcut)>, + action_panel: Option, + action_panel_scroll_handle: Option<&ScrollHandle>, + entrypoint_name: &str, + on_panel_toggle_click: impl Fn() -> T, + on_panel_primary_click: impl Fn(UiWidgetId) -> T, + on_action_click: impl Fn(UiWidgetId) -> T, + noop_msg: impl Fn() -> T, +) -> Element<'a, T> { + let entrypoint_name: Element<_> = text(entrypoint_name.to_string()).shaping(Shaping::Advanced).into(); + + let panel_height = 16 + 8 + 2; // TODO get value from theme + + let primary_action = match primary_action { + Some((label, widget_id, shortcut)) => { + let label: Element<_> = text(label) + .shaping(Shaping::Advanced) + .themed(TextStyle::RootBottomPanelPrimaryActionText); + + let label: Element<_> = container(label).themed(ContainerStyle::RootBottomPanelPrimaryActionText); + + let shortcut = render_shortcut(&shortcut); + + let content: Element<_> = row(vec![label, shortcut]).into(); + + let content: Element<_> = button(content) + .on_press(on_panel_primary_click(widget_id)) + .themed(ButtonStyle::RootBottomPanelPrimaryActionButton); + + let content: Element<_> = container(content).themed(ContainerStyle::RootBottomPanelPrimaryActionButton); + + Some(content) + } + None => None, + }; + + let (hide_action_panel, action_panel, bottom_panel) = match action_panel { + Some(action_panel) => { + let actions_text: Element<_> = text("Actions").themed(TextStyle::RootBottomPanelActionToggleText); + + let actions_text: Element<_> = + container(actions_text).themed(ContainerStyle::RootBottomPanelActionToggleText); + + let shortcut = render_shortcut(&PhysicalShortcut { + physical_key: PhysicalKey::KeyK, + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + }); + + let mut bottom_panel_content = vec![entrypoint_name]; + + if let Some(toast_text) = toast_text { + let toast_text = text(toast_text.to_string()).into(); + + bottom_panel_content.push(toast_text); + } + + let space = horizontal_space().into(); + + bottom_panel_content.push(space); + + if let Some(primary_action) = primary_action { + bottom_panel_content.push(primary_action); + + let rule: Element<_> = vertical_rule(1).class(RuleStyle::PrimaryActionSeparator).into(); + + let rule: Element<_> = container(rule) + .width(Length::Shrink) + .height(panel_height) + .max_height(panel_height) + .into(); + + bottom_panel_content.push(rule); + } + + let action_panel_toggle_content: Element<_> = row(vec![actions_text, shortcut]).into(); + + let action_panel_toggle: Element<_> = button(action_panel_toggle_content) + .on_press(on_panel_toggle_click()) + .themed(ButtonStyle::RootBottomPanelActionToggleButton); + + bottom_panel_content.push(action_panel_toggle); + + let bottom_panel: Element<_> = row(bottom_panel_content) + .align_y(Alignment::Center) + .themed(RowStyle::RootBottomPanel); + + (!show_action_panel, Some(action_panel), bottom_panel) + } + None => { + let space: Element<_> = Space::new(Length::Fill, panel_height).into(); + + let mut bottom_panel_content = vec![]; + + if let Some(toast_text) = toast_text { + let toast_text = text(toast_text.to_string()).into(); + + bottom_panel_content.push(toast_text); + } else { + bottom_panel_content.push(entrypoint_name); + } + + bottom_panel_content.push(space); + + if let Some(primary_action) = primary_action { + bottom_panel_content.push(primary_action); + } + + let bottom_panel: Element<_> = row(bottom_panel_content) + .align_y(Alignment::Center) + .themed(RowStyle::RootBottomPanel); + + (true, None, bottom_panel) + } + }; + + let bottom_panel: Element<_> = container(bottom_panel) + .width(Length::Fill) + .themed(ContainerStyle::RootBottomPanel); + + let content: Element<_> = container(content) + .width(Length::Fill) + .height(Length::Fill) + .themed(ContainerStyle::RootInner); + + let content: Element<_> = column(vec![top_panel, top_separator, content, bottom_panel]).into(); + + let content: Element<_> = mouse_area(content) + .on_press( + if hide_action_panel { + noop_msg() + } else { + on_panel_toggle_click() + }, + ) + .into(); + + let mut content = vec![content]; + + if let (Some(action_panel), Some(action_panel_scroll_handle)) = (action_panel, action_panel_scroll_handle) { + if !hide_action_panel { + let action_panel = render_action_panel(action_panel, on_action_click, action_panel_scroll_handle); + + let action_panel: Element<_> = container(action_panel) + .padding(gauntlet_common_ui::padding(0.0, 8.0, 48.0, 0.0)) + .align_right(Length::Fill) + .align_bottom(Length::Fill) + .into(); + + content.push(action_panel); + } + }; + + stack(content).into() +} diff --git a/rust/client/src/ui/widget/search_bar.rs b/rust/client/src/ui/widget/search_bar.rs new file mode 100644 index 0000000..95543ef --- /dev/null +++ b/rust/client/src/ui/widget/search_bar.rs @@ -0,0 +1,25 @@ +use gauntlet_common::model::SearchBarWidget; +use iced::widget::text_input; + +use crate::ui::theme::text_input::TextInputStyle; +use crate::ui::theme::Element; +use crate::ui::theme::ThemableWidget; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::events::ComponentWidgetEvent; +use crate::ui::widget::state::TextFieldState; + +impl<'b> ComponentWidgets<'b> { + pub fn render_search_bar_widget<'a>(&self, widget: &SearchBarWidget) -> Element<'a, ComponentWidgetEvent> { + let widget_id = widget.__id__; + let TextFieldState { + state_value, + text_input_id, + } = self.text_field_state(widget_id); + + text_input(widget.placeholder.as_deref().unwrap_or_default(), state_value) + .id(text_input_id.clone()) + .ignore_with_modifiers(true) + .on_input(move |value| ComponentWidgetEvent::OnChangeSearchBar { widget_id, value }) + .themed(TextInputStyle::PluginSearchBar) + } +} diff --git a/rust/client/src/ui/widget/state.rs b/rust/client/src/ui/widget/state.rs new file mode 100644 index 0000000..b3659cb --- /dev/null +++ b/rust/client/src/ui/widget/state.rs @@ -0,0 +1,206 @@ +use std::collections::HashMap; + +use gauntlet_common::model::FormWidgetOrderedMembers; +use gauntlet_common::model::GridSectionWidgetOrderedMembers; +use gauntlet_common::model::GridWidgetOrderedMembers; +use gauntlet_common::model::RootWidget; +use gauntlet_common::model::RootWidgetMembers; +use gauntlet_common::model::UiWidgetId; +use iced::widget::text_input; +use iced_aw::date_picker::Date; + +use crate::ui::scroll_handle::ScrollHandle; +use crate::ui::scroll_handle::ESTIMATED_MAIN_LIST_ITEM_HEIGHT; +use crate::ui::widget::grid::grid_width; + +pub fn create_state(root_widget: &RootWidget) -> HashMap { + let mut result = HashMap::new(); + + match &root_widget.content { + None => {} + Some(members) => { + match members { + RootWidgetMembers::Detail(widget) => { + result.insert(widget.__id__, ComponentWidgetState::root(0.0, 0)); + } + RootWidgetMembers::Form(widget) => { + result.insert(widget.__id__, ComponentWidgetState::root(0.0, 0)); + + for members in &widget.content.ordered_members { + match members { + FormWidgetOrderedMembers::TextField(widget) => { + result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); + } + FormWidgetOrderedMembers::PasswordField(widget) => { + result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); + } + FormWidgetOrderedMembers::Checkbox(widget) => { + result.insert(widget.__id__, ComponentWidgetState::checkbox(&widget.value)); + } + FormWidgetOrderedMembers::DatePicker(widget) => { + result.insert(widget.__id__, ComponentWidgetState::date_picker(&widget.value)); + } + FormWidgetOrderedMembers::Select(widget) => { + result.insert(widget.__id__, ComponentWidgetState::select(&widget.value)); + } + FormWidgetOrderedMembers::Separator(_) => {} + } + } + } + RootWidgetMembers::List(widget) => { + result.insert( + widget.__id__, + ComponentWidgetState::root(ESTIMATED_MAIN_LIST_ITEM_HEIGHT, 7), + ); + + if let Some(widget) = &widget.content.search_bar { + result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); + } + } + RootWidgetMembers::Grid(widget) => { + // cursed heuristic + let has_title = widget + .content + .ordered_members + .iter() + .flat_map(|members| { + match members { + GridWidgetOrderedMembers::GridItem(widget) => vec![widget], + GridWidgetOrderedMembers::GridSection(widget) => { + widget + .content + .ordered_members + .iter() + .map(|members| { + match members { + GridSectionWidgetOrderedMembers::GridItem(widget) => widget, + } + }) + .collect() + } + } + }) + .next() + .map(|widget| widget.title.is_some() || widget.subtitle.is_some()) + .unwrap_or_default(); + + let (height, rows_per_view) = match grid_width(&widget.columns) { + ..4 => (150.0, 0), + 4 => (150.0, 0), + 5 => (130.0, 0), + 6 => (110.0, 1), + 7 => (90.0, 3), + 8 => (if has_title { 50.0 } else { 50.0 }, if has_title { 3 } else { 4 }), + 8.. => (50.0, 4), + }; + + result.insert(widget.__id__, ComponentWidgetState::root(height, rows_per_view)); + + if let Some(widget) = &widget.content.search_bar { + result.insert(widget.__id__, ComponentWidgetState::text_field(&widget.value)); + } + } + RootWidgetMembers::Inline(_) => {} + } + } + } + + result +} + +#[derive(Debug, Clone)] +pub enum ComponentWidgetState { + TextField(TextFieldState), + Checkbox(CheckboxState), + DatePicker(DatePickerState), + Select(SelectState), + Root(RootState), +} + +#[derive(Debug, Clone)] +pub struct TextFieldState { + pub text_input_id: text_input::Id, + pub state_value: String, +} + +#[derive(Debug, Clone)] +pub struct CheckboxState { + pub state_value: bool, +} + +#[derive(Debug, Clone)] +pub struct DatePickerState { + pub show_picker: bool, + pub state_value: Date, +} + +#[derive(Debug, Clone)] +pub struct SelectState { + pub state_value: Option, +} + +#[derive(Debug, Clone)] +pub struct RootState { + pub show_action_panel: bool, + pub focused_item: ScrollHandle, +} + +impl ComponentWidgetState { + fn root(item_height: f32, rows_per_view: usize) -> ComponentWidgetState { + ComponentWidgetState::Root(RootState { + show_action_panel: false, + focused_item: ScrollHandle::new(false, item_height, rows_per_view), + }) + } + + fn text_field(value: &Option) -> ComponentWidgetState { + ComponentWidgetState::TextField(TextFieldState { + text_input_id: text_input::Id::unique(), + state_value: value.to_owned().unwrap_or_default(), + }) + } + + fn checkbox(value: &Option) -> ComponentWidgetState { + ComponentWidgetState::Checkbox(CheckboxState { + state_value: value.to_owned().unwrap_or(false), + }) + } + + fn date_picker(value: &Option) -> ComponentWidgetState { + let value = value + .to_owned() + .map(|value| parse_date(&value)) + .flatten() + .map(|(year, month, day)| Date::from_ymd(year, month, day)) + .unwrap_or(Date::today()); + + ComponentWidgetState::DatePicker(DatePickerState { + state_value: value, + show_picker: false, + }) + } + + fn select(value: &Option) -> ComponentWidgetState { + ComponentWidgetState::Select(SelectState { + state_value: value.to_owned(), + }) + } +} + +fn parse_date(value: &str) -> Option<(i32, u32, u32)> { + let ymd: Vec<_> = value.split("-").collect(); + + match ymd[..] { + [year, month, day] => { + let year = year.parse::(); + let month = month.parse::(); + let day = day.parse::(); + + match (year, month, day) { + (Ok(year), Ok(month), Ok(day)) => Some((year, month, day)), + _ => None, + } + } + _ => None, + } +} diff --git a/rust/client/src/ui/widget/text.rs b/rust/client/src/ui/widget/text.rs new file mode 100644 index 0000000..d47734c --- /dev/null +++ b/rust/client/src/ui/widget/text.rs @@ -0,0 +1,44 @@ +use iced::advanced::text::Shaping; +use iced::font::Weight; +use iced::widget::text; +use iced::Font; + +use crate::ui::theme::Element; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::events::ComponentWidgetEvent; + +#[derive(Debug, Clone)] +pub enum TextRenderType { + None, + H1, + H2, + H3, + H4, + H5, + H6, +} + +impl<'b> ComponentWidgets<'b> { + pub fn render_text<'a>(&self, value: &[String], context: TextRenderType) -> Element<'a, ComponentWidgetEvent> { + let header = match context { + TextRenderType::None => None, + TextRenderType::H1 => Some(34), + TextRenderType::H2 => Some(30), + TextRenderType::H3 => Some(24), + TextRenderType::H4 => Some(20), + TextRenderType::H5 => Some(18), + TextRenderType::H6 => Some(16), + }; + + let mut text = text(value.join("")).shaping(Shaping::Advanced); + + if let Some(size) = header { + text = text.size(size).font(Font { + weight: Weight::Bold, + ..Font::DEFAULT + }) + } + + text.into() + } +} diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 401bbff..3aa92f0 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -15,12 +15,12 @@ use iced::Task; use crate::model::UiViewEvent; use crate::ui::state::PluginViewState; use crate::ui::theme::Element; -use crate::ui::widget::create_state; -use crate::ui::widget::ActionPanel; -use crate::ui::widget::ComponentWidgetEvent; -use crate::ui::widget::ComponentWidgetState; -use crate::ui::widget::ComponentWidgets; -use crate::ui::widget::ComponentWidgetsMut; +use crate::ui::widget::action_panel::ActionPanel; +use crate::ui::widget::data::ComponentWidgets; +use crate::ui::widget::data_mut::ComponentWidgetsMut; +use crate::ui::widget::events::ComponentWidgetEvent; +use crate::ui::widget::state::create_state; +use crate::ui::widget::state::ComponentWidgetState; use crate::ui::AppMsg; pub struct PluginWidgetContainer { From 652af985446cbcefa7f5bfad95f807cd64424641 Mon Sep 17 00:00:00 2001 From: Benno <107504783+BennoCrafter@users.noreply.github.com> Date: Sat, 15 Feb 2025 20:09:05 +0100 Subject: [PATCH 353/540] Localized application names on macOS (#50) Co-authored-by: Exidex <16986685+exidex@users.noreply.github.com> --- CHANGELOG.md | 6 + Cargo.lock | 1 + bundled_plugins/gauntlet/gauntlet.toml | 13 ++- bundled_plugins/gauntlet/src/applications.tsx | 28 +++-- js/core/src/internal-macos.ts | 1 + js/typings/index.d.ts | 16 +-- rust/plugin_runtime/Cargo.toml | 1 + rust/plugin_runtime/src/deno.rs | 1 + .../src/plugins/applications.rs | 30 ++++- .../src/plugins/applications/macos.rs | 107 ++++++++++++++---- 10 files changed, 159 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c05c504..736ad62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Added localization support for macOS application names + - Added plugin preference `Bundle Name Lang` of enum type + - `localized` option - use localized name of bundle if available - this is the default + - `default` option - use default name of bundle (usually english) +- On macOS use app stem name as a fallback if the bundle name is empty + - Fixes empty names of some apps, like "Creality Print" which have an empty bundle name - Added --version flag to CLI to display Gauntlet version - Slightly improved close-on-unfocus behaviour of main window on X11 - Global shortcut is now executed on key press, instead of key release diff --git a/Cargo.lock b/Cargo.lock index d59ade2..976ed4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4109,6 +4109,7 @@ dependencies = [ "resvg", "serde", "smithay-client-toolkit", + "sys-locale", "tokio", "tokio-util", "tracing", diff --git a/bundled_plugins/gauntlet/gauntlet.toml b/bundled_plugins/gauntlet/gauntlet.toml index 371d915..28bc064 100644 --- a/bundled_plugins/gauntlet/gauntlet.toml +++ b/bundled_plugins/gauntlet/gauntlet.toml @@ -16,6 +16,17 @@ type = 'bool' default = true description = "Enables experimental window tracking" +[[entrypoint.preferences]] +id = "bundleNameLang" +name = "Bundle Name Lang" +type = "enum" +default = "localized" +description = "Language of the bundle name" +enum_values = [ + { label = 'Default', value = 'default' }, + { label = 'Localized', value = 'localized' }, +] + [[entrypoint]] id = 'windows' name = 'All Open Windows' @@ -46,7 +57,7 @@ read = [ # technically only uses locations defined by XDG Desktop Entry Specification, but # the spec allows for customization via XDG_DATA_DIRS and XDG_DATA_HOME env vars so it can be any path "/", - "C:\\" + "C:\\", ] [[supported_system]] diff --git a/bundled_plugins/gauntlet/src/applications.tsx b/bundled_plugins/gauntlet/src/applications.tsx index 199fa5e..f8597e1 100644 --- a/bundled_plugins/gauntlet/src/applications.tsx +++ b/bundled_plugins/gauntlet/src/applications.tsx @@ -7,6 +7,7 @@ import { macos_app_from_arbitrary_path, macos_app_from_path, macos_application_dirs, + macos_get_localized_language, macos_major_version, macos_open_application, macos_open_setting_13_and_post, @@ -20,10 +21,25 @@ import { applicationEventLoopX11, focusX11Window } from "./window/x11"; import { applicationEventLoopWayland, focusWaylandWindow } from "./window/wayland"; import { windows_app_from_path, windows_application_dirs, windows_open_application } from "gauntlet:bridge/internal-windows"; -type EntrypointPreferences = { experimentalWindowTracking: boolean }; +type EntrypointPreferences = { experimentalWindowTracking: boolean, bundleNameLang: "default" | "localized" }; export default async function Applications(context: GeneratorContext): Promise void)> { - const { add, remove, get, getAll, entrypointPreferences: { experimentalWindowTracking } } = context; + const { add, remove, get, getAll, entrypointPreferences: { experimentalWindowTracking, bundleNameLang } } = context; + let lang: string | undefined; + + switch (bundleNameLang) { + case "default": { + lang = undefined; + break; + } + case "localized": { + lang = macos_get_localized_language(); + break; + } + default: { + throw new Error("Unknown bundle name type") + } + } switch (current_os()) { case "linux": { @@ -105,7 +121,7 @@ export default async function Applications(context: GeneratorContext= 13) { - for (const setting of macos_settings_13_and_post()) { + for (const setting of macos_settings_13_and_post(lang)) { add(`settings:${setting.preferences_id}`, { name: setting.name, actions: [ @@ -137,7 +153,7 @@ export default async function Applications(context: GeneratorContext( macos_application_dirs(), - path => macos_app_from_arbitrary_path(path), + path => macos_app_from_arbitrary_path(path, lang), (_id, data) => ({ name: data.name, actions: [ @@ -279,5 +295,3 @@ async function genericGenerator( watcher.close() } } - - diff --git a/js/core/src/internal-macos.ts b/js/core/src/internal-macos.ts index 5c68d70..8bd72e2 100644 --- a/js/core/src/internal-macos.ts +++ b/js/core/src/internal-macos.ts @@ -4,6 +4,7 @@ export { macos_application_dirs, macos_major_version, macos_open_application, + macos_get_localized_language, macos_open_setting_13_and_post, macos_open_setting_pre_13, macos_settings_13_and_post, diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 7ca15d8..4f8652a 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -162,15 +162,16 @@ declare module "gauntlet:bridge/internal-linux" { declare module "gauntlet:bridge/internal-macos" { function macos_major_version(): number function macos_settings_pre_13(): MacOSDesktopSettingsPre13Data[] - function macos_settings_13_and_post(): MacOSDesktopSettings13AndPostData[] + function macos_settings_13_and_post(lang: string | undefined): MacOSDesktopSettings13AndPostData[] function macos_open_setting_13_and_post(preferences_id: String): void function macos_open_setting_pre_13(setting_path: String): void function macos_system_applications(): string[] function macos_application_dirs(): string[] - function macos_app_from_path(path: string): Promise> - function macos_app_from_arbitrary_path(path: string): Promise> + function macos_app_from_path(path: string, lang: string | undefined): Promise> + function macos_app_from_arbitrary_path(path: string, lang: string | undefined): Promise> function macos_open_application(app_path: String): void + function macos_get_localized_language(): string | undefined } declare module "gauntlet:bridge/internal-windows" { @@ -196,15 +197,16 @@ declare module "ext:core/ops" { function macos_major_version(): number function macos_settings_pre_13(): MacOSDesktopSettingsPre13Data[] - function macos_settings_13_and_post(): MacOSDesktopSettings13AndPostData[] + function macos_settings_13_and_post(lang: string | undefined): MacOSDesktopSettings13AndPostData[] function macos_open_setting_13_and_post(preferences_id: String): void function macos_open_setting_pre_13(setting_path: String): void function macos_system_applications(): string[] function macos_application_dirs(): string[] - function macos_app_from_path(path: string): Promise> - function macos_app_from_arbitrary_path(path: string): Promise> + function macos_app_from_path(path: string, lang: string | undefined): Promise> + function macos_app_from_arbitrary_path(path: string, lang: string | undefined): Promise> function macos_open_application(app_path: String): void + function macos_get_localized_language(): string | undefined function windows_application_dirs(): string[] function windows_open_application(path: string): void @@ -479,4 +481,4 @@ type X11ApplicationEventDesktopFileNamePropertyNotify = { type: "DesktopFileNamePropertyNotify", id: X11WindowId, desktop_file_name: string -}; \ No newline at end of file +}; diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml index e7d725c..3347427 100644 --- a/rust/plugin_runtime/Cargo.toml +++ b/rust/plugin_runtime/Cargo.toml @@ -34,6 +34,7 @@ numbat = "1.14.0" which = "7.0.1" uuid = "1.11.0" open = "5" +sys-locale = "0.3.2" [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] libc = "0.2" diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index 259e4f4..ac73602 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -430,6 +430,7 @@ deno_core::extension!( crate::plugins::applications::macos_app_from_arbitrary_path, crate::plugins::applications::macos_app_from_path, crate::plugins::applications::macos_open_application, + crate::plugins::applications::macos_get_localized_language, ], esm_entry_point = "ext:gauntlet/internal-macos/bootstrap.js", esm = [ diff --git a/rust/plugin_runtime/src/plugins/applications.rs b/rust/plugin_runtime/src/plugins/applications.rs index d15e9a5..3dc7818 100644 --- a/rust/plugin_runtime/src/plugins/applications.rs +++ b/rust/plugin_runtime/src/plugins/applications.rs @@ -9,6 +9,7 @@ use image::imageops::FilterType; use image::ImageFormat; use serde::Deserialize; use serde::Serialize; +use sys_locale::get_locale; use tokio::runtime::Handle; use tokio::sync::mpsc::Receiver; use tokio::task::spawn_blocking; @@ -140,15 +141,21 @@ pub fn macos_major_version() -> u8 { #[cfg(target_os = "macos")] #[op2(async)] #[serde] -pub async fn macos_app_from_path(#[string] path: String) -> anyhow::Result> { - Ok(spawn_blocking(|| macos::macos_app_from_path(&PathBuf::from(path))).await?) +pub async fn macos_app_from_path( + #[string] path: String, + #[string] lang: Option, +) -> anyhow::Result> { + Ok(spawn_blocking(|| macos::macos_app_from_path(&PathBuf::from(path), lang)).await?) } #[cfg(target_os = "macos")] #[op2(async)] #[serde] -pub async fn macos_app_from_arbitrary_path(#[string] path: String) -> anyhow::Result> { - Ok(spawn_blocking(|| macos::macos_app_from_arbitrary_path(PathBuf::from(path))).await?) +pub async fn macos_app_from_arbitrary_path( + #[string] path: String, + #[string] lang: Option, +) -> anyhow::Result> { + Ok(spawn_blocking(|| macos::macos_app_from_arbitrary_path(PathBuf::from(path), lang)).await?) } #[cfg(target_os = "macos")] @@ -189,8 +196,8 @@ pub fn macos_settings_pre_13() -> Vec { #[cfg(target_os = "macos")] #[op2] #[serde] -pub fn macos_settings_13_and_post() -> Vec { - macos::macos_settings_13_and_post() +pub fn macos_settings_13_and_post(#[string] lang: Option) -> Vec { + macos::macos_settings_13_and_post(lang) } #[cfg(target_os = "macos")] @@ -209,6 +216,17 @@ pub fn macos_open_setting_pre_13(#[string] setting_path: String) -> anyhow::Resu Ok(()) } +#[cfg(target_os = "macos")] +#[op2] +#[string] +pub fn macos_get_localized_language() -> Option { + get_locale()? + .split("-") + .collect::>() + .get(0) + .map(|s| s.to_string()) +} + #[cfg(unix)] pub fn spawn_detached(path: &str, args: I) -> std::io::Result<()> where diff --git a/rust/plugin_runtime/src/plugins/applications/macos.rs b/rust/plugin_runtime/src/plugins/applications/macos.rs index b0ce706..9db37c3 100644 --- a/rust/plugin_runtime/src/plugins/applications/macos.rs +++ b/rust/plugin_runtime/src/plugins/applications/macos.rs @@ -31,6 +31,7 @@ use objc2_foundation::NSSize; use objc2_foundation::NSString; use objc2_foundation::NSZeroRect; use plist::Dictionary; +use plist::Value; use regex::Regex; use serde::Deserialize; @@ -100,7 +101,7 @@ pub fn macos_application_dirs() -> Vec { all_applications } -pub fn macos_app_from_arbitrary_path(path: PathBuf) -> Option { +pub fn macos_app_from_arbitrary_path(path: PathBuf, lang: Option) -> Option { let path = path .ancestors() .into_iter() @@ -123,29 +124,68 @@ pub fn macos_app_from_arbitrary_path(path: PathBuf) -> Option return None; }; - macos_app_from_path(path) + macos_app_from_path(path, lang) } -pub fn macos_app_from_path(path: &Path) -> Option { - if !path.is_dir() { - return None; - } +fn get_bundle_name(app_path: &Path) -> String { + let info_path = app_path.join("Contents").join("Info.plist"); - let name = path + let info: Option = plist::from_file(info_path).ok(); + + let fallback_name = app_path .file_stem() - .expect(&format!("invalid path: {:?}", path)) + .expect(&format!("invalid path: {:?}", app_path)) .to_str() .expect("non-uft8 paths are not supported") .to_string(); - let info_path = path.join("Contents").join("Info.plist"); - - let info: Option = plist::from_file(info_path).ok(); - - let name = info + let mut bundle_name = info .as_ref() .and_then(|info| info.bundle_display_name.clone().or_else(|| info.bundle_name.clone())) - .unwrap_or(name); + .unwrap_or(fallback_name.clone()); + + if bundle_name.is_empty() { + bundle_name = fallback_name; + } + + bundle_name +} + +fn get_localized_name(path: &Path, preferred_language: &str) -> Option { + let localized_info: Option = plist::from_file(path).ok(); + + if let Some(localized_info) = localized_info { + // get language info. first try to use display name, if not available use name + localized_info + .languages + .get(preferred_language) + .and_then(|localized_info| { + localized_info + .bundle_display_name + .clone() + .or_else(|| localized_info.bundle_name.clone()) + }) + } else { + eprintln!("Error: Could not load plist from path '{}'.", path.display()); + None + } +} + +pub fn macos_app_from_path(path: &Path, lang: Option) -> Option { + if !path.is_dir() { + return None; + } + + let name = lang + .and_then(|l| { + let info_plist_path = path.join("Contents/Resources/InfoPlist.loctable"); + if info_plist_path.is_file() { + get_localized_name(info_plist_path.as_path(), &l) + } else { + None + } + }) + .unwrap_or(get_bundle_name(path)); let icon = get_application_icon(&path) .inspect_err(|err| tracing::error!("error while reading application icon for {:?}: {:?}", path, err)) @@ -198,7 +238,7 @@ pub fn macos_settings_pre_13() -> Vec { all_settings } -pub fn macos_settings_13_and_post() -> Vec { +pub fn macos_settings_13_and_post(lang: Option) -> Vec { let sidebar: Vec = plist::from_file("/System/Applications/System Settings.app/Contents/Resources/Sidebar.plist") .expect("Sidebar.plist doesn't follow expected format"); @@ -218,13 +258,24 @@ pub fn macos_settings_13_and_post() -> Vec { let extensions: HashMap<_, _> = get_extensions_in_dir(PathBuf::from("/System/Library/ExtensionKit/Extensions")) .into_iter() .filter_map(|path| { - fn read_plist(path: &Path) -> anyhow::Result<(String, (String, PathBuf))> { - let name = path + fn read_plist(path: &Path, lang: &Option) -> anyhow::Result<(String, (String, PathBuf))> { + let mut name = path .file_stem() .expect(&format!("invalid path: {:?}", path)) .to_string_lossy() .to_string(); + let localized_info_path = path.join("Contents/Resources/InfoPlist.loctable"); + if !localized_info_path.is_file() { + return Ok((name.clone(), (name, path.to_path_buf()))); + } + + if let Some(lang) = lang { + name = get_localized_name(localized_info_path.as_path(), lang).unwrap_or(name); + } else { + name = get_localized_name(localized_info_path.as_path(), "en").unwrap_or(name); + } + let info_path = path.join("Contents").join("Info.plist"); let info = plist::from_file::<_, Info>(info_path.as_path()).context(format!( @@ -232,16 +283,10 @@ pub fn macos_settings_13_and_post() -> Vec { &info_path.display() ))?; - let name = info - .bundle_display_name - .clone() - .or_else(|| info.bundle_name.clone()) - .unwrap_or(name); - Ok((info.bundle_id, (name, path.to_path_buf()))) } - read_plist(&path) + read_plist(&path, &lang) .inspect_err(|err| { tracing::error!("error while reading system extension Info.plist {:?}: {:?}", path, err) }) @@ -454,6 +499,20 @@ struct Info { bundle_icon_name: Option, } +#[derive(Deserialize)] +struct LocalizedInfo { + #[serde(rename = "CFBundleDisplayName")] + bundle_display_name: Option, + #[serde(rename = "CFBundleName")] + bundle_name: Option, +} + +#[derive(Deserialize)] +struct InfoPlist { + #[serde(flatten)] + languages: HashMap, +} + #[derive(Deserialize)] struct SystemVersion { #[serde(rename = "ProductVersion")] From 047e0c34cae78c3b4da66d6b3957699fe4101c98 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Feb 2025 20:08:18 +0100 Subject: [PATCH 354/540] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 736ad62..b6d71b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,11 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] -- Added localization support for macOS application names +- Added localization support for macOS application names (contributed by @BennoCrafter) - Added plugin preference `Bundle Name Lang` of enum type - `localized` option - use localized name of bundle if available - this is the default - `default` option - use default name of bundle (usually english) -- On macOS use app stem name as a fallback if the bundle name is empty +- On macOS use app stem name as a fallback if the bundle name is empty (contributed by @BennoCrafter) - Fixes empty names of some apps, like "Creality Print" which have an empty bundle name - Added --version flag to CLI to display Gauntlet version - Slightly improved close-on-unfocus behaviour of main window on X11 From f50a9ae51eaf7141f683be71ef995ec94f5dffd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kalinowski?= Date: Sun, 19 Jan 2025 22:55:31 +0100 Subject: [PATCH 355/540] Don't clear search bar if window is closed using global shortcut --- CHANGELOG.md | 1 + rust/client/src/ui/mod.rs | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6d71b1..fcf63d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Pressing global shortcut while window is open now preserves search bar value when window is opened next time (contributed by @Kalin8900) - Added localization support for macOS application names (contributed by @BennoCrafter) - Added plugin preference `Bundle Name Lang` of enum type - `localized` option - use localized name of bundle if available - this is the default diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index fbbda0e..e992f77 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -789,14 +789,14 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { AppMsg::RunCommand { plugin_id, entrypoint_id, - } => Task::batch([state.hide_window(), state.run_command(plugin_id, entrypoint_id)]), + } => Task::batch([state.hide_window(true), state.run_command(plugin_id, entrypoint_id)]), AppMsg::RunGeneratedEntrypoint { plugin_id, entrypoint_id, action_index, } => { Task::batch([ - state.hide_window(), + state.hide_window(true), state.run_generated_entrypoint(plugin_id, entrypoint_id, action_index), ]) } @@ -811,7 +811,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { match render_location { UiRenderLocation::InlineView => { Task::batch([ - state.hide_window(), + state.hide_window(true), Task::done(AppMsg::WidgetEvent { widget_event, plugin_id, @@ -1158,7 +1158,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } AppMsg::ToggleWindow => state.toggle_window(), AppMsg::ShowWindow => state.show_window(), - AppMsg::HideWindow => state.hide_window(), + AppMsg::HideWindow => state.hide_window(true), AppMsg::ShowPreferenceRequiredView { plugin_id, entrypoint_id, @@ -2254,7 +2254,7 @@ impl AppModel { fn on_unfocused(&mut self) -> Task { // for some reason (on both macOS and linux x11 but x11 now uses separate impl) duplicate Unfocused fires right before Focus event if self.focused { - self.hide_window() + self.hide_window(true) } else { Task::none() } @@ -2262,13 +2262,13 @@ impl AppModel { fn toggle_window(&mut self) -> Task { if self.opened { - self.hide_window() + self.hide_window(false) } else { self.show_window() } } - fn hide_window(&mut self) -> Task { + fn hide_window(&mut self, reset_state: bool) -> Task { if !self.opened { return Task::none(); } @@ -2276,7 +2276,11 @@ impl AppModel { self.focused = false; self.opened = false; - let mut commands = vec![self.reset_window_state()]; + let mut commands = vec![]; + + if reset_state { + commands.push(self.reset_window_state()); + } #[cfg(target_os = "linux")] if self.wayland { From c18a4050d085c68f74a9d326c7b4a818800753fc Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Feb 2025 20:43:44 +0100 Subject: [PATCH 356/540] Fix typo in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcf63d8..6a06c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git - Added shortcut to open Settings UI (contributed by @BennoCrafter) - Ctrl + , on Windows and Linux - Cmd + , on macOS -- Global shortcut how hides the main window if it is already open (contributed by @BennoCrafter) +- Global shortcut now hides the main window if it is already open (contributed by @BennoCrafter) - It is now possible to run commands and open views using CLI command - Format: `gauntlet run ` - Plugin ID can be found in Settings UI From 3b886b0998af1a679e137f0bfbec189a1faeb1af Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 15 Feb 2025 20:47:32 +0100 Subject: [PATCH 357/540] Add screenshot to README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 743aae7..9f1eedc 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ Web-first cross-platform application launcher with React-based plugins > > There will probably be breaking changes which will be documented in [changelog](CHANGELOG.md). +![image](https://github.com/user-attachments/assets/81339462-9cc3-469e-8cdc-ca74918bceab) + +## Demo + https://github.com/user-attachments/assets/19964ed6-9cd9-48d4-9835-6be04de14b66 ## Features From dc8f8d96ebd014bf04a0e1f04bf4498caaf7351e Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 16 Feb 2025 11:12:59 +0100 Subject: [PATCH 358/540] Fix build on linux --- rust/client/src/ui/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index e992f77..ee689da 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -1118,7 +1118,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { #[cfg(target_os = "linux")] if state.wayland { - state.hide_window() + state.hide_window(true) } else { // x11 uses separate mechanism based on _NET_ACTIVE_WINDOW property Task::none() From e49e5a391111da24e90e9add2a6d9fb6b4d0388c Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 16 Feb 2025 12:00:51 +0100 Subject: [PATCH 359/540] Fix database becoming corrupted after changing theme setting --- rust/server/src/plugins/data_db_repository.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index e7ab6c3..2795cba 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -1025,11 +1025,28 @@ impl DataDbRepository { INSERT INTO settings_data (id, global_shortcut, settings) VALUES(?1, ?2, ?3) ON CONFLICT (id) - DO UPDATE SET settings = ?2 + DO UPDATE SET settings = ?3 "#; + let data = sqlx::query_as::<_, DbSettingsDataContainer>("SELECT * FROM settings_data") + .fetch_optional(&self.pool) + .await? + .unwrap_or(DbSettingsDataContainer { + global_shortcut: DbSettingsGlobalShortcutData { + physical_key: "".to_string(), + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: false, + unset: true, + error: None, + }, + settings: None, + }); + sqlx::query(sql) .bind(SETTINGS_DATA_ID) + .bind(Json(data.global_shortcut)) .bind(Json(value)) .execute(&self.pool) .await?; From 435f836eb29e134dae35605cb11336bf946c4aeb Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Feb 2025 10:45:42 +0100 Subject: [PATCH 360/540] Add support for x86_64 on macOS --- .github/workflows/setup-macos.yaml | 6 ++- CHANGELOG.md | 2 + js/build/src/main.ts | 71 +++++++++++++++++++++--------- 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/.github/workflows/setup-macos.yaml b/.github/workflows/setup-macos.yaml index 09eb52e..2dc8c85 100644 --- a/.github/workflows/setup-macos.yaml +++ b/.github/workflows/setup-macos.yaml @@ -35,6 +35,8 @@ jobs: uses: Homebrew/actions/setup-homebrew@master - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin,x86_64-apple-darwin - run: brew install protobuf - run: brew install create-dmg @@ -59,7 +61,7 @@ jobs: - uses: actions/upload-artifact@v4 if: ${{ inputs.upload-artifact }} with: - name: 'gauntlet-aarch64-macos.dmg' - path: 'target/aarch64-apple-darwin/release/gauntlet-aarch64-macos.dmg' + name: 'gauntlet-universal-macos.dmg' + path: 'target/out/gauntlet-universal-macos.dmg' if-no-files-found: 'error' retention-days: 7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a06c57..eeaf671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Published macOS `.dmg` file is now universal and can run on x86_64 cpu architecture + - **BREAKING CHANGE**: Name of published macOS `.dmg` file was changed from `gauntlet-aarch64-macos.dmg` to `gauntlet-universal-macos.dmg` - Pressing global shortcut while window is open now preserves search bar value when window is opened next time (contributed by @Kalin8900) - Added localization support for macOS application names (contributed by @BennoCrafter) - Added plugin preference `Bundle Name Lang` of enum type diff --git a/js/build/src/main.ts b/js/build/src/main.ts index 3b5311d..ab37a4d 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -118,22 +118,42 @@ async function doPublishMacOS() { console.log("git pull...") await git.pull() - const arch = 'aarch64-apple-darwin'; + const archArm = 'aarch64-apple-darwin'; + const archIntel = 'x86_64-apple-darwin'; const profile = 'release-size'; - build(projectRoot, arch, profile) - const { fileName, filePath } = await packageForMacos(projectRoot, arch, profile, true, true) + buildJs(projectRoot) + buildRust(projectRoot, archArm, profile) + buildRust(projectRoot, archIntel, profile) + + const { fileName, filePath } = await packageForMacos( + projectRoot, + [archArm, archIntel], + profile, + true, + true + ) await addFileToRelease(filePath, fileName) } async function doBuildMacOS() { const projectRoot = getProjectRoot(); - const arch = 'aarch64-apple-darwin'; + const archArm = 'aarch64-apple-darwin'; + const archIntel = 'x86_64-apple-darwin'; const profile = 'release'; - await doBuild(projectRoot, arch, profile) - await packageForMacos(projectRoot, arch, profile, false, false) + buildJs(projectRoot) + buildRust(projectRoot, archArm, profile) + buildRust(projectRoot, archIntel, profile) + + await packageForMacos( + projectRoot, + [archArm, archIntel], + profile, + false, + false + ) } async function doPublishWindows() { @@ -321,11 +341,11 @@ function packageForLinux(projectRoot: string, arch: string, profile: string): { } } -async function packageForMacos(projectRoot: string, arch: string, profile: string, sign: boolean, notarize: boolean): Promise<{ filePath: string; fileName: string }> { - const releaseDirPath = path.join(projectRoot, 'target', arch, profile); - const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet'); - const outFileName = "gauntlet-aarch64-macos.dmg" - const outFilePath = path.join(releaseDirPath, outFileName); +async function packageForMacos(projectRoot: string, arch: string[], profile: string, sign: boolean, notarize: boolean): Promise<{ filePath: string; fileName: string }> { + const targetDirPath = path.join(projectRoot, 'target'); + const outDirPath = path.join(targetDirPath, 'out'); + const outFileName = "gauntlet-universal-macos.dmg" + const outFilePath = path.join(targetDirPath, outFileName); const assetsDirPath = path.join(projectRoot, 'assets', 'macos'); const sourceInfoFilePath = path.join(assetsDirPath, 'Info.plist'); @@ -333,7 +353,7 @@ async function packageForMacos(projectRoot: string, arch: string, profile: strin const dmgBackground = path.join(assetsDirPath, 'dmg-background.png'); const entitlementsPath = path.join(assetsDirPath, 'entitlements.plist'); - const bundleDir = path.join(releaseDirPath, 'Gauntlet.app'); + const bundleDir = path.join(outDirPath, 'Gauntlet.app'); const contentsDir = path.join(bundleDir, 'Contents'); const macosContentsDir = path.join(contentsDir, 'MacOS'); const resourcesContentsDir = path.join(contentsDir, 'Resources'); @@ -341,14 +361,25 @@ async function packageForMacos(projectRoot: string, arch: string, profile: strin const targetInfoFilePath = path.join(contentsDir, 'Info.plist'); const targetIconFilePath = path.join(resourcesContentsDir, 'AppIcon.icns'); + const sourceExecutableFilePaths = arch.map(arch => path.join(targetDirPath, arch, profile, 'gauntlet')); + const version = await readVersion(projectRoot) + mkdirSync(outDirPath) mkdirSync(bundleDir) mkdirSync(contentsDir) mkdirSync(macosContentsDir) mkdirSync(resourcesContentsDir) - copyFileSync(sourceExecutableFilePath, targetExecutableFilePath) + spawnWithErrors(`lipo`, [ + ...sourceExecutableFilePaths, + '-create', + '-output', + targetExecutableFilePath + ], { + cwd: outDirPath + }) + copyFileSync(sourceInfoFilePath, targetInfoFilePath) copyFileSync(sourceIconFilePath, targetIconFilePath) @@ -356,9 +387,9 @@ async function packageForMacos(projectRoot: string, arch: string, profile: strin const infoResult = infoSource.replace('__VERSION__', `${version}.0.0`); writeFileSync(targetInfoFilePath, infoResult,'utf8'); - const signKeyPath = path.join(releaseDirPath, 'signKey.pem'); - const signCertPath = path.join(releaseDirPath, 'signCert.pem'); - const connectApiKeyPath = path.join(releaseDirPath, 'connectApiKey.json'); + const signKeyPath = path.join(outDirPath, 'signKey.pem'); + const signCertPath = path.join(outDirPath, 'signCert.pem'); + const connectApiKeyPath = path.join(outDirPath, 'connectApiKey.json'); const signKeyContent = process.env.APPLE_SIGNING_KEY_PEM; const signCertContent = process.env.APPLE_SIGNING_CERT_PEM; @@ -382,7 +413,7 @@ async function packageForMacos(projectRoot: string, arch: string, profile: strin entitlementsPath, bundleDir ], { - cwd: releaseDirPath + cwd: outDirPath }) } @@ -397,7 +428,7 @@ async function packageForMacos(projectRoot: string, arch: string, profile: strin outFileName, bundleDir ], { - cwd: releaseDirPath + cwd: outDirPath }) if (sign) { @@ -410,7 +441,7 @@ async function packageForMacos(projectRoot: string, arch: string, profile: strin '--for-notarization', outFilePath ], { - cwd: releaseDirPath + cwd: outDirPath }) } @@ -424,7 +455,7 @@ async function packageForMacos(projectRoot: string, arch: string, profile: strin '--staple', outFilePath ], { - cwd: releaseDirPath + cwd: outDirPath }) } From 0e26ac0ba48c48905778e2b911be088cf0ef3c03 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 16 Feb 2025 12:33:59 +0100 Subject: [PATCH 361/540] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eeaf671..8d1aa95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Fixed database becoming corrupted after changing theme setting - Published macOS `.dmg` file is now universal and can run on x86_64 cpu architecture - **BREAKING CHANGE**: Name of published macOS `.dmg` file was changed from `gauntlet-aarch64-macos.dmg` to `gauntlet-universal-macos.dmg` - Pressing global shortcut while window is open now preserves search bar value when window is opened next time (contributed by @Kalin8900) From 4d49952ea7bade75eece12f128b041ce4a5300f4 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 22 Feb 2025 14:42:42 +0100 Subject: [PATCH 362/540] Update CHANGELOG.md --- CHANGELOG.md | 50 +++++++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d1aa95..7d07625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,42 +9,50 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] -- Fixed database becoming corrupted after changing theme setting -- Published macOS `.dmg` file is now universal and can run on x86_64 cpu architecture +### General +- Published macOS `.dmg` file is now universal and can run on `x86_64` CPU architecture - **BREAKING CHANGE**: Name of published macOS `.dmg` file was changed from `gauntlet-aarch64-macos.dmg` to `gauntlet-universal-macos.dmg` -- Pressing global shortcut while window is open now preserves search bar value when window is opened next time (contributed by @Kalin8900) -- Added localization support for macOS application names (contributed by @BennoCrafter) - - Added plugin preference `Bundle Name Lang` of enum type - - `localized` option - use localized name of bundle if available - this is the default - - `default` option - use default name of bundle (usually english) -- On macOS use app stem name as a fallback if the bundle name is empty (contributed by @BennoCrafter) - - Fixes empty names of some apps, like "Creality Print" which have an empty bundle name -- Added --version flag to CLI to display Gauntlet version -- Slightly improved close-on-unfocus behaviour of main window on X11 -- Global shortcut is now executed on key press, instead of key release -- When using active screen setting for window positioning, position calculated is now relative to size of the screen. Fixes unexpected position when using monitors of different size (contributed by @BennoCrafter) -- Added shortcut to open Settings UI (contributed by @BennoCrafter) - - Ctrl + , on Windows and Linux - - Cmd + , on macOS +- Pressing global shortcut while window is open now preserves search bar value when window is opened next time (contributed by @Kalin8900) - Global shortcut now hides the main window if it is already open (contributed by @BennoCrafter) +- Global shortcut is now executed on key press, instead of key release - It is now possible to run commands and open views using CLI command - Format: `gauntlet run ` - Plugin ID can be found in Settings UI - Entrypoint ID can be found in: - For entrypoint types `command` and `view` - in Plugin Manifest or in Settings UI TODO - - For entrypoint type `entrypoint-generator` - in Settings UI TODO + - For entrypoint type `entrypoint-generator` - in Settings UI TODO - Action ID can also be found in Plugin Manifest - Action ID option also accepts special values - `:primary` - to run primary action of the entrypoint - `:secondary` - to run secondary action of the entrypoint -- Slightly improved --help documentation of CLI command +- Slightly improved `--help` documentation of CLI command +- Added `--version` flag to CLI to display Gauntlet version +- Added localization support for macOS application names (contributed by @BennoCrafter) + - Added plugin preference `Bundle Name Lang` of enum type + - `localized` option - use localized name of bundle if available - this is the default + - `default` option - use default name of bundle (usually english) +- Added shortcut to open Settings UI (contributed by @BennoCrafter) + - Ctrl + , on Windows and Linux + - Cmd + , on macOS +### UI/UX Improvements - In main window search result, moved plugin name next to entrypoint name - In main window search result, displayed type of entrypoint in place of plugin name, use generator entrypoint name if generated -- Fix no plugins starting on Windows in release mode -- Fix all global shortcut registrations failing if one shortcut registration failed -- Fix error when registering shortcut erroring whole settings window instead of adding an icon +### Fixes +- Fixed database becoming corrupted after changing theme setting, preventing application server startup + - If you encounter this issue you have to remove application database, file which can be found at + - Linux: `/home/alice/.local/share/gauntlet/data.db` + - macOS: `/Users/Alice/Library/Application Support/dev.project-gauntlet.Gauntlet/data.db` + - Windows: `C:\Users\Alice\AppData\Roaming\project-gauntlet\Gauntlet\data\data.db` +- Slightly improved close-on-unfocus behaviour of main window on X11 +- On macOS use app stem name as a fallback if the bundle name is empty (contributed by @BennoCrafter) + - Fixes empty names of some apps, like "Creality Print" which have an empty bundle name +- When using active screen setting for window positioning, position calculated is now relative to size of the screen + - Fixes unexpected position when using monitors of different size (contributed by @BennoCrafter) +- Fixed no plugins starting on Windows in release mode +- Fixed all global shortcut registrations failing if one shortcut registration failed +- Fixed error when registering shortcut erroring whole settings window instead of adding an icon ## [14] - 2025-01-19 From b7f178666f60c93a9d8d09ab980d7332b5340085 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 22 Feb 2025 14:51:49 +0100 Subject: [PATCH 363/540] Remove Chocolatey install option --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9f1eedc..aac5d23 100644 --- a/README.md +++ b/README.md @@ -164,14 +164,9 @@ To start, manually open application. #### Windows -Although it is possible to install Gauntlet by using `.msi` directly, application doesn't have auto-update functionality so it is recommended to install using `chocolatey` package manager. +Download `.msi` at [Releases page](https://github.com/project-gauntlet/gauntlet/releases/latest) and open to install Gauntlet -Chocolatey package: [link](https://community.chocolatey.org/packages/gauntlet) - -To install run: -``` -choco install gauntlet -``` +Note: application doesn't have auto-update functionality, and has to be updated manually To start, manually open application. From a0b4dbe8e972b8d4c027719e6ae8fc5012e7ac45 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:52:58 +0000 Subject: [PATCH 364/540] Prepare for v15 release --- CHANGELOG.md | 2 ++ VERSION | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d07625..361c055 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +## [15] - 2025-02-22 + ### General - Published macOS `.dmg` file is now universal and can run on `x86_64` CPU architecture - **BREAKING CHANGE**: Name of published macOS `.dmg` file was changed from `gauntlet-aarch64-macos.dmg` to `gauntlet-universal-macos.dmg` diff --git a/VERSION b/VERSION index da2d398..3f10ffe 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -14 \ No newline at end of file +15 \ No newline at end of file From b9f4d98408ea9f7e505710fb855a73504b348a16 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 22 Feb 2025 18:57:15 +0100 Subject: [PATCH 365/540] Try to fix macos release --- CHANGELOG.md | 6 +----- js/build/src/main.ts | 6 ++++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 361c055..a638e56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,11 +19,7 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git - Global shortcut is now executed on key press, instead of key release - It is now possible to run commands and open views using CLI command - Format: `gauntlet run ` - - Plugin ID can be found in Settings UI - - Entrypoint ID can be found in: - - For entrypoint types `command` and `view` - in Plugin Manifest or in Settings UI TODO - - For entrypoint type `entrypoint-generator` - in Settings UI TODO - - Action ID can also be found in Plugin Manifest + - Action ID can be found in Plugin Manifest - Action ID option also accepts special values - `:primary` - to run primary action of the entrypoint - `:secondary` - to run secondary action of the entrypoint diff --git a/js/build/src/main.ts b/js/build/src/main.ts index ab37a4d..dac94ac 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -6,7 +6,7 @@ import { Octokit } from 'octokit'; import { sync as spawnSync } from "cross-spawn"; import path from "node:path"; import { mkdirSync, readFileSync } from "fs"; -import { copyFileSync, writeFileSync } from "node:fs"; +import { copyFileSync, rmdirSync, writeFileSync } from "node:fs"; import * as core from '@actions/core'; import { SpawnSyncOptions } from "child_process"; @@ -365,6 +365,8 @@ async function packageForMacos(projectRoot: string, arch: string[], profile: str const version = await readVersion(projectRoot) + rmdirSync(outDirPath) + mkdirSync(outDirPath) mkdirSync(bundleDir) mkdirSync(contentsDir) @@ -428,7 +430,7 @@ async function packageForMacos(projectRoot: string, arch: string[], profile: str outFileName, bundleDir ], { - cwd: outDirPath + cwd: targetDirPath }) if (sign) { From fe707d84195eaa9f43ac57c533a493b8c470c8ab Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 22 Feb 2025 19:35:07 +0100 Subject: [PATCH 366/540] Try to fix macos release x2 --- js/build/src/main.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/js/build/src/main.ts b/js/build/src/main.ts index dac94ac..87c0a32 100644 --- a/js/build/src/main.ts +++ b/js/build/src/main.ts @@ -6,7 +6,7 @@ import { Octokit } from 'octokit'; import { sync as spawnSync } from "cross-spawn"; import path from "node:path"; import { mkdirSync, readFileSync } from "fs"; -import { copyFileSync, rmdirSync, writeFileSync } from "node:fs"; +import { copyFileSync, existsSync, rmdirSync, writeFileSync } from "node:fs"; import * as core from '@actions/core'; import { SpawnSyncOptions } from "child_process"; @@ -365,7 +365,9 @@ async function packageForMacos(projectRoot: string, arch: string[], profile: str const version = await readVersion(projectRoot) - rmdirSync(outDirPath) + if (existsSync(outDirPath)) { + rmdirSync(outDirPath) + } mkdirSync(outDirPath) mkdirSync(bundleDir) From 8784598cd8b94fbde62011ea07f6d7253259efbd Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 22 Feb 2025 20:10:13 +0100 Subject: [PATCH 367/540] Fix macos build artifact path --- .github/workflows/setup-macos.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/setup-macos.yaml b/.github/workflows/setup-macos.yaml index 2dc8c85..6b1b8eb 100644 --- a/.github/workflows/setup-macos.yaml +++ b/.github/workflows/setup-macos.yaml @@ -62,6 +62,6 @@ jobs: if: ${{ inputs.upload-artifact }} with: name: 'gauntlet-universal-macos.dmg' - path: 'target/out/gauntlet-universal-macos.dmg' + path: 'target/gauntlet-universal-macos.dmg' if-no-files-found: 'error' retention-days: 7 From f4d63a08ee617340f94690195f874c735e7ab13c Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 23 Feb 2025 13:36:09 +0100 Subject: [PATCH 368/540] Fix broken application plugin on non-macos platforms --- bundled_plugins/gauntlet/src/applications.tsx | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/bundled_plugins/gauntlet/src/applications.tsx b/bundled_plugins/gauntlet/src/applications.tsx index f8597e1..cc1fc18 100644 --- a/bundled_plugins/gauntlet/src/applications.tsx +++ b/bundled_plugins/gauntlet/src/applications.tsx @@ -25,21 +25,6 @@ type EntrypointPreferences = { experimentalWindowTracking: boolean, bundleNameLa export default async function Applications(context: GeneratorContext): Promise void)> { const { add, remove, get, getAll, entrypointPreferences: { experimentalWindowTracking, bundleNameLang } } = context; - let lang: string | undefined; - - switch (bundleNameLang) { - case "default": { - lang = undefined; - break; - } - case "localized": { - lang = macos_get_localized_language(); - break; - } - default: { - throw new Error("Unknown bundle name type") - } - } switch (current_os()) { case "linux": { @@ -118,6 +103,22 @@ export default async function Applications(context: GeneratorContext= 13) { From 1512e6823739879df05539a195f8eb4e2fff6abc Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 23 Feb 2025 13:40:40 +0100 Subject: [PATCH 369/540] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a638e56..11ff013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Fixed application plugin being broken on non-macos platforms + ## [15] - 2025-02-22 ### General From 900180dfcbd1618dbcfbde7eb5d241f75495124f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 23 Feb 2025 13:15:12 +0000 Subject: [PATCH 370/540] Prepare for v16 release --- CHANGELOG.md | 2 ++ VERSION | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ff013..4af99fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +## [16] - 2025-02-23 + - Fixed application plugin being broken on non-macos platforms ## [15] - 2025-02-22 diff --git a/VERSION b/VERSION index 3f10ffe..19c7bdb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -15 \ No newline at end of file +16 \ No newline at end of file From e2428ff7590b8cea6475099d2e1a14accf17792e Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 2 Mar 2025 15:07:09 +0100 Subject: [PATCH 371/540] Update README.md --- README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aac5d23..df60d5a 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,10 @@ Web-first cross-platform application launcher with React-based plugins -> [!NOTE] -> Launcher is in active development, expect bugs, missing features, incomplete ux, etc. +> [!WARNING] +> Launcher is being developed by single developer in their free time. +> Changes may be few and far between. +> If you face any issues they will likely not get fixed unless they become a problem for that developer > > There will probably be breaking changes which will be documented in [changelog](CHANGELOG.md). @@ -70,10 +72,15 @@ https://github.com/user-attachments/assets/19964ed6-9cd9-48d4-9835-6be04de14b66 ##### OS Support -- Linux - - Both X11 and Wayland (via LayerShell protocol) are supported +##### Official +- Linux X11 - Application plugin depends on `gtk-launch` - macOS + +##### Best-effort +- Linux Wayland + - LayerShell support required + - Application plugin depends on `gtk-launch` - Windows ##### Planned features From ecd21cf5181e5323f9c8d32b715da7720600b4ee Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:32:55 +0100 Subject: [PATCH 372/540] Drop events if state doesn't exist due to race condition instead of panicking --- rust/client/src/ui/widget/events.rs | 32 +++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/rust/client/src/ui/widget/events.rs b/rust/client/src/ui/widget/events.rs index 3fb8d91..026ddf2 100644 --- a/rust/client/src/ui/widget/events.rs +++ b/rust/client/src/ui/widget/events.rs @@ -86,7 +86,9 @@ impl ComponentWidgetEvent { Some(create_action_on_action_event(widget_id, id)) } ComponentWidgetEvent::ToggleDatePicker { widget_id } => { - let state = state.expect("state should always exist for "); + let Some(state) = state else { + return None; + }; let ComponentWidgetState::DatePicker(DatePickerState { state_value: _, @@ -100,7 +102,9 @@ impl ComponentWidgetEvent { None } ComponentWidgetEvent::CancelDatePicker { widget_id } => { - let state = state.expect("state should always exist for "); + let Some(state) = state else { + return None; + }; let ComponentWidgetState::DatePicker(DatePickerState { state_value: _, @@ -114,7 +118,9 @@ impl ComponentWidgetEvent { None } ComponentWidgetEvent::SubmitDatePicker { widget_id, value } => { - let state = state.expect("state should always exist for "); + let Some(state) = state else { + return None; + }; { let ComponentWidgetState::DatePicker(DatePickerState { @@ -131,7 +137,9 @@ impl ComponentWidgetEvent { Some(create_date_picker_on_change_event(widget_id, Some(value))) } ComponentWidgetEvent::ToggleCheckbox { widget_id, value } => { - let state = state.expect("state should always exist for "); + let Some(state) = state else { + return None; + }; { let ComponentWidgetState::Checkbox(CheckboxState { state_value }) = state else { @@ -144,7 +152,9 @@ impl ComponentWidgetEvent { Some(create_checkbox_on_change_event(widget_id, value)) } ComponentWidgetEvent::SelectPickList { widget_id, value } => { - let state = state.expect("state should always exist for "); + let Some(state) = state else { + return None; + }; { let ComponentWidgetState::Select(SelectState { state_value }) = state else { @@ -157,7 +167,9 @@ impl ComponentWidgetEvent { Some(create_select_on_change_event(widget_id, Some(value))) } ComponentWidgetEvent::OnChangeTextField { widget_id, value } => { - let state = state.expect("state should always exist for "); + let Some(state) = state else { + return None; + }; { let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { @@ -170,7 +182,9 @@ impl ComponentWidgetEvent { Some(create_text_field_on_change_event(widget_id, Some(value))) } ComponentWidgetEvent::OnChangePasswordField { widget_id, value } => { - let state = state.expect("state should always exist for "); + let Some(state) = state else { + return None; + }; { let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { @@ -183,7 +197,9 @@ impl ComponentWidgetEvent { Some(create_password_field_on_change_event(widget_id, Some(value))) } ComponentWidgetEvent::OnChangeSearchBar { widget_id, value } => { - let state = state.expect("state should always exist for "); + let Some(state) = state else { + return None; + }; { let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { From e1216598a9f1cbe47fb2912422fafa04cacb6012 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:45:47 +0100 Subject: [PATCH 373/540] Cleanup formatting a little --- rust/client/src/ui/widget/events.rs | 78 +++++++++++------------------ 1 file changed, 28 insertions(+), 50 deletions(-) diff --git a/rust/client/src/ui/widget/events.rs b/rust/client/src/ui/widget/events.rs index 026ddf2..ed250b4 100644 --- a/rust/client/src/ui/widget/events.rs +++ b/rust/client/src/ui/widget/events.rs @@ -90,15 +90,12 @@ impl ComponentWidgetEvent { return None; }; - let ComponentWidgetState::DatePicker(DatePickerState { - state_value: _, - show_picker, - }) = state - else { + let ComponentWidgetState::DatePicker(DatePickerState { show_picker, .. }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; *show_picker = !*show_picker; + None } ComponentWidgetEvent::CancelDatePicker { widget_id } => { @@ -106,15 +103,12 @@ impl ComponentWidgetEvent { return None; }; - let ComponentWidgetState::DatePicker(DatePickerState { - state_value: _, - show_picker, - }) = state - else { + let ComponentWidgetState::DatePicker(DatePickerState { show_picker, .. }) = state else { panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) }; *show_picker = false; + None } ComponentWidgetEvent::SubmitDatePicker { widget_id, value } => { @@ -122,17 +116,11 @@ impl ComponentWidgetEvent { return None; }; - { - let ComponentWidgetState::DatePicker(DatePickerState { - state_value: _, - show_picker, - }) = state - else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; + let ComponentWidgetState::DatePicker(DatePickerState { show_picker, .. }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; - *show_picker = false; - } + *show_picker = false; Some(create_date_picker_on_change_event(widget_id, Some(value))) } @@ -141,13 +129,11 @@ impl ComponentWidgetEvent { return None; }; - { - let ComponentWidgetState::Checkbox(CheckboxState { state_value }) = state else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; + let ComponentWidgetState::Checkbox(CheckboxState { state_value }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; - *state_value = !*state_value; - } + *state_value = !*state_value; Some(create_checkbox_on_change_event(widget_id, value)) } @@ -156,13 +142,11 @@ impl ComponentWidgetEvent { return None; }; - { - let ComponentWidgetState::Select(SelectState { state_value }) = state else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; + let ComponentWidgetState::Select(SelectState { state_value }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; - *state_value = Some(value.clone()); - } + *state_value = Some(value.clone()); Some(create_select_on_change_event(widget_id, Some(value))) } @@ -171,13 +155,11 @@ impl ComponentWidgetEvent { return None; }; - { - let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; + let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; - *state_value = value.clone(); - } + *state_value = value.clone(); Some(create_text_field_on_change_event(widget_id, Some(value))) } @@ -186,13 +168,11 @@ impl ComponentWidgetEvent { return None; }; - { - let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; + let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; - *state_value = value.clone(); - } + *state_value = value.clone(); Some(create_password_field_on_change_event(widget_id, Some(value))) } @@ -201,13 +181,11 @@ impl ComponentWidgetEvent { return None; }; - { - let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { - panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) - }; + let ComponentWidgetState::TextField(TextFieldState { state_value, .. }) = state else { + panic!("unexpected state kind, widget_id: {:?} state: {:?}", widget_id, state) + }; - *state_value = value.clone(); - } + *state_value = value.clone(); Some(create_search_bar_on_change_event(widget_id, Some(value))) } From b0f03daba75e493bdb7095dbc15b0cd1567dd185 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 4 Mar 2025 22:03:15 +0100 Subject: [PATCH 374/540] Fix event loss due to race condition by allowing only one concurrent view event --- README.md | 3 +- js/core/src/core.tsx | 6 +- js/react_renderer/src/renderer.ts | 4 +- js/typings/index.d.ts | 1 + rust/client/src/ui/mod.rs | 99 +++++++++++++++++++++-------- rust/common/src/model.rs | 4 +- rust/common/src/rpc/frontend_api.rs | 10 +++ rust/plugin_runtime/src/api.rs | 10 +++ rust/plugin_runtime/src/deno.rs | 2 + rust/plugin_runtime/src/events.rs | 20 ++++++ rust/plugin_runtime/src/model.rs | 1 + rust/server/src/plugins/js.rs | 11 ++++ 12 files changed, 139 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index df60d5a..33efdc7 100644 --- a/README.md +++ b/README.md @@ -75,13 +75,14 @@ https://github.com/user-attachments/assets/19964ed6-9cd9-48d4-9835-6be04de14b66 ##### Official - Linux X11 - Application plugin depends on `gtk-launch` -- macOS +- macOS M1 ##### Best-effort - Linux Wayland - LayerShell support required - Application plugin depends on `gtk-launch` - Windows +- macOS Intel ##### Planned features diff --git a/js/core/src/core.tsx b/js/core/src/core.tsx index aa4b2a4..6d8be58 100644 --- a/js/core/src/core.tsx +++ b/js/core/src/core.tsx @@ -12,7 +12,8 @@ import { op_plugin_get_pending_event, plugin_preferences_required, show_plugin_error_view, - show_preferences_required_view + show_preferences_required_view, + synchronize_event } from "ext:core/ops"; @@ -65,6 +66,9 @@ export async function runPluginLoop() { } catch (e) { console.error("Error occurred when receiving view event to handle", e) } + + await synchronize_event() + break; } case "KeyboardEvent": { diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index 875d2bd..dd5482e 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -1,6 +1,6 @@ import ReactReconciler, { HostConfig, OpaqueHandle } from "react-reconciler"; import { createContext, ReactNode, useContext } from 'react'; -import { DefaultEventPriority } from 'react-reconciler/constants'; +import { DiscreteEventPriority } from 'react-reconciler/constants'; import { asset_data, asset_data_blocking, @@ -261,7 +261,7 @@ export const createHostConfig = (): HostConfig< }, noTimeout: -1, isPrimaryRenderer: true, - getCurrentEventPriority: () => DefaultEventPriority, + getCurrentEventPriority: () => DiscreteEventPriority, getInstanceFromNode(_node: any): ReactReconciler.Fiber | null | undefined { return undefined; }, diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 4f8652a..d016678 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -226,6 +226,7 @@ declare module "ext:core/ops" { function op_entrypoint_names(): Record; function clear_inline_view(): void; function op_plugin_get_pending_event(): Promise; + function synchronize_event(): Promise; function hide_window(): void; function get_entrypoint_generator_entrypoint_ids(): Promise diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index ee689da..efdaa19 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -3,6 +3,8 @@ use std::fs; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; +use std::sync::atomic::AtomicU32; +use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::Mutex as StdMutex; use std::sync::Mutex; @@ -50,7 +52,6 @@ use iced::event; use iced::executor; use iced::font; use iced::futures; -use iced::futures::channel::mpsc::Sender; use iced::futures::SinkExt; use iced::keyboard; use iced::keyboard::key; @@ -94,6 +95,7 @@ use iced::Task; use iced_fonts::BOOTSTRAP_FONT_BYTES; use serde::Deserialize; use tokio::runtime::Handle; +use tokio::sync::oneshot; use tokio::sync::Mutex as TokioMutex; use tokio::sync::RwLock as TokioRwLock; @@ -160,6 +162,7 @@ pub struct AppModel { window_position_file: PathBuf, #[cfg(target_os = "linux")] x11_active_window: Option, + event_synchronizer: Arc>>>, // ephemeral state prompt: String, @@ -356,6 +359,9 @@ pub enum AppMsg { X11ActiveWindowChanged { window: u32, }, + EventHanded { + plugin_id: PluginId, + }, } #[cfg(target_os = "linux")] @@ -702,6 +708,7 @@ fn new( x11_active_window: None, // ephemeral state + event_synchronizer: Arc::new(Mutex::new(HashMap::new())), prompt: "".to_string(), // state @@ -723,6 +730,12 @@ fn title(state: &AppModel, window: window::Id) -> String { } } +static BD_SEQ: AtomicU32 = AtomicU32::new(0); + +fn get_bdseq() -> u32 { + BD_SEQ.fetch_add(1, Ordering::SeqCst) + 1 +} + fn update(state: &mut AppModel, message: AppMsg) -> Task { match message { AppMsg::OpenView { @@ -923,6 +936,7 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { container, images, } => { + println!("render"); let has_children = container.content.is_some(); Task::batch([ @@ -1612,6 +1626,18 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } } + AppMsg::EventHanded { plugin_id } => { + state + .event_synchronizer + .lock() + .expect("lock is poisoned") + .remove(&plugin_id) + .expect("there should always be a responder here") + .send(()) + .expect("the other side was dropped"); + + Task::none() + } } } @@ -2431,45 +2457,63 @@ impl AppModel { ) -> Task { let mut backend_client = self.backend_api.clone(); + let val = get_bdseq(); + let event = self .client_context .handle_event(render_location, &plugin_id, widget_event.clone()); - Task::perform( - async move { - if let Some(event) = event { - match event { - UiViewEvent::View { - widget_id, - event_name, - event_arguments, - } => { - let msg = match widget_event { - ComponentWidgetEvent::ActionClick { .. } => { - AppMsg::ToggleActionPanel { keyboard: false } - } - _ => AppMsg::Noop, - }; + println!("handle_plugin_event {} {:?}", val, event); + if let Some(event) = event { + match event { + UiViewEvent::View { + widget_id, + event_name, + event_arguments, + } => { + let msg = match widget_event { + ComponentWidgetEvent::ActionClick { .. } => AppMsg::ToggleActionPanel { keyboard: false }, + _ => AppMsg::Noop, + }; + + let (sender, receiver) = oneshot::channel(); + + self.event_synchronizer + .lock() + .expect("lock is poisoned") + .insert(plugin_id.clone(), sender); + + Task::perform( + async move { backend_client .send_view_event(plugin_id, widget_id, event_name, event_arguments) .await?; + receiver.await.expect("the other side was dropped"); + + println!("handle_plugin_event {} after", val); + Ok(msg) - } - UiViewEvent::Open { href } => { + }, + |result| handle_backend_error(result, |msg| msg), + ) + } + UiViewEvent::Open { href } => { + Task::perform( + async move { backend_client.send_open_event(plugin_id, href).await?; Ok(AppMsg::Noop) - } - UiViewEvent::AppEvent { event } => Ok(event), - } - } else { - Ok(AppMsg::Noop) + }, + |result| handle_backend_error(result, |msg| msg), + ) } - }, - |result| handle_backend_error(result, |msg| msg), - ) + UiViewEvent::AppEvent { event } => Task::done(event), + } + } else { + Task::none() + } } fn handle_main_view_keyboard_event( @@ -2860,7 +2904,7 @@ fn handle_backend_error(result: Result, conver async fn request_loop( frontend_receiver: Arc>>, - mut sender: Sender, + mut sender: futures::channel::mpsc::Sender, ) { let mut frontend_receiver = frontend_receiver.write().await; loop { @@ -3005,6 +3049,7 @@ async fn request_loop( action_index, } } + UiRequestData::SynchronizeEvent { plugin_id } => AppMsg::EventHanded { plugin_id }, } }; diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index e1aeb1f..27b6711 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use std::fmt::Display; use std::fmt::Formatter; -use std::path::Path; use std::path::PathBuf; use std::sync::Arc; @@ -292,6 +291,9 @@ pub enum UiRequestData { entrypoint_name: String, action_index: usize, }, + SynchronizeEvent { + plugin_id: PluginId, + }, } #[derive(Debug)] diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index 3c65ea2..c162f81 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -262,4 +262,14 @@ impl FrontendApi { Ok(()) } + + pub async fn synchronize_event(&self, plugin_id: PluginId) -> Result<(), FrontendApiError> { + let data = UiRequestData::SynchronizeEvent { plugin_id }; + + let UiResponseData::Nothing = self.frontend_sender.send_receive(data).await? else { + unreachable!() + }; + + Ok(()) + } } diff --git a/rust/plugin_runtime/src/api.rs b/rust/plugin_runtime/src/api.rs index 02fb265..fb3a7d7 100644 --- a/rust/plugin_runtime/src/api.rs +++ b/rust/plugin_runtime/src/api.rs @@ -67,6 +67,7 @@ pub trait BackendForPluginRuntimeApi { entrypoint_preferences_required: bool, ) -> anyhow::Result<()>; async fn ui_clear_inline_view(&self) -> anyhow::Result<()>; + async fn ui_synchronize_event(&self) -> anyhow::Result<()>; } #[derive(Clone)] @@ -334,4 +335,13 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } + + async fn ui_synchronize_event(&self) -> anyhow::Result<()> { + let request = JsRequest::SynchronizeEvent; + + match self.request(request).await? { + JsResponse::Nothing => Ok(()), + value @ _ => panic!("Unexpected JsResponse type: {:?}", value), + } + } } diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index ac73602..6ddd8ef 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -49,6 +49,7 @@ use crate::environment::environment_is_development; use crate::environment::environment_plugin_cache_dir; use crate::environment::environment_plugin_data_dir; use crate::events::op_plugin_get_pending_event; +use crate::events::synchronize_event; use crate::events::EventReceiver; use crate::events::JsEvent; use crate::logs::op_log_debug; @@ -286,6 +287,7 @@ deno_core::extension!( ops = [ // core op_plugin_get_pending_event, + synchronize_event, // logs op_log_trace, diff --git a/rust/plugin_runtime/src/events.rs b/rust/plugin_runtime/src/events.rs index 4dc4ece..ef9f706 100644 --- a/rust/plugin_runtime/src/events.rs +++ b/rust/plugin_runtime/src/events.rs @@ -14,6 +14,9 @@ use serde::Deserialize; use serde::Serialize; use tokio::sync::mpsc::Receiver; +use crate::api::BackendForPluginRuntimeApiProxy; +use crate::BackendForPluginRuntimeApi; + #[derive(Debug, Deserialize, Serialize, Encode, Decode)] #[serde(tag = "type")] pub enum JsEvent { @@ -104,3 +107,20 @@ pub async fn op_plugin_get_pending_event(state: Rc>) -> anyhow: Ok(event) } + +#[op2(async)] +pub async fn synchronize_event(state: Rc>) -> anyhow::Result<()> { + let api = { + let state = state.borrow(); + + let api = state.borrow::().clone(); + + api + }; + + tracing::trace!(target = "renderer_rs", "Synchronizing event"); + + api.ui_synchronize_event().await?; + + Ok(()) +} diff --git a/rust/plugin_runtime/src/model.rs b/rust/plugin_runtime/src/model.rs index 1294429..2bbc9c7 100644 --- a/rust/plugin_runtime/src/model.rs +++ b/rust/plugin_runtime/src/model.rs @@ -122,6 +122,7 @@ pub enum JsRequest { container: RootWidget, }, ClearInlineView, + SynchronizeEvent, ShowPluginErrorView { entrypoint_id: EntrypointId, render_location: JsUiRenderLocation, diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 2615a0f..c115d63 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -682,6 +682,11 @@ async fn handle_message(message: JsRequest, api: &BackendForPluginRuntimeApiImpl Ok(JsResponse::ActionIdForShortcut { data }) } + JsRequest::SynchronizeEvent => { + api.ui_synchronize_event().await?; + + Ok(JsResponse::Nothing) + } } } @@ -1231,6 +1236,12 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { Ok(()) } + + async fn ui_synchronize_event(&self) -> anyhow::Result<()> { + self.frontend_api.synchronize_event(self.plugin_id.clone()).await?; + + Ok(()) + } } fn preferences_to_js( From b5575725f05830ee825f60a56d3ecdbece70eb4a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 4 Mar 2025 22:18:17 +0100 Subject: [PATCH 375/540] Clean up debugging leftovers --- rust/client/src/ui/mod.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index efdaa19..f96ae36 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -3,8 +3,6 @@ use std::fs; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; -use std::sync::atomic::AtomicU32; -use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::Mutex as StdMutex; use std::sync::Mutex; @@ -730,12 +728,6 @@ fn title(state: &AppModel, window: window::Id) -> String { } } -static BD_SEQ: AtomicU32 = AtomicU32::new(0); - -fn get_bdseq() -> u32 { - BD_SEQ.fetch_add(1, Ordering::SeqCst) + 1 -} - fn update(state: &mut AppModel, message: AppMsg) -> Task { match message { AppMsg::OpenView { @@ -936,7 +928,6 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { container, images, } => { - println!("render"); let has_children = container.content.is_some(); Task::batch([ @@ -2457,14 +2448,10 @@ impl AppModel { ) -> Task { let mut backend_client = self.backend_api.clone(); - let val = get_bdseq(); - let event = self .client_context .handle_event(render_location, &plugin_id, widget_event.clone()); - println!("handle_plugin_event {} {:?}", val, event); - if let Some(event) = event { match event { UiViewEvent::View { @@ -2492,8 +2479,6 @@ impl AppModel { receiver.await.expect("the other side was dropped"); - println!("handle_plugin_event {} after", val); - Ok(msg) }, |result| handle_backend_error(result, |msg| msg), From 1aca32ae9cb03fcd7ba0d2409dc10f50cfecb292 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 5 Mar 2025 21:54:15 +0100 Subject: [PATCH 376/540] Fix panic when closing inline view due to action being run --- rust/client/src/ui/mod.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index f96ae36..f143c48 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -161,6 +161,7 @@ pub struct AppModel { #[cfg(target_os = "linux")] x11_active_window: Option, event_synchronizer: Arc>>>, + pending_window_state_reset: bool, // ephemeral state prompt: String, @@ -704,9 +705,10 @@ fn new( window_position_file: setup_data.window_position_file, #[cfg(target_os = "linux")] x11_active_window: None, + event_synchronizer: Arc::new(Mutex::new(HashMap::new())), + pending_window_state_reset: false, // ephemeral state - event_synchronizer: Arc::new(Mutex::new(HashMap::new())), prompt: "".to_string(), // state @@ -2295,9 +2297,7 @@ impl AppModel { let mut commands = vec![]; - if reset_state { - commands.push(self.reset_window_state()); - } + self.pending_window_state_reset = reset_state; #[cfg(target_os = "linux")] if self.wayland { @@ -2370,7 +2370,13 @@ impl AppModel { window::change_mode(self.main_window_id, Mode::Windowed), ]); - open_task + let mut commands = vec![open_task]; + + if self.pending_window_state_reset { + commands.push(self.reset_window_state()); + } + + Task::batch(commands) } fn reset_window_state(&mut self) -> Task { From 7e5d4aa8eafd6e9d49e2b2aeaf4211af815a8877 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:43:35 +0100 Subject: [PATCH 377/540] Improve logging of panic, abort process on panic --- rust/common/src/dirs.rs | 2 +- rust/server/src/lib.rs | 34 ++++++++++++++++++---------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/rust/common/src/dirs.rs b/rust/common/src/dirs.rs index c549f76..07a64dd 100644 --- a/rust/common/src/dirs.rs +++ b/rust/common/src/dirs.rs @@ -95,7 +95,7 @@ impl Dirs { } pub fn plugin_crash_log_file(&self, plugin_uuid: &str) -> PathBuf { - self.logs_dir().join(&plugin_uuid).join("crash.txt") + self.logs_dir().join(format!("crash-{}.txt", &plugin_uuid)) } pub fn plugin_log_files(&self, plugin_uuid: &str) -> (PathBuf, PathBuf) { diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index 6ea48bf..decb5c0 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -2,6 +2,7 @@ use std::backtrace::Backtrace; use std::fs::File; use std::io::Write; use std::path::PathBuf; +use std::process::exit; use std::rc::Rc; use std::sync::Arc; use std::time::SystemTime; @@ -41,7 +42,6 @@ const PLUGIN_CONNECT_ENV: &'static str = "__GAUNTLET_INTERNAL_PLUGIN_CONNECT__"; const PLUGIN_UUID_ENV: &'static str = "__GAUNTLET_INTERNAL_PLUGIN_UUID__"; pub fn start(minimized: bool) { - #[cfg(not(feature = "release"))] register_panic_hook(std::env::var(PLUGIN_UUID_ENV).ok()); if let Ok(socket_name) = std::env::var(PLUGIN_CONNECT_ENV) { @@ -340,10 +340,9 @@ async fn handle_request( Ok(response_data) } -#[cfg(not(feature = "release"))] fn register_panic_hook(plugin_runtime: Option) { unsafe { - std::env::set_var("RUST_BACKTRACE", "1"); + std::env::set_var("RUST_BACKTRACE", "full"); }; let dirs = Dirs::new(); @@ -369,22 +368,25 @@ fn register_panic_hook(plugin_runtime: Option) { let location = panic_info.location().map(|l| l.to_string()); let backtrace = Backtrace::capture(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .map(|duration| duration.as_millis().to_string()) + .unwrap_or("Unknown".to_string()); + + let content = format!( + "Panic on {}\nPayload: {}\nLocation: {:?}\nBacktrace: {}\n", + now, payload, location, backtrace + ); + let crash_file = File::options().create(true).append(true).open(&crash_file); if let Ok(mut crash_file) = crash_file { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .map(|duration| duration.as_millis().to_string()) - .unwrap_or("Unknown".to_string()); - - let _ = crash_file.write_all( - format!( - "Panic on {}\nPayload: {}\nLocation: {:?}\nBacktrace: {}\n", - now, payload, location, backtrace - ) - .as_bytes(), - ); + let _ = crash_file.write_all(content.as_bytes()); } + + eprintln!("{}", content); + + exit(101); // poor man's abort on panic because actual setting makes v8 linking fail })); } From f96941d4360d5b77bab5411b7186e30f6064adb4 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 7 Mar 2025 21:54:53 +0100 Subject: [PATCH 378/540] Add thread names to logs --- rust/cli/src/lib.rs | 6 +- .../plugins/applications/linux/wayland/mod.rs | 14 ++-- .../src/plugins/applications/linux/x11.rs | 14 ++-- rust/server/src/lib.rs | 10 ++- rust/server/src/plugins/loader.rs | 68 ++++++++++--------- 5 files changed, 66 insertions(+), 46 deletions(-) diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs index f4c641c..74b8934 100644 --- a/rust/cli/src/lib.rs +++ b/rust/cli/src/lib.rs @@ -7,6 +7,7 @@ use gauntlet_client::open_window; use gauntlet_management_client::start_management_client; use gauntlet_server::run_action; use gauntlet_server::start; +use tracing_subscriber::EnvFilter; /// Gauntlet CLI /// @@ -48,7 +49,10 @@ enum Commands { } pub fn init() { - tracing_subscriber::fmt::init(); + tracing_subscriber::fmt::fmt() + .with_thread_names(true) + .with_env_filter(EnvFilter::from_default_env()) + .init(); let cli = Cli::parse(); diff --git a/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs b/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs index fdd780e..5a582ca 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/wayland/mod.rs @@ -1,5 +1,6 @@ use std::cell::RefCell; use std::rc::Rc; +use std::thread; use anyhow::anyhow; use deno_core::op2; @@ -52,11 +53,14 @@ impl WaylandDesktopEnvironment { let handle = Handle::current(); - std::thread::spawn(|| { - if let Err(e) = run_wayland_client(handle, event_sender, activate_receiver) { - tracing::error!("Error while running wayland client: {:?}", e); - } - }); + thread::Builder::new() + .name("gauntlet-wayland-events".to_string()) + .spawn(|| { + if let Err(e) = run_wayland_client(handle, event_sender, activate_receiver) { + tracing::error!("Error while running wayland client: {:?}", e); + } + }) + .expect("failed to spawn thread"); Ok(environment) } diff --git a/rust/plugin_runtime/src/plugins/applications/linux/x11.rs b/rust/plugin_runtime/src/plugins/applications/linux/x11.rs index 7d4531f..341eae7 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/x11.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/x11.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::convert::Infallible; use std::rc::Rc; use std::str::FromStr; +use std::thread; use anyhow::anyhow; use deno_core::op2; @@ -47,11 +48,14 @@ impl X11DesktopEnvironment { let handle = Handle::current(); - std::thread::spawn(move || { - if let Err(e) = listen_on_x11_events(handle, sender.clone()) { - tracing::error!("Error while listening on x11 events: {}", e); - } - }); + thread::Builder::new() + .name("gauntlet-x11-events".to_string()) + .spawn(move || { + if let Err(e) = listen_on_x11_events(handle, sender.clone()) { + tracing::error!("Error while listening on x11 events: {}", e); + } + }) + .expect("failed to spawn thread"); Self { receiver: Rc::new(RefCell::new(receiver)), diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index decb5c0..1ed84fc 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use std::process::exit; use std::rc::Rc; use std::sync::Arc; +use std::thread; use std::time::SystemTime; use std::time::UNIX_EPOCH; @@ -68,9 +69,12 @@ pub fn start(minimized: bool) { let (frontend_sender, frontend_receiver) = channel::(); let (backend_sender, backend_receiver) = channel::(); - std::thread::spawn(|| { - start_server(frontend_sender, backend_receiver); - }); + thread::Builder::new() + .name("gauntlet-server".to_string()) + .spawn(|| { + start_server(frontend_sender, backend_receiver); + }) + .expect("failed to spawn thread"); start_client(minimized, frontend_receiver, backend_sender) } diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index 9dba73d..af56075 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -71,45 +71,49 @@ impl PluginLoader { let handle = tokio::runtime::Handle::current(); let plugin_id_clone = plugin_id.clone(); - thread::spawn(move || { - let result = handle.block_on(async move { - let temp_dir = tempfile::tempdir()?; - PluginLoader::download(temp_dir.path(), plugin_id_clone.clone())?; + thread::Builder::new() + .name("gauntlet-plugin-download".to_string()) + .spawn(move || { + let result = handle.block_on(async move { + let temp_dir = tempfile::tempdir()?; - let plugin_data = PluginLoader::read_plugin_dir(temp_dir.path(), plugin_id_clone.clone()).await?; + PluginLoader::download(temp_dir.path(), plugin_id_clone.clone())?; - data_db_repository - .save_plugin(DbWritePlugin { - id: plugin_data.id, - name: plugin_data.name, - description: plugin_data.description, - enabled: false, - code: plugin_data.code, - entrypoints: plugin_data.entrypoints, - asset_data: plugin_data.asset_data, - permissions: plugin_data.permissions, - plugin_type: db_plugin_type_to_str(DbPluginType::Normal).to_owned(), - preferences: plugin_data.preferences, - }) - .await?; + let plugin_data = PluginLoader::read_plugin_dir(temp_dir.path(), plugin_id_clone.clone()).await?; - anyhow::Ok(()) - }); + data_db_repository + .save_plugin(DbWritePlugin { + id: plugin_data.id, + name: plugin_data.name, + description: plugin_data.description, + enabled: false, + code: plugin_data.code, + entrypoints: plugin_data.entrypoints, + asset_data: plugin_data.asset_data, + permissions: plugin_data.permissions, + plugin_type: db_plugin_type_to_str(DbPluginType::Normal).to_owned(), + preferences: plugin_data.preferences, + }) + .await?; - handle.block_on(async move { - match result { - Ok(()) => { - tracing::info!("Finished download of plugin: {:?}", plugin_id); - download_status_guard.download_finished() + anyhow::Ok(()) + }); + + handle.block_on(async move { + match result { + Ok(()) => { + tracing::info!("Finished download of plugin: {:?}", plugin_id); + download_status_guard.download_finished() + } + Err(err) => { + tracing::warn!("Download of plugin {:?} returned an error {:?}", plugin_id, err); + download_status_guard.download_failed(format!("{}", err)) + } } - Err(err) => { - tracing::warn!("Download of plugin {:?} returned an error {:?}", plugin_id, err); - download_status_guard.download_failed(format!("{}", err)) - } - } + }) }) - }); + .expect("failed to spawn thread"); Ok(()) } From 76609064df5a35393736b6d1019d7d24660186b6 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 11 Mar 2025 20:05:14 +0100 Subject: [PATCH 379/540] Revert previous attempt to fix event loss --- js/core/src/core.tsx | 6 +----- js/react_renderer/src/renderer.ts | 4 ++-- js/typings/index.d.ts | 1 - rust/client/src/ui/mod.rs | 27 --------------------------- rust/common/src/model.rs | 3 --- rust/common/src/rpc/frontend_api.rs | 10 ---------- rust/plugin_runtime/src/api.rs | 10 ---------- rust/plugin_runtime/src/deno.rs | 2 -- rust/plugin_runtime/src/events.rs | 17 ----------------- rust/plugin_runtime/src/model.rs | 1 - rust/server/src/lib.rs | 2 +- rust/server/src/plugins/js.rs | 11 ----------- 12 files changed, 4 insertions(+), 90 deletions(-) diff --git a/js/core/src/core.tsx b/js/core/src/core.tsx index 6d8be58..aa4b2a4 100644 --- a/js/core/src/core.tsx +++ b/js/core/src/core.tsx @@ -12,8 +12,7 @@ import { op_plugin_get_pending_event, plugin_preferences_required, show_plugin_error_view, - show_preferences_required_view, - synchronize_event + show_preferences_required_view } from "ext:core/ops"; @@ -66,9 +65,6 @@ export async function runPluginLoop() { } catch (e) { console.error("Error occurred when receiving view event to handle", e) } - - await synchronize_event() - break; } case "KeyboardEvent": { diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index dd5482e..875d2bd 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -1,6 +1,6 @@ import ReactReconciler, { HostConfig, OpaqueHandle } from "react-reconciler"; import { createContext, ReactNode, useContext } from 'react'; -import { DiscreteEventPriority } from 'react-reconciler/constants'; +import { DefaultEventPriority } from 'react-reconciler/constants'; import { asset_data, asset_data_blocking, @@ -261,7 +261,7 @@ export const createHostConfig = (): HostConfig< }, noTimeout: -1, isPrimaryRenderer: true, - getCurrentEventPriority: () => DiscreteEventPriority, + getCurrentEventPriority: () => DefaultEventPriority, getInstanceFromNode(_node: any): ReactReconciler.Fiber | null | undefined { return undefined; }, diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index d016678..4f8652a 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -226,7 +226,6 @@ declare module "ext:core/ops" { function op_entrypoint_names(): Record; function clear_inline_view(): void; function op_plugin_get_pending_event(): Promise; - function synchronize_event(): Promise; function hide_window(): void; function get_entrypoint_generator_entrypoint_ids(): Promise diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index f143c48..78c8c60 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -160,7 +160,6 @@ pub struct AppModel { window_position_file: PathBuf, #[cfg(target_os = "linux")] x11_active_window: Option, - event_synchronizer: Arc>>>, pending_window_state_reset: bool, // ephemeral state @@ -358,9 +357,6 @@ pub enum AppMsg { X11ActiveWindowChanged { window: u32, }, - EventHanded { - plugin_id: PluginId, - }, } #[cfg(target_os = "linux")] @@ -705,7 +701,6 @@ fn new( window_position_file: setup_data.window_position_file, #[cfg(target_os = "linux")] x11_active_window: None, - event_synchronizer: Arc::new(Mutex::new(HashMap::new())), pending_window_state_reset: false, // ephemeral state @@ -1619,18 +1614,6 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } } - AppMsg::EventHanded { plugin_id } => { - state - .event_synchronizer - .lock() - .expect("lock is poisoned") - .remove(&plugin_id) - .expect("there should always be a responder here") - .send(()) - .expect("the other side was dropped"); - - Task::none() - } } } @@ -2470,21 +2453,12 @@ impl AppModel { _ => AppMsg::Noop, }; - let (sender, receiver) = oneshot::channel(); - - self.event_synchronizer - .lock() - .expect("lock is poisoned") - .insert(plugin_id.clone(), sender); - Task::perform( async move { backend_client .send_view_event(plugin_id, widget_id, event_name, event_arguments) .await?; - receiver.await.expect("the other side was dropped"); - Ok(msg) }, |result| handle_backend_error(result, |msg| msg), @@ -3040,7 +3014,6 @@ async fn request_loop( action_index, } } - UiRequestData::SynchronizeEvent { plugin_id } => AppMsg::EventHanded { plugin_id }, } }; diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 27b6711..0b2217e 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -291,9 +291,6 @@ pub enum UiRequestData { entrypoint_name: String, action_index: usize, }, - SynchronizeEvent { - plugin_id: PluginId, - }, } #[derive(Debug)] diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index c162f81..3c65ea2 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -262,14 +262,4 @@ impl FrontendApi { Ok(()) } - - pub async fn synchronize_event(&self, plugin_id: PluginId) -> Result<(), FrontendApiError> { - let data = UiRequestData::SynchronizeEvent { plugin_id }; - - let UiResponseData::Nothing = self.frontend_sender.send_receive(data).await? else { - unreachable!() - }; - - Ok(()) - } } diff --git a/rust/plugin_runtime/src/api.rs b/rust/plugin_runtime/src/api.rs index fb3a7d7..02fb265 100644 --- a/rust/plugin_runtime/src/api.rs +++ b/rust/plugin_runtime/src/api.rs @@ -67,7 +67,6 @@ pub trait BackendForPluginRuntimeApi { entrypoint_preferences_required: bool, ) -> anyhow::Result<()>; async fn ui_clear_inline_view(&self) -> anyhow::Result<()>; - async fn ui_synchronize_event(&self) -> anyhow::Result<()>; } #[derive(Clone)] @@ -335,13 +334,4 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiProxy { value @ _ => panic!("Unexpected JsResponse type: {:?}", value), } } - - async fn ui_synchronize_event(&self) -> anyhow::Result<()> { - let request = JsRequest::SynchronizeEvent; - - match self.request(request).await? { - JsResponse::Nothing => Ok(()), - value @ _ => panic!("Unexpected JsResponse type: {:?}", value), - } - } } diff --git a/rust/plugin_runtime/src/deno.rs b/rust/plugin_runtime/src/deno.rs index 6ddd8ef..ac73602 100644 --- a/rust/plugin_runtime/src/deno.rs +++ b/rust/plugin_runtime/src/deno.rs @@ -49,7 +49,6 @@ use crate::environment::environment_is_development; use crate::environment::environment_plugin_cache_dir; use crate::environment::environment_plugin_data_dir; use crate::events::op_plugin_get_pending_event; -use crate::events::synchronize_event; use crate::events::EventReceiver; use crate::events::JsEvent; use crate::logs::op_log_debug; @@ -287,7 +286,6 @@ deno_core::extension!( ops = [ // core op_plugin_get_pending_event, - synchronize_event, // logs op_log_trace, diff --git a/rust/plugin_runtime/src/events.rs b/rust/plugin_runtime/src/events.rs index ef9f706..c808a19 100644 --- a/rust/plugin_runtime/src/events.rs +++ b/rust/plugin_runtime/src/events.rs @@ -107,20 +107,3 @@ pub async fn op_plugin_get_pending_event(state: Rc>) -> anyhow: Ok(event) } - -#[op2(async)] -pub async fn synchronize_event(state: Rc>) -> anyhow::Result<()> { - let api = { - let state = state.borrow(); - - let api = state.borrow::().clone(); - - api - }; - - tracing::trace!(target = "renderer_rs", "Synchronizing event"); - - api.ui_synchronize_event().await?; - - Ok(()) -} diff --git a/rust/plugin_runtime/src/model.rs b/rust/plugin_runtime/src/model.rs index 2bbc9c7..1294429 100644 --- a/rust/plugin_runtime/src/model.rs +++ b/rust/plugin_runtime/src/model.rs @@ -122,7 +122,6 @@ pub enum JsRequest { container: RootWidget, }, ClearInlineView, - SynchronizeEvent, ShowPluginErrorView { entrypoint_id: EntrypointId, render_location: JsUiRenderLocation, diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index 1ed84fc..a7b75f5 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -379,7 +379,7 @@ fn register_panic_hook(plugin_runtime: Option) { .unwrap_or("Unknown".to_string()); let content = format!( - "Panic on {}\nPayload: {}\nLocation: {:?}\nBacktrace: {}\n", + "Panic on {}\nPayload: {}\nLocation: {:?}\nBacktrace:\n{}", now, payload, location, backtrace ); diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index c115d63..2615a0f 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -682,11 +682,6 @@ async fn handle_message(message: JsRequest, api: &BackendForPluginRuntimeApiImpl Ok(JsResponse::ActionIdForShortcut { data }) } - JsRequest::SynchronizeEvent => { - api.ui_synchronize_event().await?; - - Ok(JsResponse::Nothing) - } } } @@ -1236,12 +1231,6 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { Ok(()) } - - async fn ui_synchronize_event(&self) -> anyhow::Result<()> { - self.frontend_api.synchronize_event(self.plugin_id.clone()).await?; - - Ok(()) - } } fn preferences_to_js( From 705b42821c3e457a7cc7eeeb8a1a1f7ece133fc7 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 11 Mar 2025 20:08:10 +0100 Subject: [PATCH 380/540] Actually fix the event loss, finally --- js/react_renderer/src/renderer.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index 875d2bd..0ac0984 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -138,20 +138,24 @@ export function showHudWindow(display: string): void { show_hud(display) } -function createWidget(hostContext: HostContext, type: ComponentType, properties: Props, children: UiWidget[] = []): Instance { +function createWidget(id: number | undefined, hostContext: HostContext, type: ComponentType, properties: Props, children: UiWidget[]): Instance { const props = Object.fromEntries( Object.entries(properties) .filter(([key, _]) => key !== "children") ); const instance: Instance = { - widgetId: hostContext.nextId, + widgetId: id != undefined ? id : hostContext.nextId, widgetType: type, widgetProperties: props, widgetChildren: children, hostContext }; - hostContext.nextId += 1 + + if (id == undefined) { + hostContext.nextId += 1 + } + return instance } @@ -183,7 +187,7 @@ export const createHostConfig = (): HostConfig< _internalHandle: OpaqueHandle, ): Instance => { op_log_trace("renderer_js_common", `createInstance is called, type: ${type}, props: ${Deno.inspect(props)}, rootContainer: ${Deno.inspect(rootContainer)}`) - const instance = createWidget(hostContext, type, props) + const instance = createWidget(undefined, hostContext, type, props, []) op_log_trace("renderer_js_common", `createInstance returned, widget: ${Deno.inspect(instance)}`) return instance; @@ -196,7 +200,7 @@ export const createHostConfig = (): HostConfig< _internalHandle: OpaqueHandle ): TextInstance => { op_log_trace("renderer_js_common", `createTextInstance is called, text: ${text}, rootContainer: ${Deno.inspect(rootContainer)}`) - const textInstance = createWidget(hostContext, "gauntlet:text_part", { value: text }) + const textInstance = createWidget(undefined, hostContext, "gauntlet:text_part", { value: text }, []) op_log_trace("renderer_js_common", `createTextInstance returned, widget: ${Deno.inspect(textInstance)}`) return textInstance; @@ -301,19 +305,21 @@ export const createHostConfig = (): HostConfig< ): Instance { op_log_trace("renderer_js_persistence", `cloneInstance is called, instance: ${Deno.inspect(instance)}, updatePayload: ${Deno.inspect(updatePayload)}, type: ${type}, oldProps: ${Deno.inspect(oldProps)}, newProps: ${Deno.inspect(newProps)}, keepChildren: ${keepChildren}, recyclableInstance: ${Deno.inspect(recyclableInstance)}`) + const recyclableId = recyclableInstance != null ? recyclableInstance.widgetId : undefined; + let clonedInstance: Instance; if (keepChildren) { if (updatePayload !== null) { - clonedInstance = createWidget(instance.hostContext, type, newProps, instance.widgetChildren) + clonedInstance = createWidget(recyclableId, instance.hostContext, type, newProps, instance.widgetChildren) } else { - clonedInstance = createWidget(instance.hostContext, type, oldProps, instance.widgetChildren) + clonedInstance = createWidget(recyclableId, instance.hostContext, type, oldProps, instance.widgetChildren) } } else { if (updatePayload !== null) { - clonedInstance = createWidget(instance.hostContext, type, newProps, []) + clonedInstance = createWidget(recyclableId, instance.hostContext, type, newProps, []) } else { - clonedInstance = createWidget(instance.hostContext, type, oldProps, []) + clonedInstance = createWidget(recyclableId, instance.hostContext, type, oldProps, []) } } From d4e88b97a59a3770dd5fbbb99b7715313b17aae3 Mon Sep 17 00:00:00 2001 From: DaRacci Date: Mon, 10 Mar 2025 06:31:09 +0000 Subject: [PATCH 381/540] fix(flake): use strings instead of deprecated url literals. --- flake.lock | 33 ++++++++++++++++++--------------- flake.nix | 10 +++++----- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/flake.lock b/flake.lock index e355443..005c4b5 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1733688869, - "narHash": "sha256-KrhxxFj1CjESDrL5+u/zsVH0K+Ik9tvoac/oFPoxSB8=", + "lastModified": 1741481578, + "narHash": "sha256-JBTSyJFQdO3V8cgcL08VaBUByEU6P5kXbTJN6R0PFQo=", "owner": "ipetkov", "repo": "crane", - "rev": "604637106e420ad99907cae401e13ab6b452e7d9", + "rev": "bb1c9567c43e4434f54e9481eb4b8e8e0d50f0b5", "type": "github" }, "original": { @@ -36,11 +36,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1733312601, - "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "lastModified": 1741352980, + "narHash": "sha256-+u2UunDA4Cl5Fci3m7S643HzKmIDAe+fiXrLqYsR2fs=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "rev": "f4330d22f1c5d2ba72d3d22df5597d123fdb60a9", "type": "github" }, "original": { @@ -51,11 +51,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1733392399, - "narHash": "sha256-kEsTJTUQfQFIJOcLYFt/RvNxIK653ZkTBIs4DG+cBns=", + "lastModified": 1741379970, + "narHash": "sha256-Wh7esNh7G24qYleLvgOSY/7HlDUzWaL/n4qzlBePpiw=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d0797a04b81caeae77bcff10a9dde78bc17f5661", + "rev": "36fd87baa9083f34f7f5027900b62ee6d09b1f2f", "type": "github" }, "original": { @@ -67,14 +67,17 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1733096140, - "narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" + "lastModified": 1740877520, + "narHash": "sha256-oiwv/ZK/2FhGxrCkQkB83i7GnWXPPLzoqFHpDD3uYpk=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "147dee35aab2193b174e4c0868bd80ead5ce755c", + "type": "github" }, "original": { - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" } }, "root": { diff --git a/flake.nix b/flake.nix index a871c7b..bd6b95a 100644 --- a/flake.nix +++ b/flake.nix @@ -2,11 +2,11 @@ description = "https://github.com/project-gauntlet/gauntlet"; outputs = inputs: inputs.flake-parts.lib.mkFlake {inherit inputs;} {imports = [./nix];}; inputs = { - nixpkgs.url = github:nixos/nixpkgs/nixos-unstable; - systems.url = github:nix-systems/default; - flake-parts.url = github:hercules-ci/flake-parts; - flake-compat.url = github:edolstra/flake-compat; + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + systems.url = "github:nix-systems/default"; + flake-parts.url = "github:hercules-ci/flake-parts"; + flake-compat.url = "github:edolstra/flake-compat"; flake-compat.flake = false; - crane.url = github:ipetkov/crane; + crane.url = "github:ipetkov/crane"; }; } From fd481027e34b078a78d06c0fbe85cebcd9184715 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 14 Mar 2025 20:43:29 +0100 Subject: [PATCH 382/540] Fix zombie processes being left over after closing Settings UI --- CHANGELOG.md | 8 ++- Cargo.lock | 2 +- rust/common/Cargo.toml | 3 ++ rust/common/src/detached_process.rs | 44 ++++++++++++++++ rust/common/src/lib.rs | 1 + rust/plugin_runtime/Cargo.toml | 3 -- .../src/plugins/applications.rs | 51 +++---------------- .../src/plugins/applications/linux/mod.rs | 6 ++- rust/plugin_runtime/src/plugins/settings.rs | 3 +- rust/server/src/plugins/mod.rs | 5 +- 10 files changed, 73 insertions(+), 53 deletions(-) create mode 100644 rust/common/src/detached_process.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4af99fe..941e8b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project doesn't adhere to Semantic Versioning, see [Versioning](./READM For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://github.com/project-gauntlet/tools/blob/main/CHANGELOG.md) ## [Unreleased] +- Fixed crash when typing/clicking fast in React-created plugin ui +- Fixed events sometimes overwriting other parallel events when typing/clicking fast in React-created plugin ui + - Fixes mouse clicks sometimes being ignored in high refresh rate views + - Fixes keystrokes being rewritten/ignored when holding keys in input fields +- Fixed crash when closing inline view due to action being run +- Fixed zombie processes being left over after closing Settings UI ## [16] - 2025-02-23 @@ -628,4 +634,4 @@ Themes are versioned and only one version is supported at the same time by appli ### Added -- Initial release. \ No newline at end of file +- Initial release. diff --git a/Cargo.lock b/Cargo.lock index 976ed4f..2b48fe6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4030,6 +4030,7 @@ dependencies = [ "gix-url", "indexmap 2.7.0", "itertools 0.13.0", + "libc", "prost", "serde", "serde_json", @@ -4097,7 +4098,6 @@ dependencies = [ "image 0.25.5", "indexmap 2.7.0", "interprocess", - "libc", "numbat", "objc2", "objc2-app-kit", diff --git a/rust/common/Cargo.toml b/rust/common/Cargo.toml index 7318e97..a6df4b7 100644 --- a/rust/common/Cargo.toml +++ b/rust/common/Cargo.toml @@ -22,6 +22,9 @@ gix-url = { version = "0.28.1" } base64 = "0.22" directories = "5.0" +[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] +libc = "0.2" + [build-dependencies] # workspaces gauntlet-component-model.workspace = true diff --git a/rust/common/src/detached_process.rs b/rust/common/src/detached_process.rs new file mode 100644 index 0000000..d3197d6 --- /dev/null +++ b/rust/common/src/detached_process.rs @@ -0,0 +1,44 @@ +use std::io; +use std::process::Command; +use std::process::Stdio; + +pub trait CommandExt { + fn spawn_detached(&mut self) -> io::Result<()>; +} + +impl CommandExt for Command { + fn spawn_detached(&mut self) -> io::Result<()> { + // This is pretty much lifted from the implementation in Alacritty: + // https://github.com/alacritty/alacritty/blob/b9c886872d1202fc9302f68a0bedbb17daa35335/alacritty/src/daemon.rs + + self.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); + + #[cfg(unix)] + unsafe { + use std::os::unix::process::CommandExt as _; + + self.pre_exec(move || { + match libc::fork() { + -1 => return Err(io::Error::last_os_error()), + 0 => (), + _ => libc::_exit(0), + } + + if libc::setsid() == -1 { + return Err(io::Error::last_os_error()); + } + + Ok(()) + }); + } + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; + const CREATE_NO_WINDOW: u32 = 0x08000000; + self.creation_flags(CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW); + } + + self.spawn().map(|_| ()) + } +} diff --git a/rust/common/src/lib.rs b/rust/common/src/lib.rs index 3c2f781..b6d78f4 100644 --- a/rust/common/src/lib.rs +++ b/rust/common/src/lib.rs @@ -1,6 +1,7 @@ use serde::Deserialize; use serde::Serialize; +pub mod detached_process; pub mod dirs; pub mod model; pub mod rpc; diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml index 3347427..53e1099 100644 --- a/rust/plugin_runtime/Cargo.toml +++ b/rust/plugin_runtime/Cargo.toml @@ -36,9 +36,6 @@ uuid = "1.11.0" open = "5" sys-locale = "0.3.2" -[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] -libc = "0.2" - [target.'cfg(target_os = "linux")'.dependencies] freedesktop_entry_parser = "1.3" freedesktop-icons = "0.2" diff --git a/rust/plugin_runtime/src/plugins/applications.rs b/rust/plugin_runtime/src/plugins/applications.rs index 3dc7818..264622b 100644 --- a/rust/plugin_runtime/src/plugins/applications.rs +++ b/rust/plugin_runtime/src/plugins/applications.rs @@ -5,6 +5,7 @@ use std::rc::Rc; use anyhow::anyhow; use deno_core::op2; use deno_core::OpState; +use gauntlet_common::detached_process::CommandExt; use image::imageops::FilterType; use image::ImageFormat; use serde::Deserialize; @@ -181,7 +182,7 @@ pub fn macos_application_dirs() -> Vec { #[cfg(target_os = "macos")] #[op2(fast)] pub fn macos_open_application(#[string] app_path: String) -> anyhow::Result<()> { - spawn_detached("open", &[app_path])?; + std::process::Command::new("open").args([app_path]).spawn_detached()?; Ok(()) } @@ -203,7 +204,9 @@ pub fn macos_settings_13_and_post(#[string] lang: Option) -> Vec anyhow::Result<()> { - spawn_detached("open", &[format!("x-apple.systempreferences:{}", preferences_id)])?; + std::process::Command::new("open") + .args([format!("x-apple.systempreferences:{}", preferences_id)]) + .spawn_detached()?; Ok(()) } @@ -211,7 +214,9 @@ pub fn macos_open_setting_13_and_post(#[string] preferences_id: String) -> anyho #[cfg(target_os = "macos")] #[op2(fast)] pub fn macos_open_setting_pre_13(#[string] setting_path: String) -> anyhow::Result<()> { - spawn_detached("open", &["-b", "com.apple.systempreferences", &setting_path])?; + std::process::Command::new("open") + .args(["-b", "com.apple.systempreferences", &setting_path]) + .spawn_detached()?; Ok(()) } @@ -227,46 +232,6 @@ pub fn macos_get_localized_language() -> Option { .map(|s| s.to_string()) } -#[cfg(unix)] -pub fn spawn_detached(path: &str, args: I) -> std::io::Result<()> -where - I: IntoIterator + Copy, - S: AsRef, -{ - // from https://github.com/alacritty/alacritty/blob/5abb4b73937b17fe501b9ca20b602950f1218b96/alacritty/src/daemon.rs#L65 - use std::os::unix::prelude::CommandExt; - use std::process::Command; - use std::process::Stdio; - - let mut command = Command::new(path); - - command - .args(args) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - - unsafe { - command - .pre_exec(|| { - match libc::fork() { - -1 => return Err(std::io::Error::last_os_error()), - 0 => (), - _ => libc::_exit(0), - } - - if libc::setsid() == -1 { - return Err(std::io::Error::last_os_error()); - } - - Ok(()) - }) - .spawn()? - .wait() - .map(|_| ()) - } -} - pub(in crate::plugins::applications) fn resize_icon(data: Vec) -> anyhow::Result> { let data = image::load_from_memory_with_format(&data, ImageFormat::Png)?; let data = image::imageops::resize(&data, 48, 48, FilterType::Lanczos3); diff --git a/rust/plugin_runtime/src/plugins/applications/linux/mod.rs b/rust/plugin_runtime/src/plugins/applications/linux/mod.rs index 5880301..2e1f195 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/mod.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/mod.rs @@ -10,6 +10,7 @@ use deno_core::op2; use deno_core::OpState; use freedesktop_entry_parser::parse_entry; use freedesktop_icons::lookup; +use gauntlet_common::detached_process::CommandExt; use image::imageops::FilterType; use image::ImageFormat; use tokio::sync::mpsc::Sender; @@ -19,7 +20,6 @@ use walkdir::WalkDir; use crate::plugin_data::PluginData; use crate::plugins::applications::linux; use crate::plugins::applications::resize_icon; -use crate::plugins::applications::spawn_detached; use crate::plugins::applications::DesktopApplication; use crate::plugins::applications::DesktopPathAction; @@ -106,7 +106,9 @@ fn linux_application_dirs(state: Rc>) -> Vec { #[op2(fast)] fn linux_open_application(#[string] desktop_file_id: String) -> anyhow::Result<()> { - spawn_detached("gtk-launch", &[desktop_file_id])?; + std::process::Command::new("gtk-launch") + .args([desktop_file_id]) + .spawn_detached()?; Ok(()) } diff --git a/rust/plugin_runtime/src/plugins/settings.rs b/rust/plugin_runtime/src/plugins/settings.rs index ddac7cd..7a2a1d8 100644 --- a/rust/plugin_runtime/src/plugins/settings.rs +++ b/rust/plugin_runtime/src/plugins/settings.rs @@ -1,10 +1,11 @@ use deno_core::op2; +use gauntlet_common::detached_process::CommandExt; #[op2(fast)] pub fn open_settings() -> anyhow::Result<()> { std::process::Command::new(std::env::current_exe()?) .args(["settings"]) - .spawn()?; + .spawn_detached()?; Ok(()) } diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 8538a46..b2acd60 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -7,6 +7,7 @@ use std::time::Duration; use anyhow::anyhow; use anyhow::Context; +use gauntlet_common::detached_process::CommandExt; use gauntlet_common::dirs::Dirs; use gauntlet_common::model::DownloadStatus; use gauntlet_common::model::EntrypointId; @@ -734,7 +735,7 @@ impl ApplicationManager { std::process::Command::new(current_exe) .args(["settings"]) - .spawn() + .spawn_detached() .expect("failed to execute settings process"); } @@ -755,7 +756,7 @@ impl ApplicationManager { std::process::Command::new(current_exe) .args(["settings"]) .env(SETTINGS_ENV, settings_env_data_to_string(data)) - .spawn() + .spawn_detached() .expect("failed to execute settings process"); // this can fail in dev if binary was replaced by more recent compilation } From 9dfab9d537e024cc8967f8356a02a42ca9e9beac Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 14 Mar 2025 21:45:11 +0100 Subject: [PATCH 383/540] Better process cleanup on linux id server process is killed --- CHANGELOG.md | 4 ++-- Cargo.lock | 1 + rust/plugin_runtime/Cargo.toml | 3 +++ rust/plugin_runtime/src/lib.rs | 5 +++++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 941e8b3..6d65ba6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git - Fixed crash when typing/clicking fast in React-created plugin ui - Fixed events sometimes overwriting other parallel events when typing/clicking fast in React-created plugin ui - Fixes mouse clicks sometimes being ignored in high refresh rate views - - Fixes keystrokes being rewritten/ignored when holding keys in input fields -- Fixed crash when closing inline view due to action being run + - Fixes keystrokes being rewritten/ignored when holding down keys or typing fast in input fields +- Fixed crash when closing inline view due to Action being run - Fixed zombie processes being left over after closing Settings UI ## [16] - 2025-02-23 diff --git a/Cargo.lock b/Cargo.lock index 2b48fe6..f079ca5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4098,6 +4098,7 @@ dependencies = [ "image 0.25.5", "indexmap 2.7.0", "interprocess", + "libc", "numbat", "objc2", "objc2-app-kit", diff --git a/rust/plugin_runtime/Cargo.toml b/rust/plugin_runtime/Cargo.toml index 53e1099..599db9e 100644 --- a/rust/plugin_runtime/Cargo.toml +++ b/rust/plugin_runtime/Cargo.toml @@ -57,6 +57,9 @@ objc2 = "0.5.2" [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.58.0", features = ["Win32_Storage_FileSystem", "Win32_UI_WindowsAndMessaging", "Win32_UI_Shell", "Win32_UI_Controls"] } +[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] +libc = "0.2" + [features] scenario_runner = [] release = [] diff --git a/rust/plugin_runtime/src/lib.rs b/rust/plugin_runtime/src/lib.rs index 5dfbf3c..7ba8f2e 100644 --- a/rust/plugin_runtime/src/lib.rs +++ b/rust/plugin_runtime/src/lib.rs @@ -67,6 +67,11 @@ use crate::api::BackendForPluginRuntimeApiProxy; use crate::deno::start_js_runtime; pub fn run_plugin_runtime(socket_name: String) { + #[cfg(target_os = "linux")] + unsafe { + libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL); + + tokio::runtime::Builder::new_current_thread() .enable_all() .build() From 93dbba1def55c62476e7c9a4bb00d47616e69ed6 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 14 Mar 2025 21:46:42 +0100 Subject: [PATCH 384/540] Fix typo --- rust/plugin_runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/plugin_runtime/src/lib.rs b/rust/plugin_runtime/src/lib.rs index 7ba8f2e..0ee10e2 100644 --- a/rust/plugin_runtime/src/lib.rs +++ b/rust/plugin_runtime/src/lib.rs @@ -70,7 +70,7 @@ pub fn run_plugin_runtime(socket_name: String) { #[cfg(target_os = "linux")] unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL); - + } tokio::runtime::Builder::new_current_thread() .enable_all() From 704b466b209903970e32b40a94334723b88c36ea Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 15 Mar 2025 07:19:55 +0000 Subject: [PATCH 385/540] Prepare for v17 release --- CHANGELOG.md | 2 ++ VERSION | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d65ba6..8090ebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project doesn't adhere to Semantic Versioning, see [Versioning](./READM For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://github.com/project-gauntlet/tools/blob/main/CHANGELOG.md) ## [Unreleased] + +## [17] - 2025-03-15 - Fixed crash when typing/clicking fast in React-created plugin ui - Fixed events sometimes overwriting other parallel events when typing/clicking fast in React-created plugin ui - Fixes mouse clicks sometimes being ignored in high refresh rate views diff --git a/VERSION b/VERSION index 19c7bdb..8e2afd3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -16 \ No newline at end of file +17 \ No newline at end of file From 81c21e9ab66a1661a9b9795193bd7c0d36d4225d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Mar 2025 08:52:53 +0100 Subject: [PATCH 386/540] Bump version in nix overlay --- nix/overlay.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/overlay.nix b/nix/overlay.nix index 2774817..0918940 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -5,7 +5,7 @@ }: { flake.overlays.default = final: _: let # These must be updated following the instructions in ./nix/README.md when dependencies are updated or the version bumped - version = "v14"; + version = "v17"; npmDepsHash = "sha256-rk5aLdXoSpqMNcSVIRJTKN1KqtddVPiKkZ1YWZ+n5m8="; RUSTY_V8_ARCHIVE = fetchRustyV8 "130.0.2" { aarch64-darwin = "sha256-aWZ/4Q4Wttx37xOdBmTCPGP+eYGhr4CM1UkYq8pC7Qs="; From 4f715674c0fc227b4c0f8ab9e9d02787f3f3dffb Mon Sep 17 00:00:00 2001 From: Benno <107504783+BennoCrafter@users.noreply.github.com> Date: Sat, 15 Mar 2025 19:32:13 +0100 Subject: [PATCH 387/540] Plugin Manifest schema (#62) Co-authored-by: Exidex <16986685+exidex@users.noreply.github.com> --- Cargo.lock | 45 + Cargo.toml | 1 + docs/schema/plugin_manifest.schema.json | 788 ++++++++++++++++++ rust/manifest_schema/Cargo.toml | 14 + rust/manifest_schema/src/main.rs | 14 + rust/server/Cargo.toml | 1 + rust/server/src/lib.rs | 2 +- rust/server/src/plugins/data_db_repository.rs | 2 +- rust/server/src/plugins/loader.rs | 494 +---------- rust/server/src/plugins/mod.rs | 1 + rust/server/src/plugins/plugin_manifest.rs | 553 ++++++++++++ 11 files changed, 1420 insertions(+), 495 deletions(-) create mode 100644 docs/schema/plugin_manifest.schema.json create mode 100644 rust/manifest_schema/Cargo.toml create mode 100644 rust/manifest_schema/src/main.rs create mode 100644 rust/server/src/plugins/plugin_manifest.rs diff --git a/Cargo.lock b/Cargo.lock index f079ca5..555a3bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4076,6 +4076,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "gauntlet-manifest-schema" +version = "0.0.0" +dependencies = [ + "gauntlet-server", + "schemars", + "serde_json", +] + [[package]] name = "gauntlet-plugin-runtime" version = "0.1.0" @@ -4158,6 +4167,7 @@ dependencies = [ "once_cell", "open", "regex", + "schemars", "serde", "sqlx", "tantivy", @@ -8960,6 +8970,30 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.90", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -9104,6 +9138,17 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "serde_json" version = "1.0.133" diff --git a/Cargo.toml b/Cargo.toml index a467fec..ad4095d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "rust/component_model", "rust/scenario_runner", "rust/plugin_runtime", + "rust/manifest_schema", ] [workspace.package] edition = "2021" diff --git a/docs/schema/plugin_manifest.schema.json b/docs/schema/plugin_manifest.schema.json new file mode 100644 index 0000000..a6cacdb --- /dev/null +++ b/docs/schema/plugin_manifest.schema.json @@ -0,0 +1,788 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PluginManifest", + "description": "Manifest structure for a plugin.", + "type": "object", + "required": [ + "entrypoint", + "gauntlet" + ], + "properties": { + "$schema": { + "type": [ + "string", + "null" + ] + }, + "entrypoint": { + "description": "Entrypoints for the plugin.", + "type": "array", + "items": { + "$ref": "#/definitions/PluginManifestEntrypoint" + } + }, + "gauntlet": { + "description": "Metadata about the plugin.", + "allOf": [ + { + "$ref": "#/definitions/PluginManifestMetadata" + } + ] + }, + "permissions": { + "description": "Permissions required by the plugin.", + "default": { + "environment": [], + "network": [], + "filesystem": { + "read": [], + "write": [] + }, + "exec": { + "command": [], + "executable": [] + }, + "system": [], + "clipboard": [], + "main_search_bar": [] + }, + "allOf": [ + { + "$ref": "#/definitions/PluginManifestPermissions" + } + ] + }, + "preferences": { + "description": "Preferences that can be configured by the user in the settings view.", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/PluginManifestPreference" + } + }, + "supported_system": { + "description": "List of supported operating systems.", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/PluginManifestSupportedSystem" + } + } + }, + "definitions": { + "PluginManifestAction": { + "description": "Action that can be performed by the plugin.", + "type": "object", + "required": [ + "description", + "id", + "shortcut" + ], + "properties": { + "description": { + "description": "Description of what the action does.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the action.", + "type": "string" + }, + "shortcut": { + "description": "Keyboard shortcut to trigger the action.", + "allOf": [ + { + "$ref": "#/definitions/PluginManifestActionShortcut" + } + ] + } + } + }, + "PluginManifestActionShortcut": { + "description": "Keyboard shortcut configuration for a plugin action.", + "type": "object", + "required": [ + "key", + "kind" + ], + "properties": { + "key": { + "description": "The key to be pressed for this shortcut.", + "allOf": [ + { + "$ref": "#/definitions/PluginManifestActionShortcutKey" + } + ] + }, + "kind": { + "description": "The type of shortcut.", + "allOf": [ + { + "$ref": "#/definitions/PluginManifestActionShortcutKind" + } + ] + } + } + }, + "PluginManifestActionShortcutKey": { + "type": "string", + "enum": [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "!", + "@", + "#", + "$", + "%", + "^", + "&", + "*", + "(", + ")", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "-", + "=", + ",", + ".", + "/", + "[", + "]", + ";", + "'", + "\\", + "_", + "+", + "<", + ">", + "?", + "{", + "}", + ":", + "\"", + "|" + ] + }, + "PluginManifestActionShortcutKind": { + "description": "The type of shortcut.", + "oneOf": [ + { + "description": "Main shortcut for the action (e.g. cmd).", + "type": "string", + "enum": [ + "main" + ] + }, + { + "description": "Alternative shortcut for the action (e.g. opt).", + "type": "string", + "enum": [ + "alternative" + ] + } + ] + }, + "PluginManifestClipboardPermissions": { + "description": "Clipboard permissions for the plugin.", + "oneOf": [ + { + "description": "Allows the plugin to read from the clipboard.", + "type": "string", + "enum": [ + "read" + ] + }, + { + "description": "Allows the plugin to write to the clipboard.", + "type": "string", + "enum": [ + "write" + ] + }, + { + "description": "Allows the plugin to clear the clipboard contents.", + "type": "string", + "enum": [ + "clear" + ] + } + ] + }, + "PluginManifestEntrypoint": { + "description": "An entrypoint for the plugin.", + "type": "object", + "required": [ + "description", + "id", + "name", + "path", + "type" + ], + "properties": { + "actions": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/PluginManifestAction" + } + }, + "description": { + "type": "string" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "preferences": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/PluginManifestPreference" + } + }, + "type": { + "$ref": "#/definitions/PluginManifestEntrypointTypes" + } + } + }, + "PluginManifestEntrypointTypes": { + "description": "Types of plugin entrypoints.", + "oneOf": [ + { + "description": "A command entrypoint.", + "type": "string", + "enum": [ + "command" + ] + }, + { + "description": "A view-based entrypoint.", + "type": "string", + "enum": [ + "view" + ] + }, + { + "description": "An inline view entrypoint.", + "type": "string", + "enum": [ + "inline-view" + ] + }, + { + "description": "Generates new entrypoints dynamically.", + "type": "string", + "enum": [ + "entrypoint-generator" + ] + } + ] + }, + "PluginManifestMainSearchBarPermissions": { + "oneOf": [ + { + "description": "Allows the plugin to read the main search bar", + "type": "string", + "enum": [ + "read" + ] + } + ] + }, + "PluginManifestMetadata": { + "description": "Metadata for the plugin manifest.", + "type": "object", + "required": [ + "description", + "name" + ], + "properties": { + "description": { + "description": "Description of the plugin.", + "type": "string" + }, + "name": { + "description": "Name of the plugin.", + "type": "string" + } + } + }, + "PluginManifestPermissions": { + "description": "Permissions required by the plugin.", + "type": "object", + "properties": { + "clipboard": { + "description": "Clipboard permissions for the plugin.", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/PluginManifestClipboardPermissions" + } + }, + "environment": { + "description": "Environment variables that the plugin can access.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "exec": { + "description": "Execution permissions for the plugin.", + "default": { + "command": [], + "executable": [] + }, + "allOf": [ + { + "$ref": "#/definitions/PluginManifestPermissionsExec" + } + ] + }, + "filesystem": { + "description": "Filesystem permissions for the plugin.", + "default": { + "read": [], + "write": [] + }, + "allOf": [ + { + "$ref": "#/definitions/PluginManifestPermissionsFileSystem" + } + ] + }, + "main_search_bar": { + "description": "Permissions for the main search bar.", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/PluginManifestMainSearchBarPermissions" + } + }, + "network": { + "description": "Network domains that the plugin can access.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "system": { + "description": "System permissions for the plugin.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PluginManifestPermissionsExec": { + "description": "Execution permissions for the plugin.", + "type": "object", + "properties": { + "command": { + "description": "Commands that the plugin can execute.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "executable": { + "description": "Executables that the plugin can run.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PluginManifestPermissionsFileSystem": { + "description": "Filesystem permissions for the plugin.", + "type": "object", + "properties": { + "read": { + "description": "Paths that the plugin can read from.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "write": { + "description": "Paths that the plugin can write to.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PluginManifestPreference": { + "description": "User-configurable preference options.", + "oneOf": [ + { + "description": "A numeric preference.", + "type": "object", + "required": [ + "description", + "id", + "name", + "type" + ], + "properties": { + "default": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "number" + ] + } + } + }, + { + "description": "A string preference.", + "type": "object", + "required": [ + "description", + "id", + "name", + "type" + ], + "properties": { + "default": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "string" + ] + } + } + }, + { + "description": "An enum preference with selectable values.", + "type": "object", + "required": [ + "description", + "enum_values", + "id", + "name", + "type" + ], + "properties": { + "default": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "enum_values": { + "type": "array", + "items": { + "$ref": "#/definitions/PluginManifestPreferenceEnumValue" + } + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "enum" + ] + } + } + }, + { + "description": "A boolean preference.", + "type": "object", + "required": [ + "description", + "id", + "name", + "type" + ], + "properties": { + "default": { + "type": [ + "boolean", + "null" + ] + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "bool" + ] + } + } + }, + { + "description": "A list of strings preference.", + "type": "object", + "required": [ + "description", + "id", + "name", + "type" + ], + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "list_of_strings" + ] + } + } + }, + { + "description": "A list of numbers preference.", + "type": "object", + "required": [ + "description", + "id", + "name", + "type" + ], + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "list_of_numbers" + ] + } + } + }, + { + "description": "A list of enumerated preference values.", + "type": "object", + "required": [ + "description", + "enum_values", + "id", + "name", + "type" + ], + "properties": { + "description": { + "type": "string" + }, + "enum_values": { + "type": "array", + "items": { + "$ref": "#/definitions/PluginManifestPreferenceEnumValue" + } + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "list_of_enums" + ] + } + } + } + ] + }, + "PluginManifestPreferenceEnumValue": { + "description": "An enumerated preference value.", + "type": "object", + "required": [ + "label", + "value" + ], + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "PluginManifestSupportedSystem": { + "oneOf": [ + { + "type": "object", + "required": [ + "os" + ], + "properties": { + "os": { + "type": "string", + "enum": [ + "linux" + ] + } + } + }, + { + "type": "object", + "required": [ + "os" + ], + "properties": { + "os": { + "type": "string", + "enum": [ + "windows" + ] + } + } + }, + { + "type": "object", + "required": [ + "os" + ], + "properties": { + "os": { + "type": "string", + "enum": [ + "macos" + ] + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/rust/manifest_schema/Cargo.toml b/rust/manifest_schema/Cargo.toml new file mode 100644 index 0000000..4090883 --- /dev/null +++ b/rust/manifest_schema/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "gauntlet-manifest-schema" +edition.workspace = true + +[dependencies] +# workspaces +gauntlet-server.workspace = true + +# other +schemars = "0.8" +serde_json = "1.0" + +[features] +release = ["gauntlet-server/release"] diff --git a/rust/manifest_schema/src/main.rs b/rust/manifest_schema/src/main.rs new file mode 100644 index 0000000..7ffbc1c --- /dev/null +++ b/rust/manifest_schema/src/main.rs @@ -0,0 +1,14 @@ +use std::io::Write; + +use gauntlet_server::plugins::plugin_manifest::PluginManifest; +use schemars::schema_for; + +fn main() { + let schema = schema_for!(PluginManifest); + let json = serde_json::to_string_pretty(&schema).unwrap(); + + std::fs::create_dir_all("../../docs/schema").expect("Failed to create directory"); + std::fs::write("../../docs/schema/plugin_manifest.schema.json", json.as_bytes()).expect("Failed to write schema"); + + println!("Schema generated and saved to schema.json"); +} diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 52349af..fb0b56e 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -41,6 +41,7 @@ url = "2.5" ureq = "2.10" vergen-pretty = "0.3" dark-light = "1.1.1" +schemars = "0.8" [features] release = ["gauntlet-common/release", "gauntlet-plugin-runtime/release"] diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index a7b75f5..6eb0b69 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -35,7 +35,7 @@ use crate::rpc::BackendServerImpl; use crate::search::SearchIndex; pub(crate) mod model; -pub(crate) mod plugins; +pub mod plugins; pub mod rpc; pub(crate) mod search; diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 2795cba..9d1144d 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -29,7 +29,7 @@ use uuid::Uuid; use crate::model::ActionShortcutKey; use crate::plugins::frecency::FrecencyItemStats; use crate::plugins::frecency::FrecencyMetaParams; -use crate::plugins::loader::PluginManifestActionShortcutKey; +use crate::plugins::plugin_manifest::PluginManifestActionShortcutKey; static MIGRATOR: Migrator = sqlx::migrate!("./db_migrations"); diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index af56075..ddce67a 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -46,6 +46,7 @@ use crate::plugins::data_db_repository::DbWritePlugin; use crate::plugins::data_db_repository::DbWritePluginAssetData; use crate::plugins::data_db_repository::DbWritePluginEntrypoint; use crate::plugins::download_status::DownloadStatusHolder; +use crate::plugins::plugin_manifest::*; pub struct PluginLoader { db_repository: DataDbRepository, @@ -894,496 +895,3 @@ struct PluginDownloadData { pub preferences: HashMap, pub preferences_user_data: HashMap, } - -#[derive(Debug, Deserialize)] -struct PluginManifest { - gauntlet: PluginManifestMetadata, - entrypoint: Vec, - #[serde(default)] - supported_system: Vec, - #[serde(default)] - permissions: PluginManifestPermissions, - #[serde(default)] - preferences: Vec, -} - -#[derive(Debug, Deserialize)] -struct PluginManifestEntrypoint { - id: String, - name: String, - description: String, - #[allow(unused)] // used when building plugin - path: String, - icon: Option, - #[serde(rename = "type")] - entrypoint_type: PluginManifestEntrypointTypes, - #[serde(default)] - preferences: Vec, - #[serde(default)] - actions: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(tag = "type")] -enum PluginManifestPreference { - #[serde(rename = "number")] - Number { - id: String, - name: String, - default: Option, - description: String, - }, - #[serde(rename = "string")] - String { - id: String, - name: String, - default: Option, - description: String, - }, - #[serde(rename = "enum")] - Enum { - id: String, - name: String, - default: Option, - description: String, - enum_values: Vec, - }, - #[serde(rename = "bool")] - Bool { - id: String, - name: String, - default: Option, - description: String, - }, - #[serde(rename = "list_of_strings")] - ListOfStrings { - id: String, - name: String, - // default: Option>, - description: String, - }, - #[serde(rename = "list_of_numbers")] - ListOfNumbers { - id: String, - name: String, - // default: Option>, - description: String, - }, - #[serde(rename = "list_of_enums")] - ListOfEnums { - id: String, - name: String, - // default: Option>, - enum_values: Vec, - description: String, - }, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct PluginManifestPreferenceEnumValue { - pub label: String, - pub value: String, -} - -#[derive(Debug, Deserialize)] -pub enum PluginManifestEntrypointTypes { - #[serde(rename = "command")] - Command, - #[serde(rename = "view")] - View, - #[serde(rename = "inline-view")] - InlineView, - #[serde(rename = "entrypoint-generator")] - EntrypointGenerator, -} - -#[derive(Debug, Deserialize)] -pub struct PluginManifestAction { - id: String, - description: String, - shortcut: PluginManifestActionShortcut, -} - -#[derive(Debug, Deserialize)] -pub struct PluginManifestActionShortcut { - key: PluginManifestActionShortcutKey, - kind: PluginManifestActionShortcutKind, -} - -// only stuff that is present on 60% keyboard -#[derive(Debug, Deserialize)] -pub enum PluginManifestActionShortcutKey { - #[serde(rename = "0")] - Num0, - #[serde(rename = "1")] - Num1, - #[serde(rename = "2")] - Num2, - #[serde(rename = "3")] - Num3, - #[serde(rename = "4")] - Num4, - #[serde(rename = "5")] - Num5, - #[serde(rename = "6")] - Num6, - #[serde(rename = "7")] - Num7, - #[serde(rename = "8")] - Num8, - #[serde(rename = "9")] - Num9, - - #[serde(rename = "!")] - Exclamation, - #[serde(rename = "@")] - AtSign, - #[serde(rename = "#")] - Hash, - #[serde(rename = "$")] - Dollar, - #[serde(rename = "%")] - Percent, - #[serde(rename = "^")] - Caret, - #[serde(rename = "&")] - Ampersand, - #[serde(rename = "*")] - Star, - #[serde(rename = "(")] - LeftParenthesis, - #[serde(rename = ")")] - RightParenthesis, - - #[serde(rename = "a")] - LowerA, - #[serde(rename = "b")] - LowerB, - #[serde(rename = "c")] - LowerC, - #[serde(rename = "d")] - LowerD, - #[serde(rename = "e")] - LowerE, - #[serde(rename = "f")] - LowerF, - #[serde(rename = "g")] - LowerG, - #[serde(rename = "h")] - LowerH, - #[serde(rename = "i")] - LowerI, - #[serde(rename = "j")] - LowerJ, - #[serde(rename = "k")] - LowerK, - #[serde(rename = "l")] - LowerL, - #[serde(rename = "m")] - LowerM, - #[serde(rename = "n")] - LowerN, - #[serde(rename = "o")] - LowerO, - #[serde(rename = "p")] - LowerP, - #[serde(rename = "q")] - LowerQ, - #[serde(rename = "r")] - LowerR, - #[serde(rename = "s")] - LowerS, - #[serde(rename = "t")] - LowerT, - #[serde(rename = "u")] - LowerU, - #[serde(rename = "v")] - LowerV, - #[serde(rename = "w")] - LowerW, - #[serde(rename = "x")] - LowerX, - #[serde(rename = "y")] - LowerY, - #[serde(rename = "z")] - LowerZ, - - #[serde(rename = "A")] - UpperA, - #[serde(rename = "B")] - UpperB, - #[serde(rename = "C")] - UpperC, - #[serde(rename = "D")] - UpperD, - #[serde(rename = "E")] - UpperE, - #[serde(rename = "F")] - UpperF, - #[serde(rename = "G")] - UpperG, - #[serde(rename = "H")] - UpperH, - #[serde(rename = "I")] - UpperI, - #[serde(rename = "J")] - UpperJ, - #[serde(rename = "K")] - UpperK, - #[serde(rename = "L")] - UpperL, - #[serde(rename = "M")] - UpperM, - #[serde(rename = "N")] - UpperN, - #[serde(rename = "O")] - UpperO, - #[serde(rename = "P")] - UpperP, - #[serde(rename = "Q")] - UpperQ, - #[serde(rename = "R")] - UpperR, - #[serde(rename = "S")] - UpperS, - #[serde(rename = "T")] - UpperT, - #[serde(rename = "U")] - UpperU, - #[serde(rename = "V")] - UpperV, - #[serde(rename = "W")] - UpperW, - #[serde(rename = "X")] - UpperX, - #[serde(rename = "Y")] - UpperY, - #[serde(rename = "Z")] - UpperZ, - - #[serde(rename = "-")] - Minus, - #[serde(rename = "=")] - Equals, - #[serde(rename = ",")] - Comma, - #[serde(rename = ".")] - Dot, - #[serde(rename = "/")] - Slash, - #[serde(rename = "[")] - OpenSquareBracket, - #[serde(rename = "]")] - CloseSquareBracket, - #[serde(rename = ";")] - Semicolon, - #[serde(rename = "'")] - Quote, - #[serde(rename = "\\")] - Backslash, - - #[serde(rename = "_")] - Underscore, - #[serde(rename = "+")] - Plus, - #[serde(rename = "<")] - LessThan, - #[serde(rename = ">")] - GreaterThan, - #[serde(rename = "?")] - QuestionMark, - #[serde(rename = "{")] - LeftBrace, - #[serde(rename = "}")] - RightBrace, - #[serde(rename = ":")] - Colon, - #[serde(rename = "\"")] - DoubleQuotes, - #[serde(rename = "|")] - Pipe, -} - -impl PluginManifestActionShortcutKey { - pub fn to_model(self) -> ActionShortcutKey { - match self { - PluginManifestActionShortcutKey::Num0 => ActionShortcutKey::Num0, - PluginManifestActionShortcutKey::Num1 => ActionShortcutKey::Num1, - PluginManifestActionShortcutKey::Num2 => ActionShortcutKey::Num2, - PluginManifestActionShortcutKey::Num3 => ActionShortcutKey::Num3, - PluginManifestActionShortcutKey::Num4 => ActionShortcutKey::Num4, - PluginManifestActionShortcutKey::Num5 => ActionShortcutKey::Num5, - PluginManifestActionShortcutKey::Num6 => ActionShortcutKey::Num6, - PluginManifestActionShortcutKey::Num7 => ActionShortcutKey::Num7, - PluginManifestActionShortcutKey::Num8 => ActionShortcutKey::Num8, - PluginManifestActionShortcutKey::Num9 => ActionShortcutKey::Num9, - PluginManifestActionShortcutKey::Exclamation => ActionShortcutKey::Exclamation, - PluginManifestActionShortcutKey::AtSign => ActionShortcutKey::AtSign, - PluginManifestActionShortcutKey::Hash => ActionShortcutKey::Hash, - PluginManifestActionShortcutKey::Dollar => ActionShortcutKey::Dollar, - PluginManifestActionShortcutKey::Percent => ActionShortcutKey::Percent, - PluginManifestActionShortcutKey::Caret => ActionShortcutKey::Caret, - PluginManifestActionShortcutKey::Ampersand => ActionShortcutKey::Ampersand, - PluginManifestActionShortcutKey::Star => ActionShortcutKey::Star, - PluginManifestActionShortcutKey::LeftParenthesis => ActionShortcutKey::LeftParenthesis, - PluginManifestActionShortcutKey::RightParenthesis => ActionShortcutKey::RightParenthesis, - PluginManifestActionShortcutKey::LowerA => ActionShortcutKey::LowerA, - PluginManifestActionShortcutKey::LowerB => ActionShortcutKey::LowerB, - PluginManifestActionShortcutKey::LowerC => ActionShortcutKey::LowerC, - PluginManifestActionShortcutKey::LowerD => ActionShortcutKey::LowerD, - PluginManifestActionShortcutKey::LowerE => ActionShortcutKey::LowerE, - PluginManifestActionShortcutKey::LowerF => ActionShortcutKey::LowerF, - PluginManifestActionShortcutKey::LowerG => ActionShortcutKey::LowerG, - PluginManifestActionShortcutKey::LowerH => ActionShortcutKey::LowerH, - PluginManifestActionShortcutKey::LowerI => ActionShortcutKey::LowerI, - PluginManifestActionShortcutKey::LowerJ => ActionShortcutKey::LowerJ, - PluginManifestActionShortcutKey::LowerK => ActionShortcutKey::LowerK, - PluginManifestActionShortcutKey::LowerL => ActionShortcutKey::LowerL, - PluginManifestActionShortcutKey::LowerM => ActionShortcutKey::LowerM, - PluginManifestActionShortcutKey::LowerN => ActionShortcutKey::LowerN, - PluginManifestActionShortcutKey::LowerO => ActionShortcutKey::LowerO, - PluginManifestActionShortcutKey::LowerP => ActionShortcutKey::LowerP, - PluginManifestActionShortcutKey::LowerQ => ActionShortcutKey::LowerQ, - PluginManifestActionShortcutKey::LowerR => ActionShortcutKey::LowerR, - PluginManifestActionShortcutKey::LowerS => ActionShortcutKey::LowerS, - PluginManifestActionShortcutKey::LowerT => ActionShortcutKey::LowerT, - PluginManifestActionShortcutKey::LowerU => ActionShortcutKey::LowerU, - PluginManifestActionShortcutKey::LowerV => ActionShortcutKey::LowerV, - PluginManifestActionShortcutKey::LowerW => ActionShortcutKey::LowerW, - PluginManifestActionShortcutKey::LowerX => ActionShortcutKey::LowerX, - PluginManifestActionShortcutKey::LowerY => ActionShortcutKey::LowerY, - PluginManifestActionShortcutKey::LowerZ => ActionShortcutKey::LowerZ, - PluginManifestActionShortcutKey::UpperA => ActionShortcutKey::UpperA, - PluginManifestActionShortcutKey::UpperB => ActionShortcutKey::UpperB, - PluginManifestActionShortcutKey::UpperC => ActionShortcutKey::UpperC, - PluginManifestActionShortcutKey::UpperD => ActionShortcutKey::UpperD, - PluginManifestActionShortcutKey::UpperE => ActionShortcutKey::UpperE, - PluginManifestActionShortcutKey::UpperF => ActionShortcutKey::UpperF, - PluginManifestActionShortcutKey::UpperG => ActionShortcutKey::UpperG, - PluginManifestActionShortcutKey::UpperH => ActionShortcutKey::UpperH, - PluginManifestActionShortcutKey::UpperI => ActionShortcutKey::UpperI, - PluginManifestActionShortcutKey::UpperJ => ActionShortcutKey::UpperJ, - PluginManifestActionShortcutKey::UpperK => ActionShortcutKey::UpperK, - PluginManifestActionShortcutKey::UpperL => ActionShortcutKey::UpperL, - PluginManifestActionShortcutKey::UpperM => ActionShortcutKey::UpperM, - PluginManifestActionShortcutKey::UpperN => ActionShortcutKey::UpperN, - PluginManifestActionShortcutKey::UpperO => ActionShortcutKey::UpperO, - PluginManifestActionShortcutKey::UpperP => ActionShortcutKey::UpperP, - PluginManifestActionShortcutKey::UpperQ => ActionShortcutKey::UpperQ, - PluginManifestActionShortcutKey::UpperR => ActionShortcutKey::UpperR, - PluginManifestActionShortcutKey::UpperS => ActionShortcutKey::UpperS, - PluginManifestActionShortcutKey::UpperT => ActionShortcutKey::UpperT, - PluginManifestActionShortcutKey::UpperU => ActionShortcutKey::UpperU, - PluginManifestActionShortcutKey::UpperV => ActionShortcutKey::UpperV, - PluginManifestActionShortcutKey::UpperW => ActionShortcutKey::UpperW, - PluginManifestActionShortcutKey::UpperX => ActionShortcutKey::UpperX, - PluginManifestActionShortcutKey::UpperY => ActionShortcutKey::UpperY, - PluginManifestActionShortcutKey::UpperZ => ActionShortcutKey::UpperZ, - PluginManifestActionShortcutKey::Minus => ActionShortcutKey::Minus, - PluginManifestActionShortcutKey::Equals => ActionShortcutKey::Equals, - PluginManifestActionShortcutKey::Comma => ActionShortcutKey::Comma, - PluginManifestActionShortcutKey::Dot => ActionShortcutKey::Dot, - PluginManifestActionShortcutKey::Slash => ActionShortcutKey::Slash, - PluginManifestActionShortcutKey::OpenSquareBracket => ActionShortcutKey::OpenSquareBracket, - PluginManifestActionShortcutKey::CloseSquareBracket => ActionShortcutKey::CloseSquareBracket, - PluginManifestActionShortcutKey::Semicolon => ActionShortcutKey::Semicolon, - PluginManifestActionShortcutKey::Quote => ActionShortcutKey::Quote, - PluginManifestActionShortcutKey::Backslash => ActionShortcutKey::Backslash, - PluginManifestActionShortcutKey::Underscore => ActionShortcutKey::Underscore, - PluginManifestActionShortcutKey::Plus => ActionShortcutKey::Plus, - PluginManifestActionShortcutKey::LessThan => ActionShortcutKey::LessThan, - PluginManifestActionShortcutKey::GreaterThan => ActionShortcutKey::GreaterThan, - PluginManifestActionShortcutKey::QuestionMark => ActionShortcutKey::QuestionMark, - PluginManifestActionShortcutKey::LeftBrace => ActionShortcutKey::LeftBrace, - PluginManifestActionShortcutKey::RightBrace => ActionShortcutKey::RightBrace, - PluginManifestActionShortcutKey::Colon => ActionShortcutKey::Colon, - PluginManifestActionShortcutKey::DoubleQuotes => ActionShortcutKey::DoubleQuotes, - PluginManifestActionShortcutKey::Pipe => ActionShortcutKey::Pipe, - } - } -} - -#[derive(Debug, Deserialize)] -pub enum PluginManifestActionShortcutKind { - #[serde(rename = "main")] - Main, - #[serde(rename = "alternative")] - Alternative, -} - -#[derive(Debug, Deserialize, PartialEq, Eq)] -#[serde(tag = "os")] -pub enum PluginManifestSupportedSystem { - #[serde(rename = "linux")] - Linux, - #[serde(rename = "windows")] - Windows, - #[serde(rename = "macos")] - MacOS, -} - -impl std::fmt::Display for PluginManifestSupportedSystem { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - PluginManifestSupportedSystem::Linux => write!(f, "Linux"), - PluginManifestSupportedSystem::Windows => write!(f, "Windows"), - PluginManifestSupportedSystem::MacOS => write!(f, "MacOS"), - } - } -} - -#[derive(Debug, Deserialize)] -struct PluginManifestMetadata { - name: String, - description: String, -} - -#[derive(Debug, Deserialize, Default)] -pub struct PluginManifestPermissions { - #[serde(default)] - environment: Vec, - #[serde(default)] - network: Vec, - #[serde(default)] - filesystem: PluginManifestPermissionsFileSystem, - #[serde(default)] - exec: PluginManifestPermissionsExec, - #[serde(default)] - system: Vec, - #[serde(default)] - clipboard: Vec, - #[serde(default)] - main_search_bar: Vec, -} - -#[derive(Debug, Deserialize, Default)] -pub struct PluginManifestPermissionsFileSystem { - #[serde(default)] - pub read: Vec, - #[serde(default)] - pub write: Vec, -} - -#[derive(Debug, Deserialize, Default)] -pub struct PluginManifestPermissionsExec { - #[serde(default)] - pub command: Vec, - #[serde(default)] - pub executable: Vec, -} - -#[derive(Debug, Deserialize)] -pub enum PluginManifestClipboardPermissions { - #[serde(rename = "read")] - Read, - #[serde(rename = "write")] - Write, - #[serde(rename = "clear")] - Clear, -} - -#[derive(Debug, Deserialize, Eq, PartialEq)] -pub enum PluginManifestMainSearchBarPermissions { - #[serde(rename = "read")] - Read, -} diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index b2acd60..f0e7b19 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -83,6 +83,7 @@ mod icon_cache; mod image_gatherer; pub mod js; mod loader; +pub mod plugin_manifest; mod run_status; mod runtime; mod settings; diff --git a/rust/server/src/plugins/plugin_manifest.rs b/rust/server/src/plugins/plugin_manifest.rs new file mode 100644 index 0000000..0665ca2 --- /dev/null +++ b/rust/server/src/plugins/plugin_manifest.rs @@ -0,0 +1,553 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; + +use crate::model::ActionShortcutKey; + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[schemars(description = "Manifest structure for a plugin.")] +pub struct PluginManifest { + #[serde(rename = "$schema")] + schema: Option, + #[schemars(description = "Metadata about the plugin.")] + pub gauntlet: PluginManifestMetadata, + #[schemars(description = "Entrypoints for the plugin.")] + pub entrypoint: Vec, + #[serde(default)] + #[schemars(description = "List of supported operating systems.")] + pub supported_system: Vec, + #[serde(default)] + #[schemars(description = "Permissions required by the plugin.")] + pub permissions: PluginManifestPermissions, + #[serde(default)] + #[schemars(description = "Preferences that can be configured by the user in the settings view.")] + pub preferences: Vec, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[schemars(description = "An entrypoint for the plugin.")] +pub struct PluginManifestEntrypoint { + pub id: String, + pub name: String, + pub description: String, + #[allow(unused)] // Used during plugin build + pub path: String, + pub icon: Option, + #[serde(rename = "type")] + pub entrypoint_type: PluginManifestEntrypointTypes, + #[serde(default)] + pub preferences: Vec, + #[serde(default)] + pub actions: Vec, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "type")] +#[schemars(description = "User-configurable preference options.")] +pub enum PluginManifestPreference { + #[serde(rename = "number")] + #[schemars(description = "A numeric preference.")] + Number { + id: String, + name: String, + default: Option, + description: String, + }, + #[serde(rename = "string")] + #[schemars(description = "A string preference.")] + String { + id: String, + name: String, + default: Option, + description: String, + }, + #[serde(rename = "enum")] + #[schemars(description = "An enum preference with selectable values.")] + Enum { + id: String, + name: String, + default: Option, + description: String, + enum_values: Vec, + }, + #[serde(rename = "bool")] + #[schemars(description = "A boolean preference.")] + Bool { + id: String, + name: String, + default: Option, + description: String, + }, + #[serde(rename = "list_of_strings")] + #[schemars(description = "A list of strings preference.")] + ListOfStrings { + id: String, + name: String, + // default: Option>, + description: String, + }, + #[serde(rename = "list_of_numbers")] + #[schemars(description = "A list of numbers preference.")] + ListOfNumbers { + id: String, + name: String, + // default: Option>, + description: String, + }, + #[serde(rename = "list_of_enums")] + #[schemars(description = "A list of enumerated preference values.")] + ListOfEnums { + id: String, + name: String, + // default: Option>, + enum_values: Vec, + description: String, + }, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[schemars(description = "An enumerated preference value.")] +pub struct PluginManifestPreferenceEnumValue { + pub label: String, + pub value: String, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[schemars(description = "Types of plugin entrypoints.")] +pub enum PluginManifestEntrypointTypes { + #[serde(rename = "command")] + #[schemars(description = "A command entrypoint.")] + Command, + #[serde(rename = "view")] + #[schemars(description = "A view-based entrypoint.")] + View, + #[serde(rename = "inline-view")] + #[schemars(description = "An inline view entrypoint.")] + InlineView, + #[serde(rename = "entrypoint-generator")] + #[schemars(description = "Generates new entrypoints dynamically.")] + EntrypointGenerator, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[schemars(description = "Action that can be performed by the plugin.")] +pub struct PluginManifestAction { + #[schemars(description = "Unique identifier for the action.")] + pub id: String, + #[schemars(description = "Description of what the action does.")] + pub description: String, + #[schemars(description = "Keyboard shortcut to trigger the action.")] + pub shortcut: PluginManifestActionShortcut, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[schemars(description = "Keyboard shortcut configuration for a plugin action.")] +pub struct PluginManifestActionShortcut { + #[schemars(description = "The key to be pressed for this shortcut.")] + pub key: PluginManifestActionShortcutKey, + #[schemars(description = "The type of shortcut.")] + pub kind: PluginManifestActionShortcutKind, +} + +// only stuff that is present on 60% keyboard +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub enum PluginManifestActionShortcutKey { + #[serde(rename = "0")] + Num0, + #[serde(rename = "1")] + Num1, + #[serde(rename = "2")] + Num2, + #[serde(rename = "3")] + Num3, + #[serde(rename = "4")] + Num4, + #[serde(rename = "5")] + Num5, + #[serde(rename = "6")] + Num6, + #[serde(rename = "7")] + Num7, + #[serde(rename = "8")] + Num8, + #[serde(rename = "9")] + Num9, + + #[serde(rename = "!")] + Exclamation, + #[serde(rename = "@")] + AtSign, + #[serde(rename = "#")] + Hash, + #[serde(rename = "$")] + Dollar, + #[serde(rename = "%")] + Percent, + #[serde(rename = "^")] + Caret, + #[serde(rename = "&")] + Ampersand, + #[serde(rename = "*")] + Star, + #[serde(rename = "(")] + LeftParenthesis, + #[serde(rename = ")")] + RightParenthesis, + + #[serde(rename = "a")] + LowerA, + #[serde(rename = "b")] + LowerB, + #[serde(rename = "c")] + LowerC, + #[serde(rename = "d")] + LowerD, + #[serde(rename = "e")] + LowerE, + #[serde(rename = "f")] + LowerF, + #[serde(rename = "g")] + LowerG, + #[serde(rename = "h")] + LowerH, + #[serde(rename = "i")] + LowerI, + #[serde(rename = "j")] + LowerJ, + #[serde(rename = "k")] + LowerK, + #[serde(rename = "l")] + LowerL, + #[serde(rename = "m")] + LowerM, + #[serde(rename = "n")] + LowerN, + #[serde(rename = "o")] + LowerO, + #[serde(rename = "p")] + LowerP, + #[serde(rename = "q")] + LowerQ, + #[serde(rename = "r")] + LowerR, + #[serde(rename = "s")] + LowerS, + #[serde(rename = "t")] + LowerT, + #[serde(rename = "u")] + LowerU, + #[serde(rename = "v")] + LowerV, + #[serde(rename = "w")] + LowerW, + #[serde(rename = "x")] + LowerX, + #[serde(rename = "y")] + LowerY, + #[serde(rename = "z")] + LowerZ, + + #[serde(rename = "A")] + UpperA, + #[serde(rename = "B")] + UpperB, + #[serde(rename = "C")] + UpperC, + #[serde(rename = "D")] + UpperD, + #[serde(rename = "E")] + UpperE, + #[serde(rename = "F")] + UpperF, + #[serde(rename = "G")] + UpperG, + #[serde(rename = "H")] + UpperH, + #[serde(rename = "I")] + UpperI, + #[serde(rename = "J")] + UpperJ, + #[serde(rename = "K")] + UpperK, + #[serde(rename = "L")] + UpperL, + #[serde(rename = "M")] + UpperM, + #[serde(rename = "N")] + UpperN, + #[serde(rename = "O")] + UpperO, + #[serde(rename = "P")] + UpperP, + #[serde(rename = "Q")] + UpperQ, + #[serde(rename = "R")] + UpperR, + #[serde(rename = "S")] + UpperS, + #[serde(rename = "T")] + UpperT, + #[serde(rename = "U")] + UpperU, + #[serde(rename = "V")] + UpperV, + #[serde(rename = "W")] + UpperW, + #[serde(rename = "X")] + UpperX, + #[serde(rename = "Y")] + UpperY, + #[serde(rename = "Z")] + UpperZ, + + #[serde(rename = "-")] + Minus, + #[serde(rename = "=")] + Equals, + #[serde(rename = ",")] + Comma, + #[serde(rename = ".")] + Dot, + #[serde(rename = "/")] + Slash, + #[serde(rename = "[")] + OpenSquareBracket, + #[serde(rename = "]")] + CloseSquareBracket, + #[serde(rename = ";")] + Semicolon, + #[serde(rename = "'")] + Quote, + #[serde(rename = "\\")] + Backslash, + + #[serde(rename = "_")] + Underscore, + #[serde(rename = "+")] + Plus, + #[serde(rename = "<")] + LessThan, + #[serde(rename = ">")] + GreaterThan, + #[serde(rename = "?")] + QuestionMark, + #[serde(rename = "{")] + LeftBrace, + #[serde(rename = "}")] + RightBrace, + #[serde(rename = ":")] + Colon, + #[serde(rename = "\"")] + DoubleQuotes, + #[serde(rename = "|")] + Pipe, +} + +impl PluginManifestActionShortcutKey { + pub fn to_model(self) -> ActionShortcutKey { + match self { + PluginManifestActionShortcutKey::Num0 => ActionShortcutKey::Num0, + PluginManifestActionShortcutKey::Num1 => ActionShortcutKey::Num1, + PluginManifestActionShortcutKey::Num2 => ActionShortcutKey::Num2, + PluginManifestActionShortcutKey::Num3 => ActionShortcutKey::Num3, + PluginManifestActionShortcutKey::Num4 => ActionShortcutKey::Num4, + PluginManifestActionShortcutKey::Num5 => ActionShortcutKey::Num5, + PluginManifestActionShortcutKey::Num6 => ActionShortcutKey::Num6, + PluginManifestActionShortcutKey::Num7 => ActionShortcutKey::Num7, + PluginManifestActionShortcutKey::Num8 => ActionShortcutKey::Num8, + PluginManifestActionShortcutKey::Num9 => ActionShortcutKey::Num9, + PluginManifestActionShortcutKey::Exclamation => ActionShortcutKey::Exclamation, + PluginManifestActionShortcutKey::AtSign => ActionShortcutKey::AtSign, + PluginManifestActionShortcutKey::Hash => ActionShortcutKey::Hash, + PluginManifestActionShortcutKey::Dollar => ActionShortcutKey::Dollar, + PluginManifestActionShortcutKey::Percent => ActionShortcutKey::Percent, + PluginManifestActionShortcutKey::Caret => ActionShortcutKey::Caret, + PluginManifestActionShortcutKey::Ampersand => ActionShortcutKey::Ampersand, + PluginManifestActionShortcutKey::Star => ActionShortcutKey::Star, + PluginManifestActionShortcutKey::LeftParenthesis => ActionShortcutKey::LeftParenthesis, + PluginManifestActionShortcutKey::RightParenthesis => ActionShortcutKey::RightParenthesis, + PluginManifestActionShortcutKey::LowerA => ActionShortcutKey::LowerA, + PluginManifestActionShortcutKey::LowerB => ActionShortcutKey::LowerB, + PluginManifestActionShortcutKey::LowerC => ActionShortcutKey::LowerC, + PluginManifestActionShortcutKey::LowerD => ActionShortcutKey::LowerD, + PluginManifestActionShortcutKey::LowerE => ActionShortcutKey::LowerE, + PluginManifestActionShortcutKey::LowerF => ActionShortcutKey::LowerF, + PluginManifestActionShortcutKey::LowerG => ActionShortcutKey::LowerG, + PluginManifestActionShortcutKey::LowerH => ActionShortcutKey::LowerH, + PluginManifestActionShortcutKey::LowerI => ActionShortcutKey::LowerI, + PluginManifestActionShortcutKey::LowerJ => ActionShortcutKey::LowerJ, + PluginManifestActionShortcutKey::LowerK => ActionShortcutKey::LowerK, + PluginManifestActionShortcutKey::LowerL => ActionShortcutKey::LowerL, + PluginManifestActionShortcutKey::LowerM => ActionShortcutKey::LowerM, + PluginManifestActionShortcutKey::LowerN => ActionShortcutKey::LowerN, + PluginManifestActionShortcutKey::LowerO => ActionShortcutKey::LowerO, + PluginManifestActionShortcutKey::LowerP => ActionShortcutKey::LowerP, + PluginManifestActionShortcutKey::LowerQ => ActionShortcutKey::LowerQ, + PluginManifestActionShortcutKey::LowerR => ActionShortcutKey::LowerR, + PluginManifestActionShortcutKey::LowerS => ActionShortcutKey::LowerS, + PluginManifestActionShortcutKey::LowerT => ActionShortcutKey::LowerT, + PluginManifestActionShortcutKey::LowerU => ActionShortcutKey::LowerU, + PluginManifestActionShortcutKey::LowerV => ActionShortcutKey::LowerV, + PluginManifestActionShortcutKey::LowerW => ActionShortcutKey::LowerW, + PluginManifestActionShortcutKey::LowerX => ActionShortcutKey::LowerX, + PluginManifestActionShortcutKey::LowerY => ActionShortcutKey::LowerY, + PluginManifestActionShortcutKey::LowerZ => ActionShortcutKey::LowerZ, + PluginManifestActionShortcutKey::UpperA => ActionShortcutKey::UpperA, + PluginManifestActionShortcutKey::UpperB => ActionShortcutKey::UpperB, + PluginManifestActionShortcutKey::UpperC => ActionShortcutKey::UpperC, + PluginManifestActionShortcutKey::UpperD => ActionShortcutKey::UpperD, + PluginManifestActionShortcutKey::UpperE => ActionShortcutKey::UpperE, + PluginManifestActionShortcutKey::UpperF => ActionShortcutKey::UpperF, + PluginManifestActionShortcutKey::UpperG => ActionShortcutKey::UpperG, + PluginManifestActionShortcutKey::UpperH => ActionShortcutKey::UpperH, + PluginManifestActionShortcutKey::UpperI => ActionShortcutKey::UpperI, + PluginManifestActionShortcutKey::UpperJ => ActionShortcutKey::UpperJ, + PluginManifestActionShortcutKey::UpperK => ActionShortcutKey::UpperK, + PluginManifestActionShortcutKey::UpperL => ActionShortcutKey::UpperL, + PluginManifestActionShortcutKey::UpperM => ActionShortcutKey::UpperM, + PluginManifestActionShortcutKey::UpperN => ActionShortcutKey::UpperN, + PluginManifestActionShortcutKey::UpperO => ActionShortcutKey::UpperO, + PluginManifestActionShortcutKey::UpperP => ActionShortcutKey::UpperP, + PluginManifestActionShortcutKey::UpperQ => ActionShortcutKey::UpperQ, + PluginManifestActionShortcutKey::UpperR => ActionShortcutKey::UpperR, + PluginManifestActionShortcutKey::UpperS => ActionShortcutKey::UpperS, + PluginManifestActionShortcutKey::UpperT => ActionShortcutKey::UpperT, + PluginManifestActionShortcutKey::UpperU => ActionShortcutKey::UpperU, + PluginManifestActionShortcutKey::UpperV => ActionShortcutKey::UpperV, + PluginManifestActionShortcutKey::UpperW => ActionShortcutKey::UpperW, + PluginManifestActionShortcutKey::UpperX => ActionShortcutKey::UpperX, + PluginManifestActionShortcutKey::UpperY => ActionShortcutKey::UpperY, + PluginManifestActionShortcutKey::UpperZ => ActionShortcutKey::UpperZ, + PluginManifestActionShortcutKey::Minus => ActionShortcutKey::Minus, + PluginManifestActionShortcutKey::Equals => ActionShortcutKey::Equals, + PluginManifestActionShortcutKey::Comma => ActionShortcutKey::Comma, + PluginManifestActionShortcutKey::Dot => ActionShortcutKey::Dot, + PluginManifestActionShortcutKey::Slash => ActionShortcutKey::Slash, + PluginManifestActionShortcutKey::OpenSquareBracket => ActionShortcutKey::OpenSquareBracket, + PluginManifestActionShortcutKey::CloseSquareBracket => ActionShortcutKey::CloseSquareBracket, + PluginManifestActionShortcutKey::Semicolon => ActionShortcutKey::Semicolon, + PluginManifestActionShortcutKey::Quote => ActionShortcutKey::Quote, + PluginManifestActionShortcutKey::Backslash => ActionShortcutKey::Backslash, + PluginManifestActionShortcutKey::Underscore => ActionShortcutKey::Underscore, + PluginManifestActionShortcutKey::Plus => ActionShortcutKey::Plus, + PluginManifestActionShortcutKey::LessThan => ActionShortcutKey::LessThan, + PluginManifestActionShortcutKey::GreaterThan => ActionShortcutKey::GreaterThan, + PluginManifestActionShortcutKey::QuestionMark => ActionShortcutKey::QuestionMark, + PluginManifestActionShortcutKey::LeftBrace => ActionShortcutKey::LeftBrace, + PluginManifestActionShortcutKey::RightBrace => ActionShortcutKey::RightBrace, + PluginManifestActionShortcutKey::Colon => ActionShortcutKey::Colon, + PluginManifestActionShortcutKey::DoubleQuotes => ActionShortcutKey::DoubleQuotes, + PluginManifestActionShortcutKey::Pipe => ActionShortcutKey::Pipe, + } + } +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[schemars(description = "The type of shortcut.")] +pub enum PluginManifestActionShortcutKind { + #[serde(rename = "main")] + #[schemars(description = "Main shortcut for the action (e.g. cmd).")] + Main, + #[serde(rename = "alternative")] + #[schemars(description = "Alternative shortcut for the action (e.g. opt).")] + Alternative, +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +#[serde(tag = "os")] +pub enum PluginManifestSupportedSystem { + #[serde(rename = "linux")] + Linux, + #[serde(rename = "windows")] + Windows, + #[serde(rename = "macos")] + MacOS, +} + +impl std::fmt::Display for PluginManifestSupportedSystem { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + PluginManifestSupportedSystem::Linux => write!(f, "Linux"), + PluginManifestSupportedSystem::Windows => write!(f, "Windows"), + PluginManifestSupportedSystem::MacOS => write!(f, "MacOS"), + } + } +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[schemars(description = "Metadata for the plugin manifest.")] +pub struct PluginManifestMetadata { + #[schemars(description = "Name of the plugin.")] + pub name: String, + #[schemars(description = "Description of the plugin.")] + pub description: String, +} + +#[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] +#[schemars(description = "Permissions required by the plugin.")] +pub struct PluginManifestPermissions { + #[serde(default)] + #[schemars(description = "Environment variables that the plugin can access.")] + pub environment: Vec, + #[serde(default)] + #[schemars(description = "Network domains that the plugin can access.")] + pub network: Vec, + #[serde(default)] + #[schemars(description = "Filesystem permissions for the plugin.")] + pub filesystem: PluginManifestPermissionsFileSystem, + #[serde(default)] + #[schemars(description = "Execution permissions for the plugin.")] + pub exec: PluginManifestPermissionsExec, + #[serde(default)] + #[schemars(description = "System permissions for the plugin.")] + pub system: Vec, + #[serde(default)] + #[schemars(description = "Clipboard permissions for the plugin.")] + pub clipboard: Vec, + #[serde(default)] + #[schemars(description = "Permissions for the main search bar.")] + pub main_search_bar: Vec, +} + +#[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] +#[schemars(description = "Filesystem permissions for the plugin.")] +pub struct PluginManifestPermissionsFileSystem { + #[serde(default)] + #[schemars(description = "Paths that the plugin can read from.")] + pub read: Vec, + #[serde(default)] + #[schemars(description = "Paths that the plugin can write to.")] + pub write: Vec, +} + +#[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] +#[schemars(description = "Execution permissions for the plugin.")] +pub struct PluginManifestPermissionsExec { + #[serde(default)] + #[schemars(description = "Commands that the plugin can execute.")] + pub command: Vec, + #[serde(default)] + #[schemars(description = "Executables that the plugin can run.")] + pub executable: Vec, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[schemars(description = "Clipboard permissions for the plugin.")] +pub enum PluginManifestClipboardPermissions { + #[serde(rename = "read")] + #[schemars(description = "Allows the plugin to read from the clipboard.")] + Read, + #[serde(rename = "write")] + #[schemars(description = "Allows the plugin to write to the clipboard.")] + Write, + #[serde(rename = "clear")] + #[schemars(description = "Allows the plugin to clear the clipboard contents.")] + Clear, +} + +#[derive(Debug, Deserialize, Eq, PartialEq, Serialize, JsonSchema)] +pub enum PluginManifestMainSearchBarPermissions { + #[serde(rename = "read")] + #[schemars(description = "Allows the plugin to read the main search bar")] + Read, +} From 338540cdec63fb38a4698698bf71c4046a42decf Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 20 Jan 2025 18:39:21 +0100 Subject: [PATCH 388/540] Fix plugin examples after breaking changes --- rust/scenario_runner/src/frontend_mock.rs | 2 +- .../docs_grid/src/content_code_block.tsx | 2 +- .../plugins/docs_grid/src/content_headers.tsx | 12 +++++----- .../plugins/docs_grid/src/content_image.tsx | 2 +- .../docs_grid/src/content_paragraph.tsx | 2 +- scenarios/plugins/docs_grid/src/main.tsx | 2 +- .../plugins/docs_grid/src/search_bar.tsx | 2 +- scenarios/plugins/docs_grid/src/section.tsx | 22 +++++++++---------- scenarios/plugins/docs_list/src/detail.tsx | 20 ++++++++--------- scenarios/plugins/docs_list/src/main.tsx | 20 ++++++++--------- .../plugins/docs_list/src/search_bar.tsx | 2 +- scenarios/plugins/docs_list/src/section.tsx | 16 +++++++------- 12 files changed, 52 insertions(+), 52 deletions(-) diff --git a/rust/scenario_runner/src/frontend_mock.rs b/rust/scenario_runner/src/frontend_mock.rs index 55d6403..c108972 100644 --- a/rust/scenario_runner/src/frontend_mock.rs +++ b/rust/scenario_runner/src/frontend_mock.rs @@ -160,7 +160,7 @@ async fn request_loop( | UiRequestData::SetTheme { .. } => { unreachable!() } - UiRequestData::SetGlobalShortcut { .. } | UiRequestData::RequestSearchResultUpdate => { + UiRequestData::SetGlobalShortcut { .. } | UiRequestData::SetWindowPositionMode { .. } | UiRequestData::RequestSearchResultUpdate => { // noop } UiRequestData::ReplaceView { diff --git a/scenarios/plugins/docs_grid/src/content_code_block.tsx b/scenarios/plugins/docs_grid/src/content_code_block.tsx index 6e793e9..3127d08 100644 --- a/scenarios/plugins/docs_grid/src/content_code_block.tsx +++ b/scenarios/plugins/docs_grid/src/content_code_block.tsx @@ -14,7 +14,7 @@ export default function Main(): ReactElement { return ( {items.map(value => ( - + {value} diff --git a/scenarios/plugins/docs_grid/src/content_headers.tsx b/scenarios/plugins/docs_grid/src/content_headers.tsx index 1c9b0c7..db65312 100644 --- a/scenarios/plugins/docs_grid/src/content_headers.tsx +++ b/scenarios/plugins/docs_grid/src/content_headers.tsx @@ -4,42 +4,42 @@ import { Grid } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - + Episode I - + Episode II - + Episode III - + Episode IV - + Episode V - + Episode VI diff --git a/scenarios/plugins/docs_grid/src/content_image.tsx b/scenarios/plugins/docs_grid/src/content_image.tsx index e5a50ac..60e0ac0 100644 --- a/scenarios/plugins/docs_grid/src/content_image.tsx +++ b/scenarios/plugins/docs_grid/src/content_image.tsx @@ -36,7 +36,7 @@ export default function Main(): ReactElement { return ( {items.map(value => ( - + diff --git a/scenarios/plugins/docs_grid/src/content_paragraph.tsx b/scenarios/plugins/docs_grid/src/content_paragraph.tsx index 3c4a339..44d76f4 100644 --- a/scenarios/plugins/docs_grid/src/content_paragraph.tsx +++ b/scenarios/plugins/docs_grid/src/content_paragraph.tsx @@ -14,7 +14,7 @@ export default function Main(): ReactElement { return ( {items.map(value => ( - + {value} diff --git a/scenarios/plugins/docs_grid/src/main.tsx b/scenarios/plugins/docs_grid/src/main.tsx index e5a50ac..60e0ac0 100644 --- a/scenarios/plugins/docs_grid/src/main.tsx +++ b/scenarios/plugins/docs_grid/src/main.tsx @@ -36,7 +36,7 @@ export default function Main(): ReactElement { return ( {items.map(value => ( - + diff --git a/scenarios/plugins/docs_grid/src/search_bar.tsx b/scenarios/plugins/docs_grid/src/search_bar.tsx index ca19caa..ce67753 100644 --- a/scenarios/plugins/docs_grid/src/search_bar.tsx +++ b/scenarios/plugins/docs_grid/src/search_bar.tsx @@ -23,7 +23,7 @@ export default function Main(): ReactElement { {results .filter(value => !searchText ? true : value.toLowerCase().includes(searchText)) .map(value => ( - + {value} diff --git a/scenarios/plugins/docs_grid/src/section.tsx b/scenarios/plugins/docs_grid/src/section.tsx index 55e79ab..2d0ab75 100644 --- a/scenarios/plugins/docs_grid/src/section.tsx +++ b/scenarios/plugins/docs_grid/src/section.tsx @@ -18,59 +18,59 @@ export default function Main(): ReactElement { return ( - + - + - + - + - + - + - + - + - + - + - + diff --git a/scenarios/plugins/docs_list/src/detail.tsx b/scenarios/plugins/docs_list/src/detail.tsx index d5d5854..72d6a6b 100644 --- a/scenarios/plugins/docs_list/src/detail.tsx +++ b/scenarios/plugins/docs_list/src/detail.tsx @@ -4,16 +4,16 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - - - - - - - - - - + + + + + + + + + + Sentient diff --git a/scenarios/plugins/docs_list/src/main.tsx b/scenarios/plugins/docs_list/src/main.tsx index 6bdafeb..6458e49 100644 --- a/scenarios/plugins/docs_list/src/main.tsx +++ b/scenarios/plugins/docs_list/src/main.tsx @@ -4,16 +4,16 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - - - - - - - - - - + + + + + + + + + + ) } diff --git a/scenarios/plugins/docs_list/src/search_bar.tsx b/scenarios/plugins/docs_list/src/search_bar.tsx index 618f476..a57aeda 100644 --- a/scenarios/plugins/docs_list/src/search_bar.tsx +++ b/scenarios/plugins/docs_list/src/search_bar.tsx @@ -23,7 +23,7 @@ export default function Main(): ReactElement { {results .filter(value => !searchText ? true : value.toLowerCase().includes(searchText)) .map(value => ( - + )) } diff --git a/scenarios/plugins/docs_list/src/section.tsx b/scenarios/plugins/docs_list/src/section.tsx index 3d9db2b..9f23846 100644 --- a/scenarios/plugins/docs_list/src/section.tsx +++ b/scenarios/plugins/docs_list/src/section.tsx @@ -4,17 +4,17 @@ import { List } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - + - - - + + + - - - - + + + + ) From 053b0a100805faade8d401d1846efc2c3774afc0 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 20 Jan 2025 20:00:27 +0100 Subject: [PATCH 389/540] Tweak window border color of macos dark theme on non-macos platforms --- bundled_themes/macos_dark.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundled_themes/macos_dark.toml b/bundled_themes/macos_dark.toml index 52389ae..e0f8e13 100644 --- a/bundled_themes/macos_dark.toml +++ b/bundled_themes/macos_dark.toml @@ -17,7 +17,7 @@ text = [ [window.border] radius = 8 width = 1 -color = { color = "#646464", alpha = 0.5 } +color = { color = "#383838", alpha = 1 } [content.border] radius = 4.0 From 74f6433beadcdad1fe63ed1534cf1148675b8018 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 20 Jan 2025 20:01:31 +0100 Subject: [PATCH 390/540] Fix screenshot generator --- rust/client/src/ui/mod.rs | 71 +++++++++++++++++---------------- rust/common/src/model.rs | 2 +- rust/scenario_runner/src/lib.rs | 28 +++++++++++++ rust/server/src/lib.rs | 24 +++++++++-- rust/server/src/plugins/mod.rs | 4 +- 5 files changed, 87 insertions(+), 42 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 78c8c60..056acae 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -157,7 +157,7 @@ pub struct AppModel { theme: GauntletComplexTheme, window_position_mode: WindowPositionMode, close_on_unfocus: bool, - window_position_file: PathBuf, + window_position_file: Option, #[cfg(target_os = "linux")] x11_active_window: Option, pending_window_state_reset: bool, @@ -429,8 +429,10 @@ fn layer_shell_settings() -> iced_layershell::reexport::NewLayerShellSettings { } } -fn open_main_window_non_wayland(minimized: bool, window_position_file: &PathBuf) -> (window::Id, Task) { - let position = fs::read_to_string(window_position_file) +fn open_main_window_non_wayland(minimized: bool, window_position_file: Option<&PathBuf>) -> (window::Id, Task) { + let position = window_position_file + .map(|window_position_file| fs::read_to_string(window_position_file).ok()) + .flatten() .map(|data| { if let Some((x, y)) = data.split_once(":") { match (x.parse(), y.parse()) { @@ -575,14 +577,16 @@ fn new( open_main_window_wayland(id) } } else { - open_main_window_non_wayland(minimized, &setup_data.window_position_file) + open_main_window_non_wayland(minimized, setup_data.window_position_file.as_ref()) }; #[cfg(not(target_os = "linux"))] - let (main_window_id, open_task) = open_main_window_non_wayland(minimized, &setup_data.window_position_file); + let (main_window_id, open_task) = open_main_window_non_wayland(minimized, setup_data.window_position_file.as_ref()); tasks.push(open_task); + let mut client_context = ClientContext::new(); + let global_state = if cfg!(feature = "scenario_runner") { let gen_in = std::env::var("GAUNTLET_SCREENSHOT_GEN_IN").expect("Unable to read GAUNTLET_SCREENSHOT_GEN_IN"); @@ -619,21 +623,20 @@ fn new( } => { let plugin_id = PluginId::from_string("__SCREENSHOT_GEN___"); let entrypoint_id = EntrypointId::from_string(entrypoint_id); + let plugin_name = "Screenshot Plugin".to_string(); + let entrypoint_name = gen_name; let render_location = ui_render_location_from_scenario(render_location); - let msg = AppMsg::RenderPluginUI { - plugin_id: plugin_id.clone(), - plugin_name: "Screenshot Plugin".to_string(), - entrypoint_id: entrypoint_id.clone(), - entrypoint_name: "Screenshot Entrypoint".to_string(), + let _ = client_context.render_ui( render_location, - top_level_view, - container: Arc::new(container), + Arc::new(container), images, - }; - - tasks.push(Task::done(msg)); + &plugin_id, + &plugin_name, + &entrypoint_id, + &entrypoint_name, + ); match render_location { UiRenderLocation::InlineView => GlobalState::new(text_input::Id::unique()), @@ -642,9 +645,9 @@ fn new( PluginViewData { top_level_view, plugin_id, - plugin_name: "Screenshot Gen".to_string(), + plugin_name, entrypoint_id, - entrypoint_name: gen_name, + entrypoint_name, action_shortcuts: Default::default(), }, true, @@ -708,7 +711,7 @@ fn new( // state global_state, - client_context: ClientContext::new(), + client_context, search_results: vec![], loading_bar_state: HashMap::new(), hud_display: None, @@ -878,30 +881,26 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } AppMsg::PromptChanged(mut new_prompt) => { - if cfg!(feature = "scenario_runner") { - Task::none() - } else { - match &mut state.global_state { - GlobalState::MainView { + match &mut state.global_state { + GlobalState::MainView { focused_search_result, sub_state, .. } => { - new_prompt.truncate(100); // search query uses regex so just to be safe truncate the prompt + new_prompt.truncate(100); // search query uses regex so just to be safe truncate the prompt - state.prompt = new_prompt.clone(); + state.prompt = new_prompt.clone(); - focused_search_result.reset(true); + focused_search_result.reset(true); - MainViewState::initial(sub_state); - } - GlobalState::ErrorView { .. } => {} - GlobalState::PluginView { .. } => {} - GlobalState::PendingPluginView { .. } => {} + MainViewState::initial(sub_state); + } + GlobalState::ErrorView { .. } => {} + GlobalState::PluginView { .. } => {} + GlobalState::PendingPluginView { .. } => {} } - state.search(new_prompt, true) - } + state.search(new_prompt, true) } AppMsg::UpdateSearchResults => { match &state.global_state { @@ -1134,8 +1133,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { return Task::none(); } - let _ = fs::create_dir_all(state.window_position_file.parent().unwrap()); - let _ = fs::write(&state.window_position_file, format!("{}:{}", point.x, point.y)); + if let Some(window_position_file) = &state.window_position_file { + let _ = fs::create_dir_all(window_position_file.parent().unwrap()); + let _ = fs::write(&window_position_file, format!("{}:{}", point.x, point.y)); + } Task::none() } diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 0b2217e..330278c 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -214,7 +214,7 @@ pub struct UiTheme { #[derive(Debug)] pub struct UiSetupData { - pub window_position_file: PathBuf, + pub window_position_file: Option, pub theme: UiTheme, pub global_shortcut: Option, pub close_on_unfocus: bool, diff --git a/rust/scenario_runner/src/lib.rs b/rust/scenario_runner/src/lib.rs index 693245f..854a703 100644 --- a/rust/scenario_runner/src/lib.rs +++ b/rust/scenario_runner/src/lib.rs @@ -1,4 +1,7 @@ use gauntlet_common::model::BackendRequestData; +use gauntlet_common::model::UiSetupData; +use gauntlet_common::model::UiTheme; +use gauntlet_common::model::WindowPositionMode; use gauntlet_common::model::BackendResponseData; use gauntlet_common::model::UiRequestData; use gauntlet_common::model::UiResponseData; @@ -16,3 +19,28 @@ pub async fn run_scenario_runner_frontend_mock( Ok(()) } + +pub async fn run_scenario_runner_mock_server( + _request_sender: RequestSender, + mut backend_receiver: RequestReceiver, + theme: UiTheme +) -> anyhow::Result<()> { + + let (_data, responder) = backend_receiver.recv().await; + responder.respond(BackendResponseData::SetupData { + data: UiSetupData { + window_position_file: None, + theme, + global_shortcut: None, + close_on_unfocus: false, + window_position_mode: WindowPositionMode::Static, + }, + }); + + let (_data, responder) = backend_receiver.recv().await; + responder.respond(BackendResponseData::Nothing); + + std::thread::park(); + + Ok(()) +} diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index 6eb0b69..4c8b800 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -18,6 +18,7 @@ use gauntlet_common::model::EntrypointId; use gauntlet_common::model::PluginId; use gauntlet_common::model::UiRequestData; use gauntlet_common::model::UiResponseData; +use gauntlet_common::model::UiTheme; use gauntlet_common::rpc::backend_api::BackendApi; use gauntlet_common::rpc::backend_api::BackendApiError; use gauntlet_common::rpc::backend_server::start_backend_server; @@ -119,10 +120,13 @@ fn run_scenario_runner() { let (frontend_sender, frontend_receiver) = channel::(); let (backend_sender, backend_receiver) = channel::(); - start_client(false, frontend_receiver, backend_sender); + std::thread::spawn(|| { + let theme = crate::plugins::theme::BundledThemes::new().unwrap(); - drop(frontend_sender); - drop(backend_receiver); + start_mock_server(frontend_sender, backend_receiver, theme.legacy_theme) + }); + + start_client(false, frontend_receiver, backend_sender); } "scenario_runner" => { let (frontend_sender, frontend_receiver) = channel::(); @@ -130,7 +134,7 @@ fn run_scenario_runner() { std::thread::spawn(|| start_server(frontend_sender, backend_receiver)); - start_frontend_mock(frontend_receiver, backend_sender) + start_frontend_mock(frontend_receiver, backend_sender); } _ => panic!("unknown type"), } @@ -168,6 +172,18 @@ fn start_server( .unwrap(); } +#[cfg(feature = "scenario_runner")] +fn start_mock_server(request_sender: RequestSender, backend_receiver: RequestReceiver, theme: UiTheme) { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("unable to start server tokio runtime") + .block_on(async { + gauntlet_scenario_runner::run_scenario_runner_mock_server(request_sender, backend_receiver, theme).await + }) + .unwrap(); +} + #[cfg(feature = "scenario_runner")] fn start_frontend_mock( request_receiver: RequestReceiver, diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index f0e7b19..b282ee2 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -87,7 +87,7 @@ pub mod plugin_manifest; mod run_status; mod runtime; mod settings; -mod theme; +pub mod theme; static BUNDLED_PLUGINS: [(&str, Dir); 1] = [( "gauntlet", @@ -146,7 +146,7 @@ impl ApplicationManager { let close_on_unfocus = self.config_reader.close_on_unfocus(); Ok(UiSetupData { - window_position_file, + window_position_file: Some(window_position_file), theme, global_shortcut, close_on_unfocus, From 6ccd091c8f6af814860f69c9bc3698626a12bd7c Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Mon, 20 Jan 2025 20:23:28 +0100 Subject: [PATCH 391/540] Rename scenarios directory to example_plugins --- {scenarios => example_plugins}/.gitignore | 0 .../plugins/docs_detail/.gitignore | 0 .../plugins/docs_detail/gauntlet.toml | 0 .../plugins/docs_detail/package.json | 0 .../plugins/docs_detail/src/content.tsx | 0 .../docs_detail/src/content_code_block.tsx | 0 .../docs_detail/src/content_header.tsx | 0 .../src/content_horizontal_break.tsx | 0 .../plugins/docs_detail/src/content_image.tsx | 0 .../docs_detail/src/content_paragraph.tsx | 0 .../plugins/docs_detail/src/main.tsx | 0 .../plugins/docs_detail/src/metadata.tsx | 0 .../plugins/docs_detail/src/metadata_icon.tsx | 0 .../plugins/docs_detail/src/metadata_link.tsx | 0 .../docs_detail/src/metadata_separator.tsx | 0 .../docs_detail/src/metadata_tag_list.tsx | 0 .../docs_detail/src/metadata_value.tsx | 0 .../plugins/docs_detail/tsconfig.json | 0 .../plugins/docs_form/.gitignore | 0 .../plugins/docs_form/gauntlet.toml | 0 .../plugins/docs_form/package.json | 0 .../plugins/docs_form/src/checkbox.tsx | 0 .../plugins/docs_form/src/date-picker.tsx | 0 .../plugins/docs_form/src/main.tsx | 0 .../plugins/docs_form/src/password-field.tsx | 0 .../plugins/docs_form/src/select.tsx | 0 .../plugins/docs_form/src/separator.tsx | 0 .../plugins/docs_form/src/text-field.tsx | 0 .../plugins/docs_form/tsconfig.json | 0 .../plugins/docs_grid/.gitignore | 0 .../plugins/docs_grid/gauntlet.toml | 0 .../plugins/docs_grid/package.json | 0 .../docs_grid/src/content_code_block.tsx | 0 .../plugins/docs_grid/src/content_headers.tsx | 0 .../plugins/docs_grid/src/content_image.tsx | 0 .../docs_grid/src/content_paragraph.tsx | 0 .../plugins/docs_grid/src/empty_view.tsx | 0 .../plugins/docs_grid/src/main.tsx | 0 .../plugins/docs_grid/src/search_bar.tsx | 0 .../plugins/docs_grid/src/section.tsx | 0 .../plugins/docs_grid/tsconfig.json | 0 .../plugins/docs_inline_separators/.gitignore | 0 .../docs_inline_separators/gauntlet.toml | 0 .../docs_inline_separators/package.json | 0 .../docs_inline_separators/src/main.tsx | 0 .../docs_inline_separators/tsconfig.json | 0 .../docs_inline_three_sections/.gitignore | 0 .../docs_inline_three_sections/gauntlet.toml | 0 .../docs_inline_three_sections/package.json | 0 .../docs_inline_three_sections/src/main.tsx | 0 .../docs_inline_three_sections/tsconfig.json | 0 .../docs_inline_two_sections/.gitignore | 0 .../docs_inline_two_sections/gauntlet.toml | 0 .../docs_inline_two_sections/package.json | 0 .../docs_inline_two_sections/src/main.tsx | 0 .../docs_inline_two_sections/tsconfig.json | 0 .../plugins/docs_list/.gitignore | 0 .../plugins/docs_list/gauntlet.toml | 0 .../plugins/docs_list/package.json | 0 .../plugins/docs_list/src/detail.tsx | 0 .../plugins/docs_list/src/empty_view.tsx | 0 .../plugins/docs_list/src/main.tsx | 0 .../plugins/docs_list/src/search_bar.tsx | 0 .../plugins/docs_list/src/section.tsx | 0 .../plugins/docs_list/tsconfig.json | 0 .../content-code-block/default.json | 0 .../docs_detail/content-header/default.json | 0 .../content-horizontal-break/default.json | 0 .../docs_detail/content-image/default.json | 0 .../content-paragraph/default.json | 0 .../docs_detail/content/default.json | 0 .../scenarios}/docs_detail/main/default.json | 0 .../docs_detail/metadata-icon/default.json | 0 .../docs_detail/metadata-link/default.json | 0 .../metadata-separator/default.json | 0 .../metadata-tag-list/default.json | 0 .../docs_detail/metadata-value/default.json | 0 .../docs_detail/metadata/default.json | 0 .../docs_form/checkbox/default.json | 0 .../docs_form/date-picker/default.json | 0 .../scenarios}/docs_form/main/default.json | 0 .../docs_form/password-field/default.json | 0 .../scenarios}/docs_form/select/default.json | 0 .../docs_form/separator/default.json | 0 .../docs_form/text-field/default.json | 0 .../docs_grid/content-code-block/default.json | 0 .../docs_grid/content-headers/default.json | 0 .../docs_grid/content-image/default.json | 0 .../docs_grid/content-paragraph/default.json | 0 .../docs_grid/empty-view/default.json | 0 .../scenarios}/docs_grid/main/default.json | 0 .../docs_grid/search-bar/default.json | 0 .../scenarios}/docs_grid/section/default.json | 0 .../main/separator.json | 0 .../main/three-sections.json | 0 .../main/two-sections.json | 0 .../scenarios}/docs_list/detail/default.json | 0 .../docs_list/empty-view/default.json | 0 .../scenarios}/docs_list/main/default.json | 0 .../docs_list/search-bar/default.json | 0 .../scenarios}/docs_list/section/default.json | 0 js/scenario_runner_cli/src/main.ts | 6 +- package-lock.json | 168 ++++++++++++++---- package.json | 4 +- rust/scenario_runner/src/frontend_mock.rs | 2 +- 105 files changed, 137 insertions(+), 43 deletions(-) rename {scenarios => example_plugins}/.gitignore (100%) rename {scenarios => example_plugins}/plugins/docs_detail/.gitignore (100%) rename {scenarios => example_plugins}/plugins/docs_detail/gauntlet.toml (100%) rename {scenarios => example_plugins}/plugins/docs_detail/package.json (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/content.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/content_code_block.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/content_header.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/content_horizontal_break.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/content_image.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/content_paragraph.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/main.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/metadata.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/metadata_icon.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/metadata_link.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/metadata_separator.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/metadata_tag_list.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/src/metadata_value.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_detail/tsconfig.json (100%) rename {scenarios => example_plugins}/plugins/docs_form/.gitignore (100%) rename {scenarios => example_plugins}/plugins/docs_form/gauntlet.toml (100%) rename {scenarios => example_plugins}/plugins/docs_form/package.json (100%) rename {scenarios => example_plugins}/plugins/docs_form/src/checkbox.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_form/src/date-picker.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_form/src/main.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_form/src/password-field.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_form/src/select.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_form/src/separator.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_form/src/text-field.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_form/tsconfig.json (100%) rename {scenarios => example_plugins}/plugins/docs_grid/.gitignore (100%) rename {scenarios => example_plugins}/plugins/docs_grid/gauntlet.toml (100%) rename {scenarios => example_plugins}/plugins/docs_grid/package.json (100%) rename {scenarios => example_plugins}/plugins/docs_grid/src/content_code_block.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_grid/src/content_headers.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_grid/src/content_image.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_grid/src/content_paragraph.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_grid/src/empty_view.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_grid/src/main.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_grid/src/search_bar.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_grid/src/section.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_grid/tsconfig.json (100%) rename {scenarios => example_plugins}/plugins/docs_inline_separators/.gitignore (100%) rename {scenarios => example_plugins}/plugins/docs_inline_separators/gauntlet.toml (100%) rename {scenarios => example_plugins}/plugins/docs_inline_separators/package.json (100%) rename {scenarios => example_plugins}/plugins/docs_inline_separators/src/main.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_inline_separators/tsconfig.json (100%) rename {scenarios => example_plugins}/plugins/docs_inline_three_sections/.gitignore (100%) rename {scenarios => example_plugins}/plugins/docs_inline_three_sections/gauntlet.toml (100%) rename {scenarios => example_plugins}/plugins/docs_inline_three_sections/package.json (100%) rename {scenarios => example_plugins}/plugins/docs_inline_three_sections/src/main.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_inline_three_sections/tsconfig.json (100%) rename {scenarios => example_plugins}/plugins/docs_inline_two_sections/.gitignore (100%) rename {scenarios => example_plugins}/plugins/docs_inline_two_sections/gauntlet.toml (100%) rename {scenarios => example_plugins}/plugins/docs_inline_two_sections/package.json (100%) rename {scenarios => example_plugins}/plugins/docs_inline_two_sections/src/main.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_inline_two_sections/tsconfig.json (100%) rename {scenarios => example_plugins}/plugins/docs_list/.gitignore (100%) rename {scenarios => example_plugins}/plugins/docs_list/gauntlet.toml (100%) rename {scenarios => example_plugins}/plugins/docs_list/package.json (100%) rename {scenarios => example_plugins}/plugins/docs_list/src/detail.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_list/src/empty_view.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_list/src/main.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_list/src/search_bar.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_list/src/section.tsx (100%) rename {scenarios => example_plugins}/plugins/docs_list/tsconfig.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/content-code-block/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/content-header/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/content-horizontal-break/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/content-image/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/content-paragraph/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/content/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/main/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/metadata-icon/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/metadata-link/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/metadata-separator/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/metadata-tag-list/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/metadata-value/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_detail/metadata/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_form/checkbox/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_form/date-picker/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_form/main/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_form/password-field/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_form/select/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_form/separator/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_form/text-field/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_grid/content-code-block/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_grid/content-headers/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_grid/content-image/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_grid/content-paragraph/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_grid/empty-view/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_grid/main/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_grid/search-bar/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_grid/section/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_inline_separators/main/separator.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_inline_three_sections/main/three-sections.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_inline_two_sections/main/two-sections.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_list/detail/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_list/empty-view/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_list/main/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_list/search-bar/default.json (100%) rename {scenarios/data => example_plugins/scenarios}/docs_list/section/default.json (100%) diff --git a/scenarios/.gitignore b/example_plugins/.gitignore similarity index 100% rename from scenarios/.gitignore rename to example_plugins/.gitignore diff --git a/scenarios/plugins/docs_detail/.gitignore b/example_plugins/plugins/docs_detail/.gitignore similarity index 100% rename from scenarios/plugins/docs_detail/.gitignore rename to example_plugins/plugins/docs_detail/.gitignore diff --git a/scenarios/plugins/docs_detail/gauntlet.toml b/example_plugins/plugins/docs_detail/gauntlet.toml similarity index 100% rename from scenarios/plugins/docs_detail/gauntlet.toml rename to example_plugins/plugins/docs_detail/gauntlet.toml diff --git a/scenarios/plugins/docs_detail/package.json b/example_plugins/plugins/docs_detail/package.json similarity index 100% rename from scenarios/plugins/docs_detail/package.json rename to example_plugins/plugins/docs_detail/package.json diff --git a/scenarios/plugins/docs_detail/src/content.tsx b/example_plugins/plugins/docs_detail/src/content.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/content.tsx rename to example_plugins/plugins/docs_detail/src/content.tsx diff --git a/scenarios/plugins/docs_detail/src/content_code_block.tsx b/example_plugins/plugins/docs_detail/src/content_code_block.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/content_code_block.tsx rename to example_plugins/plugins/docs_detail/src/content_code_block.tsx diff --git a/scenarios/plugins/docs_detail/src/content_header.tsx b/example_plugins/plugins/docs_detail/src/content_header.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/content_header.tsx rename to example_plugins/plugins/docs_detail/src/content_header.tsx diff --git a/scenarios/plugins/docs_detail/src/content_horizontal_break.tsx b/example_plugins/plugins/docs_detail/src/content_horizontal_break.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/content_horizontal_break.tsx rename to example_plugins/plugins/docs_detail/src/content_horizontal_break.tsx diff --git a/scenarios/plugins/docs_detail/src/content_image.tsx b/example_plugins/plugins/docs_detail/src/content_image.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/content_image.tsx rename to example_plugins/plugins/docs_detail/src/content_image.tsx diff --git a/scenarios/plugins/docs_detail/src/content_paragraph.tsx b/example_plugins/plugins/docs_detail/src/content_paragraph.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/content_paragraph.tsx rename to example_plugins/plugins/docs_detail/src/content_paragraph.tsx diff --git a/scenarios/plugins/docs_detail/src/main.tsx b/example_plugins/plugins/docs_detail/src/main.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/main.tsx rename to example_plugins/plugins/docs_detail/src/main.tsx diff --git a/scenarios/plugins/docs_detail/src/metadata.tsx b/example_plugins/plugins/docs_detail/src/metadata.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/metadata.tsx rename to example_plugins/plugins/docs_detail/src/metadata.tsx diff --git a/scenarios/plugins/docs_detail/src/metadata_icon.tsx b/example_plugins/plugins/docs_detail/src/metadata_icon.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/metadata_icon.tsx rename to example_plugins/plugins/docs_detail/src/metadata_icon.tsx diff --git a/scenarios/plugins/docs_detail/src/metadata_link.tsx b/example_plugins/plugins/docs_detail/src/metadata_link.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/metadata_link.tsx rename to example_plugins/plugins/docs_detail/src/metadata_link.tsx diff --git a/scenarios/plugins/docs_detail/src/metadata_separator.tsx b/example_plugins/plugins/docs_detail/src/metadata_separator.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/metadata_separator.tsx rename to example_plugins/plugins/docs_detail/src/metadata_separator.tsx diff --git a/scenarios/plugins/docs_detail/src/metadata_tag_list.tsx b/example_plugins/plugins/docs_detail/src/metadata_tag_list.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/metadata_tag_list.tsx rename to example_plugins/plugins/docs_detail/src/metadata_tag_list.tsx diff --git a/scenarios/plugins/docs_detail/src/metadata_value.tsx b/example_plugins/plugins/docs_detail/src/metadata_value.tsx similarity index 100% rename from scenarios/plugins/docs_detail/src/metadata_value.tsx rename to example_plugins/plugins/docs_detail/src/metadata_value.tsx diff --git a/scenarios/plugins/docs_detail/tsconfig.json b/example_plugins/plugins/docs_detail/tsconfig.json similarity index 100% rename from scenarios/plugins/docs_detail/tsconfig.json rename to example_plugins/plugins/docs_detail/tsconfig.json diff --git a/scenarios/plugins/docs_form/.gitignore b/example_plugins/plugins/docs_form/.gitignore similarity index 100% rename from scenarios/plugins/docs_form/.gitignore rename to example_plugins/plugins/docs_form/.gitignore diff --git a/scenarios/plugins/docs_form/gauntlet.toml b/example_plugins/plugins/docs_form/gauntlet.toml similarity index 100% rename from scenarios/plugins/docs_form/gauntlet.toml rename to example_plugins/plugins/docs_form/gauntlet.toml diff --git a/scenarios/plugins/docs_form/package.json b/example_plugins/plugins/docs_form/package.json similarity index 100% rename from scenarios/plugins/docs_form/package.json rename to example_plugins/plugins/docs_form/package.json diff --git a/scenarios/plugins/docs_form/src/checkbox.tsx b/example_plugins/plugins/docs_form/src/checkbox.tsx similarity index 100% rename from scenarios/plugins/docs_form/src/checkbox.tsx rename to example_plugins/plugins/docs_form/src/checkbox.tsx diff --git a/scenarios/plugins/docs_form/src/date-picker.tsx b/example_plugins/plugins/docs_form/src/date-picker.tsx similarity index 100% rename from scenarios/plugins/docs_form/src/date-picker.tsx rename to example_plugins/plugins/docs_form/src/date-picker.tsx diff --git a/scenarios/plugins/docs_form/src/main.tsx b/example_plugins/plugins/docs_form/src/main.tsx similarity index 100% rename from scenarios/plugins/docs_form/src/main.tsx rename to example_plugins/plugins/docs_form/src/main.tsx diff --git a/scenarios/plugins/docs_form/src/password-field.tsx b/example_plugins/plugins/docs_form/src/password-field.tsx similarity index 100% rename from scenarios/plugins/docs_form/src/password-field.tsx rename to example_plugins/plugins/docs_form/src/password-field.tsx diff --git a/scenarios/plugins/docs_form/src/select.tsx b/example_plugins/plugins/docs_form/src/select.tsx similarity index 100% rename from scenarios/plugins/docs_form/src/select.tsx rename to example_plugins/plugins/docs_form/src/select.tsx diff --git a/scenarios/plugins/docs_form/src/separator.tsx b/example_plugins/plugins/docs_form/src/separator.tsx similarity index 100% rename from scenarios/plugins/docs_form/src/separator.tsx rename to example_plugins/plugins/docs_form/src/separator.tsx diff --git a/scenarios/plugins/docs_form/src/text-field.tsx b/example_plugins/plugins/docs_form/src/text-field.tsx similarity index 100% rename from scenarios/plugins/docs_form/src/text-field.tsx rename to example_plugins/plugins/docs_form/src/text-field.tsx diff --git a/scenarios/plugins/docs_form/tsconfig.json b/example_plugins/plugins/docs_form/tsconfig.json similarity index 100% rename from scenarios/plugins/docs_form/tsconfig.json rename to example_plugins/plugins/docs_form/tsconfig.json diff --git a/scenarios/plugins/docs_grid/.gitignore b/example_plugins/plugins/docs_grid/.gitignore similarity index 100% rename from scenarios/plugins/docs_grid/.gitignore rename to example_plugins/plugins/docs_grid/.gitignore diff --git a/scenarios/plugins/docs_grid/gauntlet.toml b/example_plugins/plugins/docs_grid/gauntlet.toml similarity index 100% rename from scenarios/plugins/docs_grid/gauntlet.toml rename to example_plugins/plugins/docs_grid/gauntlet.toml diff --git a/scenarios/plugins/docs_grid/package.json b/example_plugins/plugins/docs_grid/package.json similarity index 100% rename from scenarios/plugins/docs_grid/package.json rename to example_plugins/plugins/docs_grid/package.json diff --git a/scenarios/plugins/docs_grid/src/content_code_block.tsx b/example_plugins/plugins/docs_grid/src/content_code_block.tsx similarity index 100% rename from scenarios/plugins/docs_grid/src/content_code_block.tsx rename to example_plugins/plugins/docs_grid/src/content_code_block.tsx diff --git a/scenarios/plugins/docs_grid/src/content_headers.tsx b/example_plugins/plugins/docs_grid/src/content_headers.tsx similarity index 100% rename from scenarios/plugins/docs_grid/src/content_headers.tsx rename to example_plugins/plugins/docs_grid/src/content_headers.tsx diff --git a/scenarios/plugins/docs_grid/src/content_image.tsx b/example_plugins/plugins/docs_grid/src/content_image.tsx similarity index 100% rename from scenarios/plugins/docs_grid/src/content_image.tsx rename to example_plugins/plugins/docs_grid/src/content_image.tsx diff --git a/scenarios/plugins/docs_grid/src/content_paragraph.tsx b/example_plugins/plugins/docs_grid/src/content_paragraph.tsx similarity index 100% rename from scenarios/plugins/docs_grid/src/content_paragraph.tsx rename to example_plugins/plugins/docs_grid/src/content_paragraph.tsx diff --git a/scenarios/plugins/docs_grid/src/empty_view.tsx b/example_plugins/plugins/docs_grid/src/empty_view.tsx similarity index 100% rename from scenarios/plugins/docs_grid/src/empty_view.tsx rename to example_plugins/plugins/docs_grid/src/empty_view.tsx diff --git a/scenarios/plugins/docs_grid/src/main.tsx b/example_plugins/plugins/docs_grid/src/main.tsx similarity index 100% rename from scenarios/plugins/docs_grid/src/main.tsx rename to example_plugins/plugins/docs_grid/src/main.tsx diff --git a/scenarios/plugins/docs_grid/src/search_bar.tsx b/example_plugins/plugins/docs_grid/src/search_bar.tsx similarity index 100% rename from scenarios/plugins/docs_grid/src/search_bar.tsx rename to example_plugins/plugins/docs_grid/src/search_bar.tsx diff --git a/scenarios/plugins/docs_grid/src/section.tsx b/example_plugins/plugins/docs_grid/src/section.tsx similarity index 100% rename from scenarios/plugins/docs_grid/src/section.tsx rename to example_plugins/plugins/docs_grid/src/section.tsx diff --git a/scenarios/plugins/docs_grid/tsconfig.json b/example_plugins/plugins/docs_grid/tsconfig.json similarity index 100% rename from scenarios/plugins/docs_grid/tsconfig.json rename to example_plugins/plugins/docs_grid/tsconfig.json diff --git a/scenarios/plugins/docs_inline_separators/.gitignore b/example_plugins/plugins/docs_inline_separators/.gitignore similarity index 100% rename from scenarios/plugins/docs_inline_separators/.gitignore rename to example_plugins/plugins/docs_inline_separators/.gitignore diff --git a/scenarios/plugins/docs_inline_separators/gauntlet.toml b/example_plugins/plugins/docs_inline_separators/gauntlet.toml similarity index 100% rename from scenarios/plugins/docs_inline_separators/gauntlet.toml rename to example_plugins/plugins/docs_inline_separators/gauntlet.toml diff --git a/scenarios/plugins/docs_inline_separators/package.json b/example_plugins/plugins/docs_inline_separators/package.json similarity index 100% rename from scenarios/plugins/docs_inline_separators/package.json rename to example_plugins/plugins/docs_inline_separators/package.json diff --git a/scenarios/plugins/docs_inline_separators/src/main.tsx b/example_plugins/plugins/docs_inline_separators/src/main.tsx similarity index 100% rename from scenarios/plugins/docs_inline_separators/src/main.tsx rename to example_plugins/plugins/docs_inline_separators/src/main.tsx diff --git a/scenarios/plugins/docs_inline_separators/tsconfig.json b/example_plugins/plugins/docs_inline_separators/tsconfig.json similarity index 100% rename from scenarios/plugins/docs_inline_separators/tsconfig.json rename to example_plugins/plugins/docs_inline_separators/tsconfig.json diff --git a/scenarios/plugins/docs_inline_three_sections/.gitignore b/example_plugins/plugins/docs_inline_three_sections/.gitignore similarity index 100% rename from scenarios/plugins/docs_inline_three_sections/.gitignore rename to example_plugins/plugins/docs_inline_three_sections/.gitignore diff --git a/scenarios/plugins/docs_inline_three_sections/gauntlet.toml b/example_plugins/plugins/docs_inline_three_sections/gauntlet.toml similarity index 100% rename from scenarios/plugins/docs_inline_three_sections/gauntlet.toml rename to example_plugins/plugins/docs_inline_three_sections/gauntlet.toml diff --git a/scenarios/plugins/docs_inline_three_sections/package.json b/example_plugins/plugins/docs_inline_three_sections/package.json similarity index 100% rename from scenarios/plugins/docs_inline_three_sections/package.json rename to example_plugins/plugins/docs_inline_three_sections/package.json diff --git a/scenarios/plugins/docs_inline_three_sections/src/main.tsx b/example_plugins/plugins/docs_inline_three_sections/src/main.tsx similarity index 100% rename from scenarios/plugins/docs_inline_three_sections/src/main.tsx rename to example_plugins/plugins/docs_inline_three_sections/src/main.tsx diff --git a/scenarios/plugins/docs_inline_three_sections/tsconfig.json b/example_plugins/plugins/docs_inline_three_sections/tsconfig.json similarity index 100% rename from scenarios/plugins/docs_inline_three_sections/tsconfig.json rename to example_plugins/plugins/docs_inline_three_sections/tsconfig.json diff --git a/scenarios/plugins/docs_inline_two_sections/.gitignore b/example_plugins/plugins/docs_inline_two_sections/.gitignore similarity index 100% rename from scenarios/plugins/docs_inline_two_sections/.gitignore rename to example_plugins/plugins/docs_inline_two_sections/.gitignore diff --git a/scenarios/plugins/docs_inline_two_sections/gauntlet.toml b/example_plugins/plugins/docs_inline_two_sections/gauntlet.toml similarity index 100% rename from scenarios/plugins/docs_inline_two_sections/gauntlet.toml rename to example_plugins/plugins/docs_inline_two_sections/gauntlet.toml diff --git a/scenarios/plugins/docs_inline_two_sections/package.json b/example_plugins/plugins/docs_inline_two_sections/package.json similarity index 100% rename from scenarios/plugins/docs_inline_two_sections/package.json rename to example_plugins/plugins/docs_inline_two_sections/package.json diff --git a/scenarios/plugins/docs_inline_two_sections/src/main.tsx b/example_plugins/plugins/docs_inline_two_sections/src/main.tsx similarity index 100% rename from scenarios/plugins/docs_inline_two_sections/src/main.tsx rename to example_plugins/plugins/docs_inline_two_sections/src/main.tsx diff --git a/scenarios/plugins/docs_inline_two_sections/tsconfig.json b/example_plugins/plugins/docs_inline_two_sections/tsconfig.json similarity index 100% rename from scenarios/plugins/docs_inline_two_sections/tsconfig.json rename to example_plugins/plugins/docs_inline_two_sections/tsconfig.json diff --git a/scenarios/plugins/docs_list/.gitignore b/example_plugins/plugins/docs_list/.gitignore similarity index 100% rename from scenarios/plugins/docs_list/.gitignore rename to example_plugins/plugins/docs_list/.gitignore diff --git a/scenarios/plugins/docs_list/gauntlet.toml b/example_plugins/plugins/docs_list/gauntlet.toml similarity index 100% rename from scenarios/plugins/docs_list/gauntlet.toml rename to example_plugins/plugins/docs_list/gauntlet.toml diff --git a/scenarios/plugins/docs_list/package.json b/example_plugins/plugins/docs_list/package.json similarity index 100% rename from scenarios/plugins/docs_list/package.json rename to example_plugins/plugins/docs_list/package.json diff --git a/scenarios/plugins/docs_list/src/detail.tsx b/example_plugins/plugins/docs_list/src/detail.tsx similarity index 100% rename from scenarios/plugins/docs_list/src/detail.tsx rename to example_plugins/plugins/docs_list/src/detail.tsx diff --git a/scenarios/plugins/docs_list/src/empty_view.tsx b/example_plugins/plugins/docs_list/src/empty_view.tsx similarity index 100% rename from scenarios/plugins/docs_list/src/empty_view.tsx rename to example_plugins/plugins/docs_list/src/empty_view.tsx diff --git a/scenarios/plugins/docs_list/src/main.tsx b/example_plugins/plugins/docs_list/src/main.tsx similarity index 100% rename from scenarios/plugins/docs_list/src/main.tsx rename to example_plugins/plugins/docs_list/src/main.tsx diff --git a/scenarios/plugins/docs_list/src/search_bar.tsx b/example_plugins/plugins/docs_list/src/search_bar.tsx similarity index 100% rename from scenarios/plugins/docs_list/src/search_bar.tsx rename to example_plugins/plugins/docs_list/src/search_bar.tsx diff --git a/scenarios/plugins/docs_list/src/section.tsx b/example_plugins/plugins/docs_list/src/section.tsx similarity index 100% rename from scenarios/plugins/docs_list/src/section.tsx rename to example_plugins/plugins/docs_list/src/section.tsx diff --git a/scenarios/plugins/docs_list/tsconfig.json b/example_plugins/plugins/docs_list/tsconfig.json similarity index 100% rename from scenarios/plugins/docs_list/tsconfig.json rename to example_plugins/plugins/docs_list/tsconfig.json diff --git a/scenarios/data/docs_detail/content-code-block/default.json b/example_plugins/scenarios/docs_detail/content-code-block/default.json similarity index 100% rename from scenarios/data/docs_detail/content-code-block/default.json rename to example_plugins/scenarios/docs_detail/content-code-block/default.json diff --git a/scenarios/data/docs_detail/content-header/default.json b/example_plugins/scenarios/docs_detail/content-header/default.json similarity index 100% rename from scenarios/data/docs_detail/content-header/default.json rename to example_plugins/scenarios/docs_detail/content-header/default.json diff --git a/scenarios/data/docs_detail/content-horizontal-break/default.json b/example_plugins/scenarios/docs_detail/content-horizontal-break/default.json similarity index 100% rename from scenarios/data/docs_detail/content-horizontal-break/default.json rename to example_plugins/scenarios/docs_detail/content-horizontal-break/default.json diff --git a/scenarios/data/docs_detail/content-image/default.json b/example_plugins/scenarios/docs_detail/content-image/default.json similarity index 100% rename from scenarios/data/docs_detail/content-image/default.json rename to example_plugins/scenarios/docs_detail/content-image/default.json diff --git a/scenarios/data/docs_detail/content-paragraph/default.json b/example_plugins/scenarios/docs_detail/content-paragraph/default.json similarity index 100% rename from scenarios/data/docs_detail/content-paragraph/default.json rename to example_plugins/scenarios/docs_detail/content-paragraph/default.json diff --git a/scenarios/data/docs_detail/content/default.json b/example_plugins/scenarios/docs_detail/content/default.json similarity index 100% rename from scenarios/data/docs_detail/content/default.json rename to example_plugins/scenarios/docs_detail/content/default.json diff --git a/scenarios/data/docs_detail/main/default.json b/example_plugins/scenarios/docs_detail/main/default.json similarity index 100% rename from scenarios/data/docs_detail/main/default.json rename to example_plugins/scenarios/docs_detail/main/default.json diff --git a/scenarios/data/docs_detail/metadata-icon/default.json b/example_plugins/scenarios/docs_detail/metadata-icon/default.json similarity index 100% rename from scenarios/data/docs_detail/metadata-icon/default.json rename to example_plugins/scenarios/docs_detail/metadata-icon/default.json diff --git a/scenarios/data/docs_detail/metadata-link/default.json b/example_plugins/scenarios/docs_detail/metadata-link/default.json similarity index 100% rename from scenarios/data/docs_detail/metadata-link/default.json rename to example_plugins/scenarios/docs_detail/metadata-link/default.json diff --git a/scenarios/data/docs_detail/metadata-separator/default.json b/example_plugins/scenarios/docs_detail/metadata-separator/default.json similarity index 100% rename from scenarios/data/docs_detail/metadata-separator/default.json rename to example_plugins/scenarios/docs_detail/metadata-separator/default.json diff --git a/scenarios/data/docs_detail/metadata-tag-list/default.json b/example_plugins/scenarios/docs_detail/metadata-tag-list/default.json similarity index 100% rename from scenarios/data/docs_detail/metadata-tag-list/default.json rename to example_plugins/scenarios/docs_detail/metadata-tag-list/default.json diff --git a/scenarios/data/docs_detail/metadata-value/default.json b/example_plugins/scenarios/docs_detail/metadata-value/default.json similarity index 100% rename from scenarios/data/docs_detail/metadata-value/default.json rename to example_plugins/scenarios/docs_detail/metadata-value/default.json diff --git a/scenarios/data/docs_detail/metadata/default.json b/example_plugins/scenarios/docs_detail/metadata/default.json similarity index 100% rename from scenarios/data/docs_detail/metadata/default.json rename to example_plugins/scenarios/docs_detail/metadata/default.json diff --git a/scenarios/data/docs_form/checkbox/default.json b/example_plugins/scenarios/docs_form/checkbox/default.json similarity index 100% rename from scenarios/data/docs_form/checkbox/default.json rename to example_plugins/scenarios/docs_form/checkbox/default.json diff --git a/scenarios/data/docs_form/date-picker/default.json b/example_plugins/scenarios/docs_form/date-picker/default.json similarity index 100% rename from scenarios/data/docs_form/date-picker/default.json rename to example_plugins/scenarios/docs_form/date-picker/default.json diff --git a/scenarios/data/docs_form/main/default.json b/example_plugins/scenarios/docs_form/main/default.json similarity index 100% rename from scenarios/data/docs_form/main/default.json rename to example_plugins/scenarios/docs_form/main/default.json diff --git a/scenarios/data/docs_form/password-field/default.json b/example_plugins/scenarios/docs_form/password-field/default.json similarity index 100% rename from scenarios/data/docs_form/password-field/default.json rename to example_plugins/scenarios/docs_form/password-field/default.json diff --git a/scenarios/data/docs_form/select/default.json b/example_plugins/scenarios/docs_form/select/default.json similarity index 100% rename from scenarios/data/docs_form/select/default.json rename to example_plugins/scenarios/docs_form/select/default.json diff --git a/scenarios/data/docs_form/separator/default.json b/example_plugins/scenarios/docs_form/separator/default.json similarity index 100% rename from scenarios/data/docs_form/separator/default.json rename to example_plugins/scenarios/docs_form/separator/default.json diff --git a/scenarios/data/docs_form/text-field/default.json b/example_plugins/scenarios/docs_form/text-field/default.json similarity index 100% rename from scenarios/data/docs_form/text-field/default.json rename to example_plugins/scenarios/docs_form/text-field/default.json diff --git a/scenarios/data/docs_grid/content-code-block/default.json b/example_plugins/scenarios/docs_grid/content-code-block/default.json similarity index 100% rename from scenarios/data/docs_grid/content-code-block/default.json rename to example_plugins/scenarios/docs_grid/content-code-block/default.json diff --git a/scenarios/data/docs_grid/content-headers/default.json b/example_plugins/scenarios/docs_grid/content-headers/default.json similarity index 100% rename from scenarios/data/docs_grid/content-headers/default.json rename to example_plugins/scenarios/docs_grid/content-headers/default.json diff --git a/scenarios/data/docs_grid/content-image/default.json b/example_plugins/scenarios/docs_grid/content-image/default.json similarity index 100% rename from scenarios/data/docs_grid/content-image/default.json rename to example_plugins/scenarios/docs_grid/content-image/default.json diff --git a/scenarios/data/docs_grid/content-paragraph/default.json b/example_plugins/scenarios/docs_grid/content-paragraph/default.json similarity index 100% rename from scenarios/data/docs_grid/content-paragraph/default.json rename to example_plugins/scenarios/docs_grid/content-paragraph/default.json diff --git a/scenarios/data/docs_grid/empty-view/default.json b/example_plugins/scenarios/docs_grid/empty-view/default.json similarity index 100% rename from scenarios/data/docs_grid/empty-view/default.json rename to example_plugins/scenarios/docs_grid/empty-view/default.json diff --git a/scenarios/data/docs_grid/main/default.json b/example_plugins/scenarios/docs_grid/main/default.json similarity index 100% rename from scenarios/data/docs_grid/main/default.json rename to example_plugins/scenarios/docs_grid/main/default.json diff --git a/scenarios/data/docs_grid/search-bar/default.json b/example_plugins/scenarios/docs_grid/search-bar/default.json similarity index 100% rename from scenarios/data/docs_grid/search-bar/default.json rename to example_plugins/scenarios/docs_grid/search-bar/default.json diff --git a/scenarios/data/docs_grid/section/default.json b/example_plugins/scenarios/docs_grid/section/default.json similarity index 100% rename from scenarios/data/docs_grid/section/default.json rename to example_plugins/scenarios/docs_grid/section/default.json diff --git a/scenarios/data/docs_inline_separators/main/separator.json b/example_plugins/scenarios/docs_inline_separators/main/separator.json similarity index 100% rename from scenarios/data/docs_inline_separators/main/separator.json rename to example_plugins/scenarios/docs_inline_separators/main/separator.json diff --git a/scenarios/data/docs_inline_three_sections/main/three-sections.json b/example_plugins/scenarios/docs_inline_three_sections/main/three-sections.json similarity index 100% rename from scenarios/data/docs_inline_three_sections/main/three-sections.json rename to example_plugins/scenarios/docs_inline_three_sections/main/three-sections.json diff --git a/scenarios/data/docs_inline_two_sections/main/two-sections.json b/example_plugins/scenarios/docs_inline_two_sections/main/two-sections.json similarity index 100% rename from scenarios/data/docs_inline_two_sections/main/two-sections.json rename to example_plugins/scenarios/docs_inline_two_sections/main/two-sections.json diff --git a/scenarios/data/docs_list/detail/default.json b/example_plugins/scenarios/docs_list/detail/default.json similarity index 100% rename from scenarios/data/docs_list/detail/default.json rename to example_plugins/scenarios/docs_list/detail/default.json diff --git a/scenarios/data/docs_list/empty-view/default.json b/example_plugins/scenarios/docs_list/empty-view/default.json similarity index 100% rename from scenarios/data/docs_list/empty-view/default.json rename to example_plugins/scenarios/docs_list/empty-view/default.json diff --git a/scenarios/data/docs_list/main/default.json b/example_plugins/scenarios/docs_list/main/default.json similarity index 100% rename from scenarios/data/docs_list/main/default.json rename to example_plugins/scenarios/docs_list/main/default.json diff --git a/scenarios/data/docs_list/search-bar/default.json b/example_plugins/scenarios/docs_list/search-bar/default.json similarity index 100% rename from scenarios/data/docs_list/search-bar/default.json rename to example_plugins/scenarios/docs_list/search-bar/default.json diff --git a/scenarios/data/docs_list/section/default.json b/example_plugins/scenarios/docs_list/section/default.json similarity index 100% rename from scenarios/data/docs_list/section/default.json rename to example_plugins/scenarios/docs_list/section/default.json diff --git a/js/scenario_runner_cli/src/main.ts b/js/scenario_runner_cli/src/main.ts index 01923e2..abc193e 100644 --- a/js/scenario_runner_cli/src/main.ts +++ b/js/scenario_runner_cli/src/main.ts @@ -31,8 +31,8 @@ async function sleep(ms: number) { async function runScenarios(expectedPlugin: string | undefined) { const projectRoot = path.resolve(process.cwd(), '..', '..'); - const scenarios = path.join(projectRoot, "scenarios"); - const scenariosData = path.join(scenarios, "data"); + const scenarios = path.join(projectRoot, "example_plugins"); + const scenariosData = path.join(scenarios, "scenarios"); const scenariosRun = path.join(scenarios, "run"); console.log("Building server") @@ -78,7 +78,7 @@ async function runScenarios(expectedPlugin: string | undefined) { async function runScreenshotGen(expectedPlugin: string | undefined, expectedEntrypoint: string | undefined) { const projectRoot = path.resolve(process.cwd(), '..', '..'); - const scenarios = path.join(projectRoot, "scenarios"); + const scenarios = path.join(projectRoot, "example_plugins"); const scenariosOut = path.join(scenarios, "out"); for (const plugin of readdirSync(scenariosOut)) { diff --git a/package-lock.json b/package-lock.json index 519073c..be00d1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "workspaces": [ "dev_plugin", "bundled_plugins/*", - "scenarios/plugins/*", + "example_plugins/plugins/*", "js/typings", "js/build", "js/api_build", @@ -48,6 +48,119 @@ "typescript": "^5.7.2" } }, + "example_plugins/js/api": {}, + "example_plugins/plugins/docs_detail": { + "name": "@project-gauntlet/docs-detailt", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/docs_detail/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/docs_form": { + "name": "@project-gauntlet/docs-form", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/docs_form/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/docs_grid": { + "name": "@project-gauntlet/docs-grid", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/docs_grid/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/docs_inline_separators": { + "name": "@project-gauntlet/docs-inline-separators", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/docs_inline_separators/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/docs_inline_three_sections": { + "name": "@project-gauntlet/docs-inline-three-sections", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/docs_inline_two_sections": { + "name": "@project-gauntlet/docs-inline-two-sections", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/docs_list": { + "name": "@project-gauntlet/docs-list", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/docs_list/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, "js/api": { "name": "@project-gauntlet/api", "version": "0.0.0", @@ -675,31 +788,31 @@ "link": true }, "node_modules/@project-gauntlet/docs-detailt": { - "resolved": "scenarios/plugins/docs_detail", + "resolved": "example_plugins/plugins/docs_detail", "link": true }, "node_modules/@project-gauntlet/docs-form": { - "resolved": "scenarios/plugins/docs_form", + "resolved": "example_plugins/plugins/docs_form", "link": true }, "node_modules/@project-gauntlet/docs-grid": { - "resolved": "scenarios/plugins/docs_grid", + "resolved": "example_plugins/plugins/docs_grid", "link": true }, "node_modules/@project-gauntlet/docs-inline-separators": { - "resolved": "scenarios/plugins/docs_inline_separators", + "resolved": "example_plugins/plugins/docs_inline_separators", "link": true }, "node_modules/@project-gauntlet/docs-inline-three-sections": { - "resolved": "scenarios/plugins/docs_inline_three_sections", + "resolved": "example_plugins/plugins/docs_inline_three_sections", "link": true }, "node_modules/@project-gauntlet/docs-inline-two-sections": { - "resolved": "scenarios/plugins/docs_inline_two_sections", + "resolved": "example_plugins/plugins/docs_inline_two_sections", "link": true }, "node_modules/@project-gauntlet/docs-list": { - "resolved": "scenarios/plugins/docs_list", + "resolved": "example_plugins/plugins/docs_list", "link": true }, "node_modules/@project-gauntlet/react": { @@ -2829,9 +2942,12 @@ "zod": "^3.18.0" } }, - "scenarios/js/api": {}, + "scenarios/js/api": { + "extraneous": true + }, "scenarios/plugins/docs_detail": { "name": "@project-gauntlet/docs-detailt", + "extraneous": true, "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -2842,12 +2958,9 @@ "typescript": "*" } }, - "scenarios/plugins/docs_detail/node_modules/@project-gauntlet/api": { - "resolved": "scenarios/js/api", - "link": true - }, "scenarios/plugins/docs_form": { "name": "@project-gauntlet/docs-form", + "extraneous": true, "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -2858,12 +2971,9 @@ "typescript": "*" } }, - "scenarios/plugins/docs_form/node_modules/@project-gauntlet/api": { - "resolved": "scenarios/js/api", - "link": true - }, "scenarios/plugins/docs_grid": { "name": "@project-gauntlet/docs-grid", + "extraneous": true, "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -2874,12 +2984,9 @@ "typescript": "*" } }, - "scenarios/plugins/docs_grid/node_modules/@project-gauntlet/api": { - "resolved": "scenarios/js/api", - "link": true - }, "scenarios/plugins/docs_inline_separators": { "name": "@project-gauntlet/docs-inline-separators", + "extraneous": true, "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -2890,12 +2997,9 @@ "typescript": "*" } }, - "scenarios/plugins/docs_inline_separators/node_modules/@project-gauntlet/api": { - "resolved": "scenarios/js/api", - "link": true - }, "scenarios/plugins/docs_inline_three_sections": { "name": "@project-gauntlet/docs-inline-three-sections", + "extraneous": true, "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -2906,12 +3010,9 @@ "typescript": "*" } }, - "scenarios/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/api": { - "resolved": "scenarios/js/api", - "link": true - }, "scenarios/plugins/docs_inline_two_sections": { "name": "@project-gauntlet/docs-inline-two-sections", + "extraneous": true, "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -2922,12 +3023,9 @@ "typescript": "*" } }, - "scenarios/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/api": { - "resolved": "scenarios/js/api", - "link": true - }, "scenarios/plugins/docs_list": { "name": "@project-gauntlet/docs-list", + "extraneous": true, "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -2937,10 +3035,6 @@ "@types/react": "*", "typescript": "*" } - }, - "scenarios/plugins/docs_list/node_modules/@project-gauntlet/api": { - "resolved": "scenarios/js/api", - "link": true } } } diff --git a/package.json b/package.json index d1e35bf..cf422ab 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "scripts": { "build-all": "npm run build --workspaces --if-present", - "build-scenarios": "npm run build --workspace scenarios --if-present", + "build-scenarios": "npm run build --workspace example_plugins --if-present", "build-dev-plugin": "npm run build --workspace dev_plugin", "build": "npm run build --workspace js --workspace bundled_plugins --if-present", "run-scenarios": "npm run run-scenarios --workspace js/scenario_runner_cli", @@ -12,7 +12,7 @@ "workspaces": [ "dev_plugin", "bundled_plugins/*", - "scenarios/plugins/*", + "example_plugins/plugins/*", "js/typings", "js/build", "js/api_build", diff --git a/rust/scenario_runner/src/frontend_mock.rs b/rust/scenario_runner/src/frontend_mock.rs index c108972..29431e1 100644 --- a/rust/scenario_runner/src/frontend_mock.rs +++ b/rust/scenario_runner/src/frontend_mock.rs @@ -36,7 +36,7 @@ pub async fn start_scenario_runner_frontend( .to_string(); let scenario_data_dir = scenario_dir - .join("data") + .join("scenarios") .join(&plugin_name) .to_str() .expect("scenario_data_dir is invalid UTF-8") From d7d87d0829d8ac48f9a560ad3531529a6991eebd Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:35:45 +0100 Subject: [PATCH 392/540] Rename example plugins from docs_* to ui_* --- example_plugins/plugins/{docs_detail => ui_detail}/.gitignore | 0 example_plugins/plugins/{docs_detail => ui_detail}/gauntlet.toml | 0 example_plugins/plugins/{docs_detail => ui_detail}/package.json | 0 .../plugins/{docs_detail => ui_detail}/src/content.tsx | 0 .../plugins/{docs_detail => ui_detail}/src/content_code_block.tsx | 0 .../plugins/{docs_detail => ui_detail}/src/content_header.tsx | 0 .../{docs_detail => ui_detail}/src/content_horizontal_break.tsx | 0 .../plugins/{docs_detail => ui_detail}/src/content_image.tsx | 0 .../plugins/{docs_detail => ui_detail}/src/content_paragraph.tsx | 0 example_plugins/plugins/{docs_detail => ui_detail}/src/main.tsx | 0 .../plugins/{docs_detail => ui_detail}/src/metadata.tsx | 0 .../plugins/{docs_detail => ui_detail}/src/metadata_icon.tsx | 0 .../plugins/{docs_detail => ui_detail}/src/metadata_link.tsx | 0 .../plugins/{docs_detail => ui_detail}/src/metadata_separator.tsx | 0 .../plugins/{docs_detail => ui_detail}/src/metadata_tag_list.tsx | 0 .../plugins/{docs_detail => ui_detail}/src/metadata_value.tsx | 0 example_plugins/plugins/{docs_detail => ui_detail}/tsconfig.json | 0 example_plugins/plugins/{docs_form => ui_form}/.gitignore | 0 example_plugins/plugins/{docs_form => ui_form}/gauntlet.toml | 0 example_plugins/plugins/{docs_form => ui_form}/package.json | 0 example_plugins/plugins/{docs_form => ui_form}/src/checkbox.tsx | 0 .../plugins/{docs_form => ui_form}/src/date-picker.tsx | 0 example_plugins/plugins/{docs_form => ui_form}/src/main.tsx | 0 .../plugins/{docs_form => ui_form}/src/password-field.tsx | 0 example_plugins/plugins/{docs_form => ui_form}/src/select.tsx | 0 example_plugins/plugins/{docs_form => ui_form}/src/separator.tsx | 0 example_plugins/plugins/{docs_form => ui_form}/src/text-field.tsx | 0 example_plugins/plugins/{docs_form => ui_form}/tsconfig.json | 0 example_plugins/plugins/{docs_grid => ui_grid}/.gitignore | 0 example_plugins/plugins/{docs_grid => ui_grid}/gauntlet.toml | 0 example_plugins/plugins/{docs_grid => ui_grid}/package.json | 0 .../plugins/{docs_grid => ui_grid}/src/content_code_block.tsx | 0 .../plugins/{docs_grid => ui_grid}/src/content_headers.tsx | 0 .../plugins/{docs_grid => ui_grid}/src/content_image.tsx | 0 .../plugins/{docs_grid => ui_grid}/src/content_paragraph.tsx | 0 example_plugins/plugins/{docs_grid => ui_grid}/src/empty_view.tsx | 0 example_plugins/plugins/{docs_grid => ui_grid}/src/main.tsx | 0 example_plugins/plugins/{docs_grid => ui_grid}/src/search_bar.tsx | 0 example_plugins/plugins/{docs_grid => ui_grid}/src/section.tsx | 0 example_plugins/plugins/{docs_grid => ui_grid}/tsconfig.json | 0 .../{docs_inline_separators => ui_inline_separators}/.gitignore | 0 .../gauntlet.toml | 0 .../{docs_inline_separators => ui_inline_separators}/package.json | 0 .../{docs_inline_separators => ui_inline_separators}/src/main.tsx | 0 .../tsconfig.json | 0 .../.gitignore | 0 .../gauntlet.toml | 0 .../package.json | 0 .../src/main.tsx | 0 .../tsconfig.json | 0 .../.gitignore | 0 .../gauntlet.toml | 0 .../package.json | 0 .../src/main.tsx | 0 .../tsconfig.json | 0 example_plugins/plugins/{docs_list => ui_list}/.gitignore | 0 example_plugins/plugins/{docs_list => ui_list}/gauntlet.toml | 0 example_plugins/plugins/{docs_list => ui_list}/package.json | 0 example_plugins/plugins/{docs_list => ui_list}/src/detail.tsx | 0 example_plugins/plugins/{docs_list => ui_list}/src/empty_view.tsx | 0 example_plugins/plugins/{docs_list => ui_list}/src/main.tsx | 0 example_plugins/plugins/{docs_list => ui_list}/src/search_bar.tsx | 0 example_plugins/plugins/{docs_list => ui_list}/src/section.tsx | 0 example_plugins/plugins/{docs_list => ui_list}/tsconfig.json | 0 .../{docs_detail => ui_detail}/content-code-block/default.json | 0 .../{docs_detail => ui_detail}/content-header/default.json | 0 .../content-horizontal-break/default.json | 0 .../{docs_detail => ui_detail}/content-image/default.json | 0 .../{docs_detail => ui_detail}/content-paragraph/default.json | 0 .../scenarios/{docs_detail => ui_detail}/content/default.json | 0 .../scenarios/{docs_detail => ui_detail}/main/default.json | 0 .../{docs_detail => ui_detail}/metadata-icon/default.json | 0 .../{docs_detail => ui_detail}/metadata-link/default.json | 0 .../{docs_detail => ui_detail}/metadata-separator/default.json | 0 .../{docs_detail => ui_detail}/metadata-tag-list/default.json | 0 .../{docs_detail => ui_detail}/metadata-value/default.json | 0 .../scenarios/{docs_detail => ui_detail}/metadata/default.json | 0 .../scenarios/{docs_form => ui_form}/checkbox/default.json | 0 .../scenarios/{docs_form => ui_form}/date-picker/default.json | 0 .../scenarios/{docs_form => ui_form}/main/default.json | 0 .../scenarios/{docs_form => ui_form}/password-field/default.json | 0 .../scenarios/{docs_form => ui_form}/select/default.json | 0 .../scenarios/{docs_form => ui_form}/separator/default.json | 0 .../scenarios/{docs_form => ui_form}/text-field/default.json | 0 .../{docs_grid => ui_grid}/content-code-block/default.json | 0 .../scenarios/{docs_grid => ui_grid}/content-headers/default.json | 0 .../scenarios/{docs_grid => ui_grid}/content-image/default.json | 0 .../{docs_grid => ui_grid}/content-paragraph/default.json | 0 .../scenarios/{docs_grid => ui_grid}/empty-view/default.json | 0 .../scenarios/{docs_grid => ui_grid}/main/default.json | 0 .../scenarios/{docs_grid => ui_grid}/search-bar/default.json | 0 .../scenarios/{docs_grid => ui_grid}/section/default.json | 0 .../main/separator.json | 0 .../main/three-sections.json | 0 .../main/two-sections.json | 0 .../scenarios/{docs_list => ui_list}/detail/default.json | 0 .../scenarios/{docs_list => ui_list}/empty-view/default.json | 0 .../scenarios/{docs_list => ui_list}/main/default.json | 0 .../scenarios/{docs_list => ui_list}/search-bar/default.json | 0 .../scenarios/{docs_list => ui_list}/section/default.json | 0 100 files changed, 0 insertions(+), 0 deletions(-) rename example_plugins/plugins/{docs_detail => ui_detail}/.gitignore (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/gauntlet.toml (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/package.json (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/content.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/content_code_block.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/content_header.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/content_horizontal_break.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/content_image.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/content_paragraph.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/main.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/metadata.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/metadata_icon.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/metadata_link.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/metadata_separator.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/metadata_tag_list.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/src/metadata_value.tsx (100%) rename example_plugins/plugins/{docs_detail => ui_detail}/tsconfig.json (100%) rename example_plugins/plugins/{docs_form => ui_form}/.gitignore (100%) rename example_plugins/plugins/{docs_form => ui_form}/gauntlet.toml (100%) rename example_plugins/plugins/{docs_form => ui_form}/package.json (100%) rename example_plugins/plugins/{docs_form => ui_form}/src/checkbox.tsx (100%) rename example_plugins/plugins/{docs_form => ui_form}/src/date-picker.tsx (100%) rename example_plugins/plugins/{docs_form => ui_form}/src/main.tsx (100%) rename example_plugins/plugins/{docs_form => ui_form}/src/password-field.tsx (100%) rename example_plugins/plugins/{docs_form => ui_form}/src/select.tsx (100%) rename example_plugins/plugins/{docs_form => ui_form}/src/separator.tsx (100%) rename example_plugins/plugins/{docs_form => ui_form}/src/text-field.tsx (100%) rename example_plugins/plugins/{docs_form => ui_form}/tsconfig.json (100%) rename example_plugins/plugins/{docs_grid => ui_grid}/.gitignore (100%) rename example_plugins/plugins/{docs_grid => ui_grid}/gauntlet.toml (100%) rename example_plugins/plugins/{docs_grid => ui_grid}/package.json (100%) rename example_plugins/plugins/{docs_grid => ui_grid}/src/content_code_block.tsx (100%) rename example_plugins/plugins/{docs_grid => ui_grid}/src/content_headers.tsx (100%) rename example_plugins/plugins/{docs_grid => ui_grid}/src/content_image.tsx (100%) rename example_plugins/plugins/{docs_grid => ui_grid}/src/content_paragraph.tsx (100%) rename example_plugins/plugins/{docs_grid => ui_grid}/src/empty_view.tsx (100%) rename example_plugins/plugins/{docs_grid => ui_grid}/src/main.tsx (100%) rename example_plugins/plugins/{docs_grid => ui_grid}/src/search_bar.tsx (100%) rename example_plugins/plugins/{docs_grid => ui_grid}/src/section.tsx (100%) rename example_plugins/plugins/{docs_grid => ui_grid}/tsconfig.json (100%) rename example_plugins/plugins/{docs_inline_separators => ui_inline_separators}/.gitignore (100%) rename example_plugins/plugins/{docs_inline_separators => ui_inline_separators}/gauntlet.toml (100%) rename example_plugins/plugins/{docs_inline_separators => ui_inline_separators}/package.json (100%) rename example_plugins/plugins/{docs_inline_separators => ui_inline_separators}/src/main.tsx (100%) rename example_plugins/plugins/{docs_inline_separators => ui_inline_separators}/tsconfig.json (100%) rename example_plugins/plugins/{docs_inline_three_sections => ui_inline_three_sections}/.gitignore (100%) rename example_plugins/plugins/{docs_inline_three_sections => ui_inline_three_sections}/gauntlet.toml (100%) rename example_plugins/plugins/{docs_inline_three_sections => ui_inline_three_sections}/package.json (100%) rename example_plugins/plugins/{docs_inline_three_sections => ui_inline_three_sections}/src/main.tsx (100%) rename example_plugins/plugins/{docs_inline_three_sections => ui_inline_three_sections}/tsconfig.json (100%) rename example_plugins/plugins/{docs_inline_two_sections => ui_inline_two_sections}/.gitignore (100%) rename example_plugins/plugins/{docs_inline_two_sections => ui_inline_two_sections}/gauntlet.toml (100%) rename example_plugins/plugins/{docs_inline_two_sections => ui_inline_two_sections}/package.json (100%) rename example_plugins/plugins/{docs_inline_two_sections => ui_inline_two_sections}/src/main.tsx (100%) rename example_plugins/plugins/{docs_inline_two_sections => ui_inline_two_sections}/tsconfig.json (100%) rename example_plugins/plugins/{docs_list => ui_list}/.gitignore (100%) rename example_plugins/plugins/{docs_list => ui_list}/gauntlet.toml (100%) rename example_plugins/plugins/{docs_list => ui_list}/package.json (100%) rename example_plugins/plugins/{docs_list => ui_list}/src/detail.tsx (100%) rename example_plugins/plugins/{docs_list => ui_list}/src/empty_view.tsx (100%) rename example_plugins/plugins/{docs_list => ui_list}/src/main.tsx (100%) rename example_plugins/plugins/{docs_list => ui_list}/src/search_bar.tsx (100%) rename example_plugins/plugins/{docs_list => ui_list}/src/section.tsx (100%) rename example_plugins/plugins/{docs_list => ui_list}/tsconfig.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/content-code-block/default.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/content-header/default.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/content-horizontal-break/default.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/content-image/default.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/content-paragraph/default.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/content/default.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/main/default.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/metadata-icon/default.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/metadata-link/default.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/metadata-separator/default.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/metadata-tag-list/default.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/metadata-value/default.json (100%) rename example_plugins/scenarios/{docs_detail => ui_detail}/metadata/default.json (100%) rename example_plugins/scenarios/{docs_form => ui_form}/checkbox/default.json (100%) rename example_plugins/scenarios/{docs_form => ui_form}/date-picker/default.json (100%) rename example_plugins/scenarios/{docs_form => ui_form}/main/default.json (100%) rename example_plugins/scenarios/{docs_form => ui_form}/password-field/default.json (100%) rename example_plugins/scenarios/{docs_form => ui_form}/select/default.json (100%) rename example_plugins/scenarios/{docs_form => ui_form}/separator/default.json (100%) rename example_plugins/scenarios/{docs_form => ui_form}/text-field/default.json (100%) rename example_plugins/scenarios/{docs_grid => ui_grid}/content-code-block/default.json (100%) rename example_plugins/scenarios/{docs_grid => ui_grid}/content-headers/default.json (100%) rename example_plugins/scenarios/{docs_grid => ui_grid}/content-image/default.json (100%) rename example_plugins/scenarios/{docs_grid => ui_grid}/content-paragraph/default.json (100%) rename example_plugins/scenarios/{docs_grid => ui_grid}/empty-view/default.json (100%) rename example_plugins/scenarios/{docs_grid => ui_grid}/main/default.json (100%) rename example_plugins/scenarios/{docs_grid => ui_grid}/search-bar/default.json (100%) rename example_plugins/scenarios/{docs_grid => ui_grid}/section/default.json (100%) rename example_plugins/scenarios/{docs_inline_separators => ui_inline_separators}/main/separator.json (100%) rename example_plugins/scenarios/{docs_inline_three_sections => ui_inline_three_sections}/main/three-sections.json (100%) rename example_plugins/scenarios/{docs_inline_two_sections => ui_inline_two_sections}/main/two-sections.json (100%) rename example_plugins/scenarios/{docs_list => ui_list}/detail/default.json (100%) rename example_plugins/scenarios/{docs_list => ui_list}/empty-view/default.json (100%) rename example_plugins/scenarios/{docs_list => ui_list}/main/default.json (100%) rename example_plugins/scenarios/{docs_list => ui_list}/search-bar/default.json (100%) rename example_plugins/scenarios/{docs_list => ui_list}/section/default.json (100%) diff --git a/example_plugins/plugins/docs_detail/.gitignore b/example_plugins/plugins/ui_detail/.gitignore similarity index 100% rename from example_plugins/plugins/docs_detail/.gitignore rename to example_plugins/plugins/ui_detail/.gitignore diff --git a/example_plugins/plugins/docs_detail/gauntlet.toml b/example_plugins/plugins/ui_detail/gauntlet.toml similarity index 100% rename from example_plugins/plugins/docs_detail/gauntlet.toml rename to example_plugins/plugins/ui_detail/gauntlet.toml diff --git a/example_plugins/plugins/docs_detail/package.json b/example_plugins/plugins/ui_detail/package.json similarity index 100% rename from example_plugins/plugins/docs_detail/package.json rename to example_plugins/plugins/ui_detail/package.json diff --git a/example_plugins/plugins/docs_detail/src/content.tsx b/example_plugins/plugins/ui_detail/src/content.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/content.tsx rename to example_plugins/plugins/ui_detail/src/content.tsx diff --git a/example_plugins/plugins/docs_detail/src/content_code_block.tsx b/example_plugins/plugins/ui_detail/src/content_code_block.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/content_code_block.tsx rename to example_plugins/plugins/ui_detail/src/content_code_block.tsx diff --git a/example_plugins/plugins/docs_detail/src/content_header.tsx b/example_plugins/plugins/ui_detail/src/content_header.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/content_header.tsx rename to example_plugins/plugins/ui_detail/src/content_header.tsx diff --git a/example_plugins/plugins/docs_detail/src/content_horizontal_break.tsx b/example_plugins/plugins/ui_detail/src/content_horizontal_break.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/content_horizontal_break.tsx rename to example_plugins/plugins/ui_detail/src/content_horizontal_break.tsx diff --git a/example_plugins/plugins/docs_detail/src/content_image.tsx b/example_plugins/plugins/ui_detail/src/content_image.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/content_image.tsx rename to example_plugins/plugins/ui_detail/src/content_image.tsx diff --git a/example_plugins/plugins/docs_detail/src/content_paragraph.tsx b/example_plugins/plugins/ui_detail/src/content_paragraph.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/content_paragraph.tsx rename to example_plugins/plugins/ui_detail/src/content_paragraph.tsx diff --git a/example_plugins/plugins/docs_detail/src/main.tsx b/example_plugins/plugins/ui_detail/src/main.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/main.tsx rename to example_plugins/plugins/ui_detail/src/main.tsx diff --git a/example_plugins/plugins/docs_detail/src/metadata.tsx b/example_plugins/plugins/ui_detail/src/metadata.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/metadata.tsx rename to example_plugins/plugins/ui_detail/src/metadata.tsx diff --git a/example_plugins/plugins/docs_detail/src/metadata_icon.tsx b/example_plugins/plugins/ui_detail/src/metadata_icon.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/metadata_icon.tsx rename to example_plugins/plugins/ui_detail/src/metadata_icon.tsx diff --git a/example_plugins/plugins/docs_detail/src/metadata_link.tsx b/example_plugins/plugins/ui_detail/src/metadata_link.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/metadata_link.tsx rename to example_plugins/plugins/ui_detail/src/metadata_link.tsx diff --git a/example_plugins/plugins/docs_detail/src/metadata_separator.tsx b/example_plugins/plugins/ui_detail/src/metadata_separator.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/metadata_separator.tsx rename to example_plugins/plugins/ui_detail/src/metadata_separator.tsx diff --git a/example_plugins/plugins/docs_detail/src/metadata_tag_list.tsx b/example_plugins/plugins/ui_detail/src/metadata_tag_list.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/metadata_tag_list.tsx rename to example_plugins/plugins/ui_detail/src/metadata_tag_list.tsx diff --git a/example_plugins/plugins/docs_detail/src/metadata_value.tsx b/example_plugins/plugins/ui_detail/src/metadata_value.tsx similarity index 100% rename from example_plugins/plugins/docs_detail/src/metadata_value.tsx rename to example_plugins/plugins/ui_detail/src/metadata_value.tsx diff --git a/example_plugins/plugins/docs_detail/tsconfig.json b/example_plugins/plugins/ui_detail/tsconfig.json similarity index 100% rename from example_plugins/plugins/docs_detail/tsconfig.json rename to example_plugins/plugins/ui_detail/tsconfig.json diff --git a/example_plugins/plugins/docs_form/.gitignore b/example_plugins/plugins/ui_form/.gitignore similarity index 100% rename from example_plugins/plugins/docs_form/.gitignore rename to example_plugins/plugins/ui_form/.gitignore diff --git a/example_plugins/plugins/docs_form/gauntlet.toml b/example_plugins/plugins/ui_form/gauntlet.toml similarity index 100% rename from example_plugins/plugins/docs_form/gauntlet.toml rename to example_plugins/plugins/ui_form/gauntlet.toml diff --git a/example_plugins/plugins/docs_form/package.json b/example_plugins/plugins/ui_form/package.json similarity index 100% rename from example_plugins/plugins/docs_form/package.json rename to example_plugins/plugins/ui_form/package.json diff --git a/example_plugins/plugins/docs_form/src/checkbox.tsx b/example_plugins/plugins/ui_form/src/checkbox.tsx similarity index 100% rename from example_plugins/plugins/docs_form/src/checkbox.tsx rename to example_plugins/plugins/ui_form/src/checkbox.tsx diff --git a/example_plugins/plugins/docs_form/src/date-picker.tsx b/example_plugins/plugins/ui_form/src/date-picker.tsx similarity index 100% rename from example_plugins/plugins/docs_form/src/date-picker.tsx rename to example_plugins/plugins/ui_form/src/date-picker.tsx diff --git a/example_plugins/plugins/docs_form/src/main.tsx b/example_plugins/plugins/ui_form/src/main.tsx similarity index 100% rename from example_plugins/plugins/docs_form/src/main.tsx rename to example_plugins/plugins/ui_form/src/main.tsx diff --git a/example_plugins/plugins/docs_form/src/password-field.tsx b/example_plugins/plugins/ui_form/src/password-field.tsx similarity index 100% rename from example_plugins/plugins/docs_form/src/password-field.tsx rename to example_plugins/plugins/ui_form/src/password-field.tsx diff --git a/example_plugins/plugins/docs_form/src/select.tsx b/example_plugins/plugins/ui_form/src/select.tsx similarity index 100% rename from example_plugins/plugins/docs_form/src/select.tsx rename to example_plugins/plugins/ui_form/src/select.tsx diff --git a/example_plugins/plugins/docs_form/src/separator.tsx b/example_plugins/plugins/ui_form/src/separator.tsx similarity index 100% rename from example_plugins/plugins/docs_form/src/separator.tsx rename to example_plugins/plugins/ui_form/src/separator.tsx diff --git a/example_plugins/plugins/docs_form/src/text-field.tsx b/example_plugins/plugins/ui_form/src/text-field.tsx similarity index 100% rename from example_plugins/plugins/docs_form/src/text-field.tsx rename to example_plugins/plugins/ui_form/src/text-field.tsx diff --git a/example_plugins/plugins/docs_form/tsconfig.json b/example_plugins/plugins/ui_form/tsconfig.json similarity index 100% rename from example_plugins/plugins/docs_form/tsconfig.json rename to example_plugins/plugins/ui_form/tsconfig.json diff --git a/example_plugins/plugins/docs_grid/.gitignore b/example_plugins/plugins/ui_grid/.gitignore similarity index 100% rename from example_plugins/plugins/docs_grid/.gitignore rename to example_plugins/plugins/ui_grid/.gitignore diff --git a/example_plugins/plugins/docs_grid/gauntlet.toml b/example_plugins/plugins/ui_grid/gauntlet.toml similarity index 100% rename from example_plugins/plugins/docs_grid/gauntlet.toml rename to example_plugins/plugins/ui_grid/gauntlet.toml diff --git a/example_plugins/plugins/docs_grid/package.json b/example_plugins/plugins/ui_grid/package.json similarity index 100% rename from example_plugins/plugins/docs_grid/package.json rename to example_plugins/plugins/ui_grid/package.json diff --git a/example_plugins/plugins/docs_grid/src/content_code_block.tsx b/example_plugins/plugins/ui_grid/src/content_code_block.tsx similarity index 100% rename from example_plugins/plugins/docs_grid/src/content_code_block.tsx rename to example_plugins/plugins/ui_grid/src/content_code_block.tsx diff --git a/example_plugins/plugins/docs_grid/src/content_headers.tsx b/example_plugins/plugins/ui_grid/src/content_headers.tsx similarity index 100% rename from example_plugins/plugins/docs_grid/src/content_headers.tsx rename to example_plugins/plugins/ui_grid/src/content_headers.tsx diff --git a/example_plugins/plugins/docs_grid/src/content_image.tsx b/example_plugins/plugins/ui_grid/src/content_image.tsx similarity index 100% rename from example_plugins/plugins/docs_grid/src/content_image.tsx rename to example_plugins/plugins/ui_grid/src/content_image.tsx diff --git a/example_plugins/plugins/docs_grid/src/content_paragraph.tsx b/example_plugins/plugins/ui_grid/src/content_paragraph.tsx similarity index 100% rename from example_plugins/plugins/docs_grid/src/content_paragraph.tsx rename to example_plugins/plugins/ui_grid/src/content_paragraph.tsx diff --git a/example_plugins/plugins/docs_grid/src/empty_view.tsx b/example_plugins/plugins/ui_grid/src/empty_view.tsx similarity index 100% rename from example_plugins/plugins/docs_grid/src/empty_view.tsx rename to example_plugins/plugins/ui_grid/src/empty_view.tsx diff --git a/example_plugins/plugins/docs_grid/src/main.tsx b/example_plugins/plugins/ui_grid/src/main.tsx similarity index 100% rename from example_plugins/plugins/docs_grid/src/main.tsx rename to example_plugins/plugins/ui_grid/src/main.tsx diff --git a/example_plugins/plugins/docs_grid/src/search_bar.tsx b/example_plugins/plugins/ui_grid/src/search_bar.tsx similarity index 100% rename from example_plugins/plugins/docs_grid/src/search_bar.tsx rename to example_plugins/plugins/ui_grid/src/search_bar.tsx diff --git a/example_plugins/plugins/docs_grid/src/section.tsx b/example_plugins/plugins/ui_grid/src/section.tsx similarity index 100% rename from example_plugins/plugins/docs_grid/src/section.tsx rename to example_plugins/plugins/ui_grid/src/section.tsx diff --git a/example_plugins/plugins/docs_grid/tsconfig.json b/example_plugins/plugins/ui_grid/tsconfig.json similarity index 100% rename from example_plugins/plugins/docs_grid/tsconfig.json rename to example_plugins/plugins/ui_grid/tsconfig.json diff --git a/example_plugins/plugins/docs_inline_separators/.gitignore b/example_plugins/plugins/ui_inline_separators/.gitignore similarity index 100% rename from example_plugins/plugins/docs_inline_separators/.gitignore rename to example_plugins/plugins/ui_inline_separators/.gitignore diff --git a/example_plugins/plugins/docs_inline_separators/gauntlet.toml b/example_plugins/plugins/ui_inline_separators/gauntlet.toml similarity index 100% rename from example_plugins/plugins/docs_inline_separators/gauntlet.toml rename to example_plugins/plugins/ui_inline_separators/gauntlet.toml diff --git a/example_plugins/plugins/docs_inline_separators/package.json b/example_plugins/plugins/ui_inline_separators/package.json similarity index 100% rename from example_plugins/plugins/docs_inline_separators/package.json rename to example_plugins/plugins/ui_inline_separators/package.json diff --git a/example_plugins/plugins/docs_inline_separators/src/main.tsx b/example_plugins/plugins/ui_inline_separators/src/main.tsx similarity index 100% rename from example_plugins/plugins/docs_inline_separators/src/main.tsx rename to example_plugins/plugins/ui_inline_separators/src/main.tsx diff --git a/example_plugins/plugins/docs_inline_separators/tsconfig.json b/example_plugins/plugins/ui_inline_separators/tsconfig.json similarity index 100% rename from example_plugins/plugins/docs_inline_separators/tsconfig.json rename to example_plugins/plugins/ui_inline_separators/tsconfig.json diff --git a/example_plugins/plugins/docs_inline_three_sections/.gitignore b/example_plugins/plugins/ui_inline_three_sections/.gitignore similarity index 100% rename from example_plugins/plugins/docs_inline_three_sections/.gitignore rename to example_plugins/plugins/ui_inline_three_sections/.gitignore diff --git a/example_plugins/plugins/docs_inline_three_sections/gauntlet.toml b/example_plugins/plugins/ui_inline_three_sections/gauntlet.toml similarity index 100% rename from example_plugins/plugins/docs_inline_three_sections/gauntlet.toml rename to example_plugins/plugins/ui_inline_three_sections/gauntlet.toml diff --git a/example_plugins/plugins/docs_inline_three_sections/package.json b/example_plugins/plugins/ui_inline_three_sections/package.json similarity index 100% rename from example_plugins/plugins/docs_inline_three_sections/package.json rename to example_plugins/plugins/ui_inline_three_sections/package.json diff --git a/example_plugins/plugins/docs_inline_three_sections/src/main.tsx b/example_plugins/plugins/ui_inline_three_sections/src/main.tsx similarity index 100% rename from example_plugins/plugins/docs_inline_three_sections/src/main.tsx rename to example_plugins/plugins/ui_inline_three_sections/src/main.tsx diff --git a/example_plugins/plugins/docs_inline_three_sections/tsconfig.json b/example_plugins/plugins/ui_inline_three_sections/tsconfig.json similarity index 100% rename from example_plugins/plugins/docs_inline_three_sections/tsconfig.json rename to example_plugins/plugins/ui_inline_three_sections/tsconfig.json diff --git a/example_plugins/plugins/docs_inline_two_sections/.gitignore b/example_plugins/plugins/ui_inline_two_sections/.gitignore similarity index 100% rename from example_plugins/plugins/docs_inline_two_sections/.gitignore rename to example_plugins/plugins/ui_inline_two_sections/.gitignore diff --git a/example_plugins/plugins/docs_inline_two_sections/gauntlet.toml b/example_plugins/plugins/ui_inline_two_sections/gauntlet.toml similarity index 100% rename from example_plugins/plugins/docs_inline_two_sections/gauntlet.toml rename to example_plugins/plugins/ui_inline_two_sections/gauntlet.toml diff --git a/example_plugins/plugins/docs_inline_two_sections/package.json b/example_plugins/plugins/ui_inline_two_sections/package.json similarity index 100% rename from example_plugins/plugins/docs_inline_two_sections/package.json rename to example_plugins/plugins/ui_inline_two_sections/package.json diff --git a/example_plugins/plugins/docs_inline_two_sections/src/main.tsx b/example_plugins/plugins/ui_inline_two_sections/src/main.tsx similarity index 100% rename from example_plugins/plugins/docs_inline_two_sections/src/main.tsx rename to example_plugins/plugins/ui_inline_two_sections/src/main.tsx diff --git a/example_plugins/plugins/docs_inline_two_sections/tsconfig.json b/example_plugins/plugins/ui_inline_two_sections/tsconfig.json similarity index 100% rename from example_plugins/plugins/docs_inline_two_sections/tsconfig.json rename to example_plugins/plugins/ui_inline_two_sections/tsconfig.json diff --git a/example_plugins/plugins/docs_list/.gitignore b/example_plugins/plugins/ui_list/.gitignore similarity index 100% rename from example_plugins/plugins/docs_list/.gitignore rename to example_plugins/plugins/ui_list/.gitignore diff --git a/example_plugins/plugins/docs_list/gauntlet.toml b/example_plugins/plugins/ui_list/gauntlet.toml similarity index 100% rename from example_plugins/plugins/docs_list/gauntlet.toml rename to example_plugins/plugins/ui_list/gauntlet.toml diff --git a/example_plugins/plugins/docs_list/package.json b/example_plugins/plugins/ui_list/package.json similarity index 100% rename from example_plugins/plugins/docs_list/package.json rename to example_plugins/plugins/ui_list/package.json diff --git a/example_plugins/plugins/docs_list/src/detail.tsx b/example_plugins/plugins/ui_list/src/detail.tsx similarity index 100% rename from example_plugins/plugins/docs_list/src/detail.tsx rename to example_plugins/plugins/ui_list/src/detail.tsx diff --git a/example_plugins/plugins/docs_list/src/empty_view.tsx b/example_plugins/plugins/ui_list/src/empty_view.tsx similarity index 100% rename from example_plugins/plugins/docs_list/src/empty_view.tsx rename to example_plugins/plugins/ui_list/src/empty_view.tsx diff --git a/example_plugins/plugins/docs_list/src/main.tsx b/example_plugins/plugins/ui_list/src/main.tsx similarity index 100% rename from example_plugins/plugins/docs_list/src/main.tsx rename to example_plugins/plugins/ui_list/src/main.tsx diff --git a/example_plugins/plugins/docs_list/src/search_bar.tsx b/example_plugins/plugins/ui_list/src/search_bar.tsx similarity index 100% rename from example_plugins/plugins/docs_list/src/search_bar.tsx rename to example_plugins/plugins/ui_list/src/search_bar.tsx diff --git a/example_plugins/plugins/docs_list/src/section.tsx b/example_plugins/plugins/ui_list/src/section.tsx similarity index 100% rename from example_plugins/plugins/docs_list/src/section.tsx rename to example_plugins/plugins/ui_list/src/section.tsx diff --git a/example_plugins/plugins/docs_list/tsconfig.json b/example_plugins/plugins/ui_list/tsconfig.json similarity index 100% rename from example_plugins/plugins/docs_list/tsconfig.json rename to example_plugins/plugins/ui_list/tsconfig.json diff --git a/example_plugins/scenarios/docs_detail/content-code-block/default.json b/example_plugins/scenarios/ui_detail/content-code-block/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/content-code-block/default.json rename to example_plugins/scenarios/ui_detail/content-code-block/default.json diff --git a/example_plugins/scenarios/docs_detail/content-header/default.json b/example_plugins/scenarios/ui_detail/content-header/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/content-header/default.json rename to example_plugins/scenarios/ui_detail/content-header/default.json diff --git a/example_plugins/scenarios/docs_detail/content-horizontal-break/default.json b/example_plugins/scenarios/ui_detail/content-horizontal-break/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/content-horizontal-break/default.json rename to example_plugins/scenarios/ui_detail/content-horizontal-break/default.json diff --git a/example_plugins/scenarios/docs_detail/content-image/default.json b/example_plugins/scenarios/ui_detail/content-image/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/content-image/default.json rename to example_plugins/scenarios/ui_detail/content-image/default.json diff --git a/example_plugins/scenarios/docs_detail/content-paragraph/default.json b/example_plugins/scenarios/ui_detail/content-paragraph/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/content-paragraph/default.json rename to example_plugins/scenarios/ui_detail/content-paragraph/default.json diff --git a/example_plugins/scenarios/docs_detail/content/default.json b/example_plugins/scenarios/ui_detail/content/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/content/default.json rename to example_plugins/scenarios/ui_detail/content/default.json diff --git a/example_plugins/scenarios/docs_detail/main/default.json b/example_plugins/scenarios/ui_detail/main/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/main/default.json rename to example_plugins/scenarios/ui_detail/main/default.json diff --git a/example_plugins/scenarios/docs_detail/metadata-icon/default.json b/example_plugins/scenarios/ui_detail/metadata-icon/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/metadata-icon/default.json rename to example_plugins/scenarios/ui_detail/metadata-icon/default.json diff --git a/example_plugins/scenarios/docs_detail/metadata-link/default.json b/example_plugins/scenarios/ui_detail/metadata-link/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/metadata-link/default.json rename to example_plugins/scenarios/ui_detail/metadata-link/default.json diff --git a/example_plugins/scenarios/docs_detail/metadata-separator/default.json b/example_plugins/scenarios/ui_detail/metadata-separator/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/metadata-separator/default.json rename to example_plugins/scenarios/ui_detail/metadata-separator/default.json diff --git a/example_plugins/scenarios/docs_detail/metadata-tag-list/default.json b/example_plugins/scenarios/ui_detail/metadata-tag-list/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/metadata-tag-list/default.json rename to example_plugins/scenarios/ui_detail/metadata-tag-list/default.json diff --git a/example_plugins/scenarios/docs_detail/metadata-value/default.json b/example_plugins/scenarios/ui_detail/metadata-value/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/metadata-value/default.json rename to example_plugins/scenarios/ui_detail/metadata-value/default.json diff --git a/example_plugins/scenarios/docs_detail/metadata/default.json b/example_plugins/scenarios/ui_detail/metadata/default.json similarity index 100% rename from example_plugins/scenarios/docs_detail/metadata/default.json rename to example_plugins/scenarios/ui_detail/metadata/default.json diff --git a/example_plugins/scenarios/docs_form/checkbox/default.json b/example_plugins/scenarios/ui_form/checkbox/default.json similarity index 100% rename from example_plugins/scenarios/docs_form/checkbox/default.json rename to example_plugins/scenarios/ui_form/checkbox/default.json diff --git a/example_plugins/scenarios/docs_form/date-picker/default.json b/example_plugins/scenarios/ui_form/date-picker/default.json similarity index 100% rename from example_plugins/scenarios/docs_form/date-picker/default.json rename to example_plugins/scenarios/ui_form/date-picker/default.json diff --git a/example_plugins/scenarios/docs_form/main/default.json b/example_plugins/scenarios/ui_form/main/default.json similarity index 100% rename from example_plugins/scenarios/docs_form/main/default.json rename to example_plugins/scenarios/ui_form/main/default.json diff --git a/example_plugins/scenarios/docs_form/password-field/default.json b/example_plugins/scenarios/ui_form/password-field/default.json similarity index 100% rename from example_plugins/scenarios/docs_form/password-field/default.json rename to example_plugins/scenarios/ui_form/password-field/default.json diff --git a/example_plugins/scenarios/docs_form/select/default.json b/example_plugins/scenarios/ui_form/select/default.json similarity index 100% rename from example_plugins/scenarios/docs_form/select/default.json rename to example_plugins/scenarios/ui_form/select/default.json diff --git a/example_plugins/scenarios/docs_form/separator/default.json b/example_plugins/scenarios/ui_form/separator/default.json similarity index 100% rename from example_plugins/scenarios/docs_form/separator/default.json rename to example_plugins/scenarios/ui_form/separator/default.json diff --git a/example_plugins/scenarios/docs_form/text-field/default.json b/example_plugins/scenarios/ui_form/text-field/default.json similarity index 100% rename from example_plugins/scenarios/docs_form/text-field/default.json rename to example_plugins/scenarios/ui_form/text-field/default.json diff --git a/example_plugins/scenarios/docs_grid/content-code-block/default.json b/example_plugins/scenarios/ui_grid/content-code-block/default.json similarity index 100% rename from example_plugins/scenarios/docs_grid/content-code-block/default.json rename to example_plugins/scenarios/ui_grid/content-code-block/default.json diff --git a/example_plugins/scenarios/docs_grid/content-headers/default.json b/example_plugins/scenarios/ui_grid/content-headers/default.json similarity index 100% rename from example_plugins/scenarios/docs_grid/content-headers/default.json rename to example_plugins/scenarios/ui_grid/content-headers/default.json diff --git a/example_plugins/scenarios/docs_grid/content-image/default.json b/example_plugins/scenarios/ui_grid/content-image/default.json similarity index 100% rename from example_plugins/scenarios/docs_grid/content-image/default.json rename to example_plugins/scenarios/ui_grid/content-image/default.json diff --git a/example_plugins/scenarios/docs_grid/content-paragraph/default.json b/example_plugins/scenarios/ui_grid/content-paragraph/default.json similarity index 100% rename from example_plugins/scenarios/docs_grid/content-paragraph/default.json rename to example_plugins/scenarios/ui_grid/content-paragraph/default.json diff --git a/example_plugins/scenarios/docs_grid/empty-view/default.json b/example_plugins/scenarios/ui_grid/empty-view/default.json similarity index 100% rename from example_plugins/scenarios/docs_grid/empty-view/default.json rename to example_plugins/scenarios/ui_grid/empty-view/default.json diff --git a/example_plugins/scenarios/docs_grid/main/default.json b/example_plugins/scenarios/ui_grid/main/default.json similarity index 100% rename from example_plugins/scenarios/docs_grid/main/default.json rename to example_plugins/scenarios/ui_grid/main/default.json diff --git a/example_plugins/scenarios/docs_grid/search-bar/default.json b/example_plugins/scenarios/ui_grid/search-bar/default.json similarity index 100% rename from example_plugins/scenarios/docs_grid/search-bar/default.json rename to example_plugins/scenarios/ui_grid/search-bar/default.json diff --git a/example_plugins/scenarios/docs_grid/section/default.json b/example_plugins/scenarios/ui_grid/section/default.json similarity index 100% rename from example_plugins/scenarios/docs_grid/section/default.json rename to example_plugins/scenarios/ui_grid/section/default.json diff --git a/example_plugins/scenarios/docs_inline_separators/main/separator.json b/example_plugins/scenarios/ui_inline_separators/main/separator.json similarity index 100% rename from example_plugins/scenarios/docs_inline_separators/main/separator.json rename to example_plugins/scenarios/ui_inline_separators/main/separator.json diff --git a/example_plugins/scenarios/docs_inline_three_sections/main/three-sections.json b/example_plugins/scenarios/ui_inline_three_sections/main/three-sections.json similarity index 100% rename from example_plugins/scenarios/docs_inline_three_sections/main/three-sections.json rename to example_plugins/scenarios/ui_inline_three_sections/main/three-sections.json diff --git a/example_plugins/scenarios/docs_inline_two_sections/main/two-sections.json b/example_plugins/scenarios/ui_inline_two_sections/main/two-sections.json similarity index 100% rename from example_plugins/scenarios/docs_inline_two_sections/main/two-sections.json rename to example_plugins/scenarios/ui_inline_two_sections/main/two-sections.json diff --git a/example_plugins/scenarios/docs_list/detail/default.json b/example_plugins/scenarios/ui_list/detail/default.json similarity index 100% rename from example_plugins/scenarios/docs_list/detail/default.json rename to example_plugins/scenarios/ui_list/detail/default.json diff --git a/example_plugins/scenarios/docs_list/empty-view/default.json b/example_plugins/scenarios/ui_list/empty-view/default.json similarity index 100% rename from example_plugins/scenarios/docs_list/empty-view/default.json rename to example_plugins/scenarios/ui_list/empty-view/default.json diff --git a/example_plugins/scenarios/docs_list/main/default.json b/example_plugins/scenarios/ui_list/main/default.json similarity index 100% rename from example_plugins/scenarios/docs_list/main/default.json rename to example_plugins/scenarios/ui_list/main/default.json diff --git a/example_plugins/scenarios/docs_list/search-bar/default.json b/example_plugins/scenarios/ui_list/search-bar/default.json similarity index 100% rename from example_plugins/scenarios/docs_list/search-bar/default.json rename to example_plugins/scenarios/ui_list/search-bar/default.json diff --git a/example_plugins/scenarios/docs_list/section/default.json b/example_plugins/scenarios/ui_list/section/default.json similarity index 100% rename from example_plugins/scenarios/docs_list/section/default.json rename to example_plugins/scenarios/ui_list/section/default.json From fc4df0efa14651a5444e507dc900ea2d7c97e687 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:41:59 +0100 Subject: [PATCH 393/540] Rename out-screenshot to out_screenshot --- example_plugins/.gitignore | 2 +- js/scenario_runner_cli/src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example_plugins/.gitignore b/example_plugins/.gitignore index 6990142..5ba1e87 100644 --- a/example_plugins/.gitignore +++ b/example_plugins/.gitignore @@ -1,3 +1,3 @@ run -out-screenshot +out_screenshot out \ No newline at end of file diff --git a/js/scenario_runner_cli/src/main.ts b/js/scenario_runner_cli/src/main.ts index abc193e..4d72330 100644 --- a/js/scenario_runner_cli/src/main.ts +++ b/js/scenario_runner_cli/src/main.ts @@ -121,7 +121,7 @@ async function runScreenshotGen(expectedPlugin: string | undefined, expectedEntr RUST_LOG: "gauntlet-client=INFO", GAUNTLET_SCENARIO_RUNNER_TYPE: "screenshot_gen", GAUNTLET_SCREENSHOT_GEN_IN: scenarioFile, - GAUNTLET_SCREENSHOT_GEN_OUT: path.join(scenarios, "out-screenshot", plugin, entrypoint, scenarioName + ".png"), + GAUNTLET_SCREENSHOT_GEN_OUT: path.join(scenarios, "out_screenshot", plugin, entrypoint, scenarioName + ".png"), GAUNTLET_SCREENSHOT_GEN_NAME: scenarioNameTitle, }) }); From 77d20bcd6c653cd8124831c0f4765a14d4ec277a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:46:25 +0100 Subject: [PATCH 394/540] More ui_list plugin examples --- example_plugins/plugins/ui_list/gauntlet.toml | 126 ++++++++++++++++++ .../plugins/ui_list/src/content.tsx | 47 +++++++ .../ui_list/src/content_code_block.tsx | 23 ++++ .../plugins/ui_list/src/content_headers.tsx | 29 ++++ .../ui_list/src/content_horizontal_break.tsx | 40 ++++++ .../plugins/ui_list/src/content_image.tsx | 17 +++ .../plugins/ui_list/src/content_paragraph.tsx | 32 +++++ .../plugins/ui_list/src/detail.tsx | 1 + example_plugins/plugins/ui_list/src/item.tsx | 13 ++ example_plugins/plugins/ui_list/src/main.tsx | 97 ++++++++++++-- .../plugins/ui_list/src/metadata.tsx | 41 ++++++ .../plugins/ui_list/src/metadata_icon.tsx | 16 +++ .../plugins/ui_list/src/metadata_link.tsx | 15 +++ .../ui_list/src/metadata_separator.tsx | 19 +++ .../plugins/ui_list/src/metadata_tag_list.tsx | 23 ++++ .../plugins/ui_list/src/metadata_value.tsx | 15 +++ .../ui_list/content-code-block/default.json | 3 + .../ui_list/content-headers/default.json | 3 + .../content-horizontal-break/default.json | 3 + .../ui_list/content-image/default.json | 3 + .../ui_list/content-paragraph/default.json | 3 + .../scenarios/ui_list/content/default.json | 3 + .../scenarios/ui_list/item/default.json | 3 + .../ui_list/metadata-icon/default.json | 3 + .../ui_list/metadata-link/default.json | 3 + .../ui_list/metadata-separator/default.json | 3 + .../ui_list/metadata-tag-list/default.json | 3 + .../ui_list/metadata-value/default.json | 3 + .../scenarios/ui_list/metadata/default.json | 3 + 29 files changed, 580 insertions(+), 13 deletions(-) create mode 100644 example_plugins/plugins/ui_list/src/content.tsx create mode 100644 example_plugins/plugins/ui_list/src/content_code_block.tsx create mode 100644 example_plugins/plugins/ui_list/src/content_headers.tsx create mode 100644 example_plugins/plugins/ui_list/src/content_horizontal_break.tsx create mode 100644 example_plugins/plugins/ui_list/src/content_image.tsx create mode 100644 example_plugins/plugins/ui_list/src/content_paragraph.tsx create mode 100644 example_plugins/plugins/ui_list/src/item.tsx create mode 100644 example_plugins/plugins/ui_list/src/metadata.tsx create mode 100644 example_plugins/plugins/ui_list/src/metadata_icon.tsx create mode 100644 example_plugins/plugins/ui_list/src/metadata_link.tsx create mode 100644 example_plugins/plugins/ui_list/src/metadata_separator.tsx create mode 100644 example_plugins/plugins/ui_list/src/metadata_tag_list.tsx create mode 100644 example_plugins/plugins/ui_list/src/metadata_value.tsx create mode 100644 example_plugins/scenarios/ui_list/content-code-block/default.json create mode 100644 example_plugins/scenarios/ui_list/content-headers/default.json create mode 100644 example_plugins/scenarios/ui_list/content-horizontal-break/default.json create mode 100644 example_plugins/scenarios/ui_list/content-image/default.json create mode 100644 example_plugins/scenarios/ui_list/content-paragraph/default.json create mode 100644 example_plugins/scenarios/ui_list/content/default.json create mode 100644 example_plugins/scenarios/ui_list/item/default.json create mode 100644 example_plugins/scenarios/ui_list/metadata-icon/default.json create mode 100644 example_plugins/scenarios/ui_list/metadata-link/default.json create mode 100644 example_plugins/scenarios/ui_list/metadata-separator/default.json create mode 100644 example_plugins/scenarios/ui_list/metadata-tag-list/default.json create mode 100644 example_plugins/scenarios/ui_list/metadata-value/default.json create mode 100644 example_plugins/scenarios/ui_list/metadata/default.json diff --git a/example_plugins/plugins/ui_list/gauntlet.toml b/example_plugins/plugins/ui_list/gauntlet.toml index 45fe43f..c0e7845 100644 --- a/example_plugins/plugins/ui_list/gauntlet.toml +++ b/example_plugins/plugins/ui_list/gauntlet.toml @@ -48,6 +48,132 @@ type = 'view' description = '' # docs-code-segment:end +# docs-code-segment:start item +[[entrypoint]] +id = 'item' +name = 'List Item' +path = 'src/item.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start content +[[entrypoint]] +id = 'content' +name = 'List Content' +path = 'src/content.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start content-code-block +[[entrypoint]] +id = 'content-code-block' +name = 'List Content Code Block' +path = 'src/content_code_block.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start content-headers +[[entrypoint]] +id = 'content-headers' +name = 'List Content Headers' +path = 'src/content_headers.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start content-horizontal-break +[[entrypoint]] +id = 'content-horizontal-break' +name = 'List Content Horizontal Break' +path = 'src/content_horizontal_break.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start content-image +[[entrypoint]] +id = 'content-image' +name = 'List Content Image' +path = 'src/content_image.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start content-paragraph +[[entrypoint]] +id = 'content-paragraph' +name = 'List Content Paragraph' +path = 'src/content_paragraph.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start metadata +[[entrypoint]] +id = 'metadata' +name = 'List Metadata' +path = 'src/metadata.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start metadata-icon +[[entrypoint]] +id = 'metadata-icon' +name = 'List Metadata Icon' +path = 'src/metadata_icon.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start metadata-icon +[[entrypoint]] +id = 'metadata-icon' +name = 'List Metadata Icon' +path = 'src/metadata_icon.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start metadata-link +[[entrypoint]] +id = 'metadata-link' +name = 'List Metadata Link' +path = 'src/metadata_link.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start metadata-separator +[[entrypoint]] +id = 'metadata-separator' +name = 'List Metadata Separator' +path = 'src/metadata_separator.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start metadata-tag-list +[[entrypoint]] +id = 'metadata-tag-list' +name = 'List Metadata Tag List' +path = 'src/metadata_tag_list.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start metadata-value +[[entrypoint]] +id = 'metadata-value' +name = 'List Metadata Value' +path = 'src/metadata_value.tsx' +type = 'view' +description = '' +# docs-code-segment:end + [permissions] network = ["static.wikia.nocookie.net"] diff --git a/example_plugins/plugins/ui_list/src/content.tsx b/example_plugins/plugins/ui_list/src/content.tsx new file mode 100644 index 0000000..72c7ecd --- /dev/null +++ b/example_plugins/plugins/ui_list/src/content.tsx @@ -0,0 +1,47 @@ +import React, { ReactElement } from "react"; +import { Action, ActionPanel, List } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + {}}/> + + } + > + + + + + + + + + + + + + + The Ezaraa were a species of warmongering carnivorous sentients that were native to the the planet + Ezaraa. + They intended to overthrow the Galactic Empire, only to replace it with their own dominion and feed + on the other species, which they deemed as lesser to them. + To arm their revolution, the dominion sent Ezaraa to take advantage of opportunities such as the + Auction of Rur. + + + Society and culture + + + "Bring the Dominion of the Ezaraa across the stars! And consume the flesh of all the lesser + species!" + + + ―An Ezaraa, to Luke Skywalker + + + + + ) +} diff --git a/example_plugins/plugins/ui_list/src/content_code_block.tsx b/example_plugins/plugins/ui_list/src/content_code_block.tsx new file mode 100644 index 0000000..11a9a02 --- /dev/null +++ b/example_plugins/plugins/ui_list/src/content_code_block.tsx @@ -0,0 +1,23 @@ +import { ReactElement } from "react"; +import { List } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + + + + + Hah! Why pay when one can slay? Then retrieve the bauble from its smoking chassis! It is the Ezaraa way!" + "For the glory of the Ezaraa Dominion!" + [The Ezaraas take a single causality] + "Retreat!" + + + ―Ezaraa warriors during the Rur Crystal incident + + + + + ) +} diff --git a/example_plugins/plugins/ui_list/src/content_headers.tsx b/example_plugins/plugins/ui_list/src/content_headers.tsx new file mode 100644 index 0000000..a811601 --- /dev/null +++ b/example_plugins/plugins/ui_list/src/content_headers.tsx @@ -0,0 +1,29 @@ +import { ReactElement } from "react"; +import { List } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + + + + + + + + + + + + + + Behind the scenes + + + The Ezaraa species first appeared in the canon crossover comic The Screaming Citadel 1, written by Kieron Gillen, illustrated by Marco Checchetto and released on May 10, 2017. + + + + + ) +} diff --git a/example_plugins/plugins/ui_list/src/content_horizontal_break.tsx b/example_plugins/plugins/ui_list/src/content_horizontal_break.tsx new file mode 100644 index 0000000..7690bf2 --- /dev/null +++ b/example_plugins/plugins/ui_list/src/content_horizontal_break.tsx @@ -0,0 +1,40 @@ +import { ReactElement } from "react"; +import { List } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + + + + + + + + + + + + + + Quotes + + + "Hah! Why pay when one can slay? Then retrieve the bauble from its smoking chassis! It is the Ezaraa way!" + "For the glory of the Ezaraa Dominion!" + [The Ezaraas take a single causality] + "Retreat!" + + + + "The Ezaraa will rule the universe. It is our destiny. For delivering the Rur sentience to us, you will receive 0.00001% of our Imperial revenue, for you and your next ten descendants." + + + + The Ezaraa species first appeared in the canon crossover comic The Screaming Citadel 1, written by Kieron Gillen, illustrated by Marco Checchetto and released on May 10, 2017. + + + + + ) +} diff --git a/example_plugins/plugins/ui_list/src/content_image.tsx b/example_plugins/plugins/ui_list/src/content_image.tsx new file mode 100644 index 0000000..ed3ca2f --- /dev/null +++ b/example_plugins/plugins/ui_list/src/content_image.tsx @@ -0,0 +1,17 @@ +import { ReactElement } from "react"; +import { List } from "@project-gauntlet/api/components"; + +const imgUrl = "https://static.wikia.nocookie.net/starwars/images/e/e4/Ezaraa.png/revision/latest/scale-to-width-down/200?cb=20170511082800" + +export default function Main(): ReactElement { + return ( + + + + + + + + + ) +} diff --git a/example_plugins/plugins/ui_list/src/content_paragraph.tsx b/example_plugins/plugins/ui_list/src/content_paragraph.tsx new file mode 100644 index 0000000..29b1f8d --- /dev/null +++ b/example_plugins/plugins/ui_list/src/content_paragraph.tsx @@ -0,0 +1,32 @@ +import { ReactElement } from "react"; +import { List } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + + + + + + + + + + + + + + Hah! Why pay when one can slay? Then retrieve the bauble from its smoking chassis! It is the Ezaraa way!" + "For the glory of the Ezaraa Dominion!" + [The Ezaraas take a single causality] + "Retreat!" + + + ―Ezaraa warriors during the Rur Crystal incident + + + + + ) +} diff --git a/example_plugins/plugins/ui_list/src/detail.tsx b/example_plugins/plugins/ui_list/src/detail.tsx index 72d6a6b..c5c3685 100644 --- a/example_plugins/plugins/ui_list/src/detail.tsx +++ b/example_plugins/plugins/ui_list/src/detail.tsx @@ -16,6 +16,7 @@ export default function Main(): ReactElement { + Ezaraa Sentient Humanoid Ezaraa diff --git a/example_plugins/plugins/ui_list/src/item.tsx b/example_plugins/plugins/ui_list/src/item.tsx new file mode 100644 index 0000000..9b94484 --- /dev/null +++ b/example_plugins/plugins/ui_list/src/item.tsx @@ -0,0 +1,13 @@ +import { ReactElement } from "react"; +import { IconAccessory, Icons, List, TextAccessory } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + ]}/> + ]}/> + + + + ) +} diff --git a/example_plugins/plugins/ui_list/src/main.tsx b/example_plugins/plugins/ui_list/src/main.tsx index 6458e49..4fc65b8 100644 --- a/example_plugins/plugins/ui_list/src/main.tsx +++ b/example_plugins/plugins/ui_list/src/main.tsx @@ -1,19 +1,90 @@ -import { ReactElement } from "react"; -import { List } from "@project-gauntlet/api/components"; +import React, { ReactElement } from "react"; +import { Action, ActionPanel, IconAccessory, Icons, List, TextAccessory } from "@project-gauntlet/api/components"; export default function Main(): ReactElement { return ( - - - - - - - - - - - + + {}}/> + + } + > + , + , + ]} + /> + , + , + ]} + /> + , + , + ]} + /> + , + , + ]} + /> + , + , + ]} + /> + , + , + , + ]} + /> + , + , + ]} + /> + ) } diff --git a/example_plugins/plugins/ui_list/src/metadata.tsx b/example_plugins/plugins/ui_list/src/metadata.tsx new file mode 100644 index 0000000..7d06e3a --- /dev/null +++ b/example_plugins/plugins/ui_list/src/metadata.tsx @@ -0,0 +1,41 @@ +import React, { ReactElement } from "react"; +import { Action, ActionPanel, List } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + {}}/> + + } + > + + + + + + + + + + + + + Ezaraa + Sentient + Humanoid + Ezaraa + Carnivorous + + The Screaming Citadel 1 + Doctor Aphra (2016) 9 + Doctor Aphra (2016) 10 + Doctor Aphra (2016) 11 + Doctor Aphra (2016) 12 + + + + + ) +} diff --git a/example_plugins/plugins/ui_list/src/metadata_icon.tsx b/example_plugins/plugins/ui_list/src/metadata_icon.tsx new file mode 100644 index 0000000..e559f6f --- /dev/null +++ b/example_plugins/plugins/ui_list/src/metadata_icon.tsx @@ -0,0 +1,16 @@ +import { ReactElement } from "react"; +import { Icons, List } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + + + + + + + + + ) +} diff --git a/example_plugins/plugins/ui_list/src/metadata_link.tsx b/example_plugins/plugins/ui_list/src/metadata_link.tsx new file mode 100644 index 0000000..bc99323 --- /dev/null +++ b/example_plugins/plugins/ui_list/src/metadata_link.tsx @@ -0,0 +1,15 @@ +import { ReactElement } from "react"; +import { Icons, List } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + + + + + + + + ) +} diff --git a/example_plugins/plugins/ui_list/src/metadata_separator.tsx b/example_plugins/plugins/ui_list/src/metadata_separator.tsx new file mode 100644 index 0000000..0cf8e38 --- /dev/null +++ b/example_plugins/plugins/ui_list/src/metadata_separator.tsx @@ -0,0 +1,19 @@ +import { ReactElement } from "react"; +import { Icons, List } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + + + + Sentient + Humanoid + + Ezaraa + Carnivorous + + + + ) +} diff --git a/example_plugins/plugins/ui_list/src/metadata_tag_list.tsx b/example_plugins/plugins/ui_list/src/metadata_tag_list.tsx new file mode 100644 index 0000000..89316aa --- /dev/null +++ b/example_plugins/plugins/ui_list/src/metadata_tag_list.tsx @@ -0,0 +1,23 @@ +import { ReactElement } from "react"; +import { Icons, List } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + + + + + Bounty Hunters 14 + Bounty Hunters 20 + Bounty Hunters 23 + Bounty Hunters 24 + Bounty Hunters 35 + "Tall Tales" — Revelations (2023) 1 + Bounty Hunters 42 + + + + + ) +} diff --git a/example_plugins/plugins/ui_list/src/metadata_value.tsx b/example_plugins/plugins/ui_list/src/metadata_value.tsx new file mode 100644 index 0000000..a922c7a --- /dev/null +++ b/example_plugins/plugins/ui_list/src/metadata_value.tsx @@ -0,0 +1,15 @@ +import { ReactElement } from "react"; +import { Icons, List } from "@project-gauntlet/api/components"; + +export default function Main(): ReactElement { + return ( + + + + + Sentient + + + + ) +} diff --git a/example_plugins/scenarios/ui_list/content-code-block/default.json b/example_plugins/scenarios/ui_list/content-code-block/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/content-code-block/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_list/content-headers/default.json b/example_plugins/scenarios/ui_list/content-headers/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/content-headers/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_list/content-horizontal-break/default.json b/example_plugins/scenarios/ui_list/content-horizontal-break/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/content-horizontal-break/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_list/content-image/default.json b/example_plugins/scenarios/ui_list/content-image/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/content-image/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_list/content-paragraph/default.json b/example_plugins/scenarios/ui_list/content-paragraph/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/content-paragraph/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_list/content/default.json b/example_plugins/scenarios/ui_list/content/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/content/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_list/item/default.json b/example_plugins/scenarios/ui_list/item/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/item/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_list/metadata-icon/default.json b/example_plugins/scenarios/ui_list/metadata-icon/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/metadata-icon/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_list/metadata-link/default.json b/example_plugins/scenarios/ui_list/metadata-link/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/metadata-link/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_list/metadata-separator/default.json b/example_plugins/scenarios/ui_list/metadata-separator/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/metadata-separator/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_list/metadata-tag-list/default.json b/example_plugins/scenarios/ui_list/metadata-tag-list/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/metadata-tag-list/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_list/metadata-value/default.json b/example_plugins/scenarios/ui_list/metadata-value/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/metadata-value/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_list/metadata/default.json b/example_plugins/scenarios/ui_list/metadata/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_list/metadata/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file From c08e33c48c5e6ad2cee3cc821b4dfa3599a877ab Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 22 Jan 2025 20:15:43 +0100 Subject: [PATCH 395/540] Remove links from docs markdown for now --- docs/js/components/action/props/id.md | 2 +- docs/js/components/content/description.md | 2 +- docs/js/components/detail/description.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/js/components/action/props/id.md b/docs/js/components/action/props/id.md index 17dbcd3..8c9761b 100644 --- a/docs/js/components/action/props/id.md +++ b/docs/js/components/action/props/id.md @@ -1 +1 @@ -The identifier of an action. Referenced in [plugin manifest]($LINK$__PLUGIN_MANIFEST__) to assign shortcut to button in UI \ No newline at end of file +The identifier of an action. Referenced in plugin manifest to assign shortcut to button in UI \ No newline at end of file diff --git a/docs/js/components/content/description.md b/docs/js/components/content/description.md index ee6dab1..77668fe 100644 --- a/docs/js/components/content/description.md +++ b/docs/js/components/content/description.md @@ -1,3 +1,3 @@ A container for a set of non-interactable components. -Used in a variety of places like [Detail]($COMPONENT$__DETAIL__), [Inline]($COMPONENT$__INLINE__) and [GridItem]($COMPONENT$__GRID_ITEM__). +Used in a variety of places like , and . By utilizing the power of React the content can also be made dynamic \ No newline at end of file diff --git a/docs/js/components/detail/description.md b/docs/js/components/detail/description.md index 18f2574..2524727 100644 --- a/docs/js/components/detail/description.md +++ b/docs/js/components/detail/description.md @@ -1 +1 @@ -Detail is a root component that contains a possibly dynamic, non-interactable [Content]($COMPONENT$__CONTENT__) and an optional side panel \ No newline at end of file +Detail is a root component that contains a possibly dynamic, non-interactable component and an optional side panel \ No newline at end of file From 345ab751434950ba38eacec19208fe86b3809d1d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 22 Jan 2025 20:16:09 +0100 Subject: [PATCH 396/540] Add script to generate component model from root --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index cf422ab..a9e2bc9 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "build-scenarios": "npm run build --workspace example_plugins --if-present", "build-dev-plugin": "npm run build --workspace dev_plugin", "build": "npm run build --workspace js --workspace bundled_plugins --if-present", + "run-component-model-gen": "npm run generate-json --workspace js/api_build", "run-scenarios": "npm run run-scenarios --workspace js/scenario_runner_cli", "run-screenshot-gen": "npm run run-screenshot-gen --workspace js/scenario_runner_cli" }, From 67c7234b411b1f184092f1341d77e4508cc9ad8f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 22 Jan 2025 20:36:10 +0100 Subject: [PATCH 397/540] Replace _ with - in plugin examples file names --- .../plugins/ui_detail/gauntlet.toml | 20 ++++++------ ..._code_block.tsx => content-code-block.tsx} | 4 +-- ...{content_header.tsx => content-header.tsx} | 4 +-- ...break.tsx => content-horizontal-break.tsx} | 4 +-- .../{content_image.tsx => content-image.tsx} | 6 ++-- ...nt_paragraph.tsx => content-paragraph.tsx} | 4 +-- .../plugins/ui_detail/src/content.tsx | 4 +-- .../plugins/ui_detail/src/main.tsx | 10 ++++-- .../{metadata_icon.tsx => metadata-icon.tsx} | 4 +-- .../{metadata_link.tsx => metadata-link.tsx} | 4 +-- ...a_separator.tsx => metadata-separator.tsx} | 4 +-- .../ui_detail/src/metadata-tag-list.tsx | 31 +++++++++++++++++++ ...{metadata_value.tsx => metadata-value.tsx} | 4 +-- .../plugins/ui_detail/src/metadata.tsx | 4 +-- .../ui_detail/src/metadata_tag_list.tsx | 18 ----------- .../plugins/ui_form/src/checkbox.tsx | 2 +- .../plugins/ui_form/src/date-picker.tsx | 2 +- example_plugins/plugins/ui_form/src/main.tsx | 2 +- .../plugins/ui_form/src/password-field.tsx | 2 +- .../plugins/ui_form/src/select.tsx | 2 +- .../plugins/ui_form/src/separator.tsx | 2 +- .../plugins/ui_form/src/text-field.tsx | 2 +- example_plugins/plugins/ui_grid/gauntlet.toml | 12 +++---- ..._code_block.tsx => content-code-block.tsx} | 2 +- ...ontent_headers.tsx => content-headers.tsx} | 2 +- .../{content_image.tsx => content-image.tsx} | 2 +- ...nt_paragraph.tsx => content-paragraph.tsx} | 2 +- .../src/{empty_view.tsx => empty-view.tsx} | 2 +- example_plugins/plugins/ui_grid/src/main.tsx | 2 +- .../src/{search_bar.tsx => search-bar.tsx} | 2 +- .../plugins/ui_grid/src/section.tsx | 2 +- example_plugins/plugins/ui_list/gauntlet.toml | 26 ++++++++-------- ..._code_block.tsx => content-code-block.tsx} | 2 +- ...ontent_headers.tsx => content-headers.tsx} | 2 +- ...break.tsx => content-horizontal-break.tsx} | 2 +- .../{content_image.tsx => content-image.tsx} | 2 +- ...nt_paragraph.tsx => content-paragraph.tsx} | 2 +- .../plugins/ui_list/src/content.tsx | 4 +-- .../plugins/ui_list/src/detail.tsx | 2 +- .../src/{empty_view.tsx => empty-view.tsx} | 2 +- example_plugins/plugins/ui_list/src/item.tsx | 2 +- example_plugins/plugins/ui_list/src/main.tsx | 4 +-- .../{metadata_icon.tsx => metadata-icon.tsx} | 2 +- .../{metadata_link.tsx => metadata-link.tsx} | 4 +-- ...a_separator.tsx => metadata-separator.tsx} | 4 +-- ...ata_tag_list.tsx => metadata-tag-list.tsx} | 4 +-- ...{metadata_value.tsx => metadata-value.tsx} | 4 +-- .../plugins/ui_list/src/metadata.tsx | 4 +-- .../src/{search_bar.tsx => search-bar.tsx} | 2 +- .../plugins/ui_list/src/section.tsx | 2 +- 50 files changed, 131 insertions(+), 112 deletions(-) rename example_plugins/plugins/ui_detail/src/{content_code_block.tsx => content-code-block.tsx} (88%) rename example_plugins/plugins/ui_detail/src/{content_header.tsx => content-header.tsx} (93%) rename example_plugins/plugins/ui_detail/src/{content_horizontal_break.tsx => content-horizontal-break.tsx} (90%) rename example_plugins/plugins/ui_detail/src/{content_image.tsx => content-image.tsx} (83%) rename example_plugins/plugins/ui_detail/src/{content_paragraph.tsx => content-paragraph.tsx} (92%) rename example_plugins/plugins/ui_detail/src/{metadata_icon.tsx => metadata-icon.tsx} (84%) rename example_plugins/plugins/ui_detail/src/{metadata_link.tsx => metadata-link.tsx} (86%) rename example_plugins/plugins/ui_detail/src/{metadata_separator.tsx => metadata-separator.tsx} (88%) create mode 100644 example_plugins/plugins/ui_detail/src/metadata-tag-list.tsx rename example_plugins/plugins/ui_detail/src/{metadata_value.tsx => metadata-value.tsx} (85%) delete mode 100644 example_plugins/plugins/ui_detail/src/metadata_tag_list.tsx rename example_plugins/plugins/ui_grid/src/{content_code_block.tsx => content-code-block.tsx} (90%) rename example_plugins/plugins/ui_grid/src/{content_headers.tsx => content-headers.tsx} (96%) rename example_plugins/plugins/ui_grid/src/{content_image.tsx => content-image.tsx} (96%) rename example_plugins/plugins/ui_grid/src/{content_paragraph.tsx => content-paragraph.tsx} (90%) rename example_plugins/plugins/ui_grid/src/{empty_view.tsx => empty-view.tsx} (87%) rename example_plugins/plugins/ui_grid/src/{search_bar.tsx => search-bar.tsx} (95%) rename example_plugins/plugins/ui_list/src/{content_code_block.tsx => content-code-block.tsx} (93%) rename example_plugins/plugins/ui_list/src/{content_headers.tsx => content-headers.tsx} (95%) rename example_plugins/plugins/ui_list/src/{content_horizontal_break.tsx => content-horizontal-break.tsx} (96%) rename example_plugins/plugins/ui_list/src/{content_image.tsx => content-image.tsx} (89%) rename example_plugins/plugins/ui_list/src/{content_paragraph.tsx => content-paragraph.tsx} (95%) rename example_plugins/plugins/ui_list/src/{empty_view.tsx => empty-view.tsx} (87%) rename example_plugins/plugins/ui_list/src/{metadata_icon.tsx => metadata-icon.tsx} (89%) rename example_plugins/plugins/ui_list/src/{metadata_link.tsx => metadata-link.tsx} (76%) rename example_plugins/plugins/ui_list/src/{metadata_separator.tsx => metadata-separator.tsx} (85%) rename example_plugins/plugins/ui_list/src/{metadata_tag_list.tsx => metadata-tag-list.tsx} (90%) rename example_plugins/plugins/ui_list/src/{metadata_value.tsx => metadata-value.tsx} (76%) rename example_plugins/plugins/ui_list/src/{search_bar.tsx => search-bar.tsx} (93%) diff --git a/example_plugins/plugins/ui_detail/gauntlet.toml b/example_plugins/plugins/ui_detail/gauntlet.toml index f950b31..b787a2e 100644 --- a/example_plugins/plugins/ui_detail/gauntlet.toml +++ b/example_plugins/plugins/ui_detail/gauntlet.toml @@ -15,7 +15,7 @@ description = '' [[entrypoint]] id = 'content-code-block' name = 'Content Code Block' -path = 'src/content_code_block.tsx' +path = 'src/content-code-block.tsx' type = 'view' description = '' # docs-code-segment:end @@ -24,7 +24,7 @@ description = '' [[entrypoint]] id = 'content-header' name = 'Content Header' -path = 'src/content_header.tsx' +path = 'src/content-header.tsx' type = 'view' description = '' # docs-code-segment:end @@ -33,7 +33,7 @@ description = '' [[entrypoint]] id = 'content-horizontal-break' name = 'Content Horizontal Break' -path = 'src/content_horizontal_break.tsx' +path = 'src/content-horizontal-break.tsx' type = 'view' description = '' # docs-code-segment:end @@ -42,7 +42,7 @@ description = '' [[entrypoint]] id = 'content-image' name = 'Content Image' -path = 'src/content_image.tsx' +path = 'src/content-image.tsx' type = 'view' description = '' # docs-code-segment:end @@ -51,7 +51,7 @@ description = '' [[entrypoint]] id = 'content-paragraph' name = 'Content Paragraph' -path = 'src/content_paragraph.tsx' +path = 'src/content-paragraph.tsx' type = 'view' description = '' # docs-code-segment:end @@ -78,7 +78,7 @@ description = '' [[entrypoint]] id = 'metadata-icon' name = 'Metadata Icon' -path = 'src/metadata_icon.tsx' +path = 'src/metadata-icon.tsx' type = 'view' description = '' # docs-code-segment:end @@ -87,7 +87,7 @@ description = '' [[entrypoint]] id = 'metadata-link' name = 'Metadata Link' -path = 'src/metadata_link.tsx' +path = 'src/metadata-link.tsx' type = 'view' description = '' # docs-code-segment:end @@ -96,7 +96,7 @@ description = '' [[entrypoint]] id = 'metadata-separator' name = 'Metadata Separator' -path = 'src/metadata_separator.tsx' +path = 'src/metadata-separator.tsx' type = 'view' description = '' # docs-code-segment:end @@ -105,7 +105,7 @@ description = '' [[entrypoint]] id = 'metadata-tag-list' name = 'Metadata Tag List' -path = 'src/metadata_tag_list.tsx' +path = 'src/metadata-tag-list.tsx' type = 'view' description = '' # docs-code-segment:end @@ -114,7 +114,7 @@ description = '' [[entrypoint]] id = 'metadata-value' name = 'Metadata Value' -path = 'src/metadata_value.tsx' +path = 'src/metadata-value.tsx' type = 'view' description = '' # docs-code-segment:end diff --git a/example_plugins/plugins/ui_detail/src/content_code_block.tsx b/example_plugins/plugins/ui_detail/src/content-code-block.tsx similarity index 88% rename from example_plugins/plugins/ui_detail/src/content_code_block.tsx rename to example_plugins/plugins/ui_detail/src/content-code-block.tsx index 5326068..5ade758 100644 --- a/example_plugins/plugins/ui_detail/src/content_code_block.tsx +++ b/example_plugins/plugins/ui_detail/src/content-code-block.tsx @@ -1,5 +1,5 @@ -import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Detail } from "@project-gauntlet/api/components"; const code = `\ fib :: Integer -> Integer @@ -8,7 +8,7 @@ fib 1 = 1 fib n = fib (n-1) + fib (n-2) ` -export default function Main(): ReactNode { +export default function ContentCodeBlock(): ReactNode { return ( diff --git a/example_plugins/plugins/ui_detail/src/content_header.tsx b/example_plugins/plugins/ui_detail/src/content-header.tsx similarity index 93% rename from example_plugins/plugins/ui_detail/src/content_header.tsx rename to example_plugins/plugins/ui_detail/src/content-header.tsx index 299defd..03bb711 100644 --- a/example_plugins/plugins/ui_detail/src/content_header.tsx +++ b/example_plugins/plugins/ui_detail/src/content-header.tsx @@ -1,7 +1,7 @@ -import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Detail } from "@project-gauntlet/api/components"; -export default function Main(): ReactNode { +export default function ContentHeader(): ReactNode { return ( diff --git a/example_plugins/plugins/ui_detail/src/content_horizontal_break.tsx b/example_plugins/plugins/ui_detail/src/content-horizontal-break.tsx similarity index 90% rename from example_plugins/plugins/ui_detail/src/content_horizontal_break.tsx rename to example_plugins/plugins/ui_detail/src/content-horizontal-break.tsx index b5f0a3d..bc86170 100644 --- a/example_plugins/plugins/ui_detail/src/content_horizontal_break.tsx +++ b/example_plugins/plugins/ui_detail/src/content-horizontal-break.tsx @@ -1,7 +1,7 @@ -import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Detail } from "@project-gauntlet/api/components"; -export default function Main(): ReactNode { +export default function ContentHorizontalBreak(): ReactNode { return ( diff --git a/example_plugins/plugins/ui_detail/src/content_image.tsx b/example_plugins/plugins/ui_detail/src/content-image.tsx similarity index 83% rename from example_plugins/plugins/ui_detail/src/content_image.tsx rename to example_plugins/plugins/ui_detail/src/content-image.tsx index 2109e1c..31b0c5c 100644 --- a/example_plugins/plugins/ui_detail/src/content_image.tsx +++ b/example_plugins/plugins/ui_detail/src/content-image.tsx @@ -1,9 +1,9 @@ -import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Detail } from "@project-gauntlet/api/components"; -const imgUrl = "https://static.wikia.nocookie.net/starwars/images/a/ae/The_Whills_Strike_Back.png/revision/latest/scale-to-width-down/400?cb=20201006180053" +const imgUrl = "https://static.wikia.nocookie.net/starwars/images/a/ae/The_Whills_Strike_Back.png/revision/latest/scale-to-width-down/250?cb=20201006180053" -export default function Main(): ReactNode { +export default function ContentImage(): ReactNode { return ( diff --git a/example_plugins/plugins/ui_detail/src/content_paragraph.tsx b/example_plugins/plugins/ui_detail/src/content-paragraph.tsx similarity index 92% rename from example_plugins/plugins/ui_detail/src/content_paragraph.tsx rename to example_plugins/plugins/ui_detail/src/content-paragraph.tsx index e7bfdf4..f219348 100644 --- a/example_plugins/plugins/ui_detail/src/content_paragraph.tsx +++ b/example_plugins/plugins/ui_detail/src/content-paragraph.tsx @@ -1,7 +1,7 @@ -import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Detail } from "@project-gauntlet/api/components"; -export default function Main(): ReactNode { +export default function ContentParagraph(): ReactNode { return ( diff --git a/example_plugins/plugins/ui_detail/src/content.tsx b/example_plugins/plugins/ui_detail/src/content.tsx index 3229f11..d89110f 100644 --- a/example_plugins/plugins/ui_detail/src/content.tsx +++ b/example_plugins/plugins/ui_detail/src/content.tsx @@ -1,7 +1,7 @@ -import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Detail } from "@project-gauntlet/api/components"; -export default function Main(): ReactNode { +export default function Content(): ReactNode { return ( diff --git a/example_plugins/plugins/ui_detail/src/main.tsx b/example_plugins/plugins/ui_detail/src/main.tsx index 8073b8f..e250d91 100644 --- a/example_plugins/plugins/ui_detail/src/main.tsx +++ b/example_plugins/plugins/ui_detail/src/main.tsx @@ -1,9 +1,15 @@ -import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Action, ActionPanel, Detail } from "@project-gauntlet/api/components"; export default function Main(): ReactNode { return ( - + + {/* */}}/> + + } + > Sentient Humanoid diff --git a/example_plugins/plugins/ui_detail/src/metadata_icon.tsx b/example_plugins/plugins/ui_detail/src/metadata-icon.tsx similarity index 84% rename from example_plugins/plugins/ui_detail/src/metadata_icon.tsx rename to example_plugins/plugins/ui_detail/src/metadata-icon.tsx index 48b8b41..5011693 100644 --- a/example_plugins/plugins/ui_detail/src/metadata_icon.tsx +++ b/example_plugins/plugins/ui_detail/src/metadata-icon.tsx @@ -1,7 +1,7 @@ -import { Detail, Icons } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Detail, Icons } from "@project-gauntlet/api/components"; -export default function Main(): ReactNode { +export default function MetadataIcon(): ReactNode { return ( diff --git a/example_plugins/plugins/ui_detail/src/metadata_link.tsx b/example_plugins/plugins/ui_detail/src/metadata-link.tsx similarity index 86% rename from example_plugins/plugins/ui_detail/src/metadata_link.tsx rename to example_plugins/plugins/ui_detail/src/metadata-link.tsx index bbaef8c..7f7d8cf 100644 --- a/example_plugins/plugins/ui_detail/src/metadata_link.tsx +++ b/example_plugins/plugins/ui_detail/src/metadata-link.tsx @@ -1,7 +1,7 @@ -import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Detail } from "@project-gauntlet/api/components"; -export default function Main(): ReactNode { +export default function MetadataLink(): ReactNode { return ( diff --git a/example_plugins/plugins/ui_detail/src/metadata_separator.tsx b/example_plugins/plugins/ui_detail/src/metadata-separator.tsx similarity index 88% rename from example_plugins/plugins/ui_detail/src/metadata_separator.tsx rename to example_plugins/plugins/ui_detail/src/metadata-separator.tsx index 1b0921f..a4dca12 100644 --- a/example_plugins/plugins/ui_detail/src/metadata_separator.tsx +++ b/example_plugins/plugins/ui_detail/src/metadata-separator.tsx @@ -1,7 +1,7 @@ -import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Detail } from "@project-gauntlet/api/components"; -export default function Main(): ReactNode { +export default function MetadataSeparator(): ReactNode { return ( diff --git a/example_plugins/plugins/ui_detail/src/metadata-tag-list.tsx b/example_plugins/plugins/ui_detail/src/metadata-tag-list.tsx new file mode 100644 index 0000000..8292e75 --- /dev/null +++ b/example_plugins/plugins/ui_detail/src/metadata-tag-list.tsx @@ -0,0 +1,31 @@ +import { ReactNode } from "react"; +import { Detail } from "@project-gauntlet/api/components"; +import { showHud } from "@project-gauntlet/api/helpers"; + +const items = [ + "The Screaming Citadel 1", + "Doctor Aphra (2016) 9", + "Doctor Aphra (2016) 10", + "Doctor Aphra (2016) 11", + "Doctor Aphra (2016) 12" +] + +export default function MetadataTagList(): ReactNode { + return ( + + + + { + items.map(value => { + return ( + showHud(value)}> + {value} + + ) + }) + } + + + + ) +} diff --git a/example_plugins/plugins/ui_detail/src/metadata_value.tsx b/example_plugins/plugins/ui_detail/src/metadata-value.tsx similarity index 85% rename from example_plugins/plugins/ui_detail/src/metadata_value.tsx rename to example_plugins/plugins/ui_detail/src/metadata-value.tsx index 0f4c7b4..e3d90d8 100644 --- a/example_plugins/plugins/ui_detail/src/metadata_value.tsx +++ b/example_plugins/plugins/ui_detail/src/metadata-value.tsx @@ -1,7 +1,7 @@ -import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Detail } from "@project-gauntlet/api/components"; -export default function Main(): ReactNode { +export default function MetadataValue(): ReactNode { return ( diff --git a/example_plugins/plugins/ui_detail/src/metadata.tsx b/example_plugins/plugins/ui_detail/src/metadata.tsx index 5ea91c9..9bba77d 100644 --- a/example_plugins/plugins/ui_detail/src/metadata.tsx +++ b/example_plugins/plugins/ui_detail/src/metadata.tsx @@ -1,7 +1,7 @@ -import { Detail } from "@project-gauntlet/api/components"; import { ReactNode } from "react"; +import { Detail } from "@project-gauntlet/api/components"; -export default function Main(): ReactNode { +export default function Metadata(): ReactNode { return ( diff --git a/example_plugins/plugins/ui_detail/src/metadata_tag_list.tsx b/example_plugins/plugins/ui_detail/src/metadata_tag_list.tsx deleted file mode 100644 index 858a784..0000000 --- a/example_plugins/plugins/ui_detail/src/metadata_tag_list.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Detail } from "@project-gauntlet/api/components"; -import { ReactNode } from "react"; - -export default function Main(): ReactNode { - return ( - - - - The Screaming Citadel 1 - Doctor Aphra (2016) 9 - Doctor Aphra (2016) 10 - Doctor Aphra (2016) 11 - Doctor Aphra (2016) 12 - - - - ) -} diff --git a/example_plugins/plugins/ui_form/src/checkbox.tsx b/example_plugins/plugins/ui_form/src/checkbox.tsx index e545fa4..5cd0c18 100644 --- a/example_plugins/plugins/ui_form/src/checkbox.tsx +++ b/example_plugins/plugins/ui_form/src/checkbox.tsx @@ -1,7 +1,7 @@ import { ReactElement } from 'react'; import { Form } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function CheckboxExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_form/src/select.tsx b/example_plugins/plugins/ui_form/src/select.tsx index 891975a..ef9cb99 100644 --- a/example_plugins/plugins/ui_form/src/select.tsx +++ b/example_plugins/plugins/ui_form/src/select.tsx @@ -1,7 +1,7 @@ import { ReactElement } from 'react'; import { Form } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function SelectExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_form/src/separator.tsx b/example_plugins/plugins/ui_form/src/separator.tsx index 4e39930..d3622c5 100644 --- a/example_plugins/plugins/ui_form/src/separator.tsx +++ b/example_plugins/plugins/ui_form/src/separator.tsx @@ -1,7 +1,7 @@ import { ReactElement } from 'react'; import { Form } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function SeparatorExample(): ReactElement { return ( {items.map(value => ( diff --git a/example_plugins/plugins/ui_grid/src/content_headers.tsx b/example_plugins/plugins/ui_grid/src/content-headers.tsx similarity index 96% rename from example_plugins/plugins/ui_grid/src/content_headers.tsx rename to example_plugins/plugins/ui_grid/src/content-headers.tsx index db65312..0d2b070 100644 --- a/example_plugins/plugins/ui_grid/src/content_headers.tsx +++ b/example_plugins/plugins/ui_grid/src/content-headers.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; import { Grid } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function ContentHeadersExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_grid/src/content_image.tsx b/example_plugins/plugins/ui_grid/src/content-image.tsx similarity index 96% rename from example_plugins/plugins/ui_grid/src/content_image.tsx rename to example_plugins/plugins/ui_grid/src/content-image.tsx index 60e0ac0..fd71682 100644 --- a/example_plugins/plugins/ui_grid/src/content_image.tsx +++ b/example_plugins/plugins/ui_grid/src/content-image.tsx @@ -32,7 +32,7 @@ const items = [ }, ] -export default function Main(): ReactElement { +export default function ContentImageExample(): ReactElement { return ( {items.map(value => ( diff --git a/example_plugins/plugins/ui_grid/src/content_paragraph.tsx b/example_plugins/plugins/ui_grid/src/content-paragraph.tsx similarity index 90% rename from example_plugins/plugins/ui_grid/src/content_paragraph.tsx rename to example_plugins/plugins/ui_grid/src/content-paragraph.tsx index 44d76f4..9bc0019 100644 --- a/example_plugins/plugins/ui_grid/src/content_paragraph.tsx +++ b/example_plugins/plugins/ui_grid/src/content-paragraph.tsx @@ -10,7 +10,7 @@ const items = [ "C1-10P", ] -export default function Main(): ReactElement { +export default function ContentParagraphExample(): ReactElement { return ( {items.map(value => ( diff --git a/example_plugins/plugins/ui_grid/src/empty_view.tsx b/example_plugins/plugins/ui_grid/src/empty-view.tsx similarity index 87% rename from example_plugins/plugins/ui_grid/src/empty_view.tsx rename to example_plugins/plugins/ui_grid/src/empty-view.tsx index 418fed3..5aeb75f 100644 --- a/example_plugins/plugins/ui_grid/src/empty_view.tsx +++ b/example_plugins/plugins/ui_grid/src/empty-view.tsx @@ -3,7 +3,7 @@ import { Grid } from "@project-gauntlet/api/components"; const alderaanImage = "https://static.wikia.nocookie.net/starwars/images/4/4a/Alderaan.jpg/revision/latest?cb=20061211013805" -export default function Main(): ReactElement { +export default function EmptyViewExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_grid/src/main.tsx b/example_plugins/plugins/ui_grid/src/main.tsx index 60e0ac0..07ca131 100644 --- a/example_plugins/plugins/ui_grid/src/main.tsx +++ b/example_plugins/plugins/ui_grid/src/main.tsx @@ -32,7 +32,7 @@ const items = [ }, ] -export default function Main(): ReactElement { +export default function MainExample(): ReactElement { return ( {items.map(value => ( diff --git a/example_plugins/plugins/ui_grid/src/search_bar.tsx b/example_plugins/plugins/ui_grid/src/search-bar.tsx similarity index 95% rename from example_plugins/plugins/ui_grid/src/search_bar.tsx rename to example_plugins/plugins/ui_grid/src/search-bar.tsx index ce67753..3fffa9c 100644 --- a/example_plugins/plugins/ui_grid/src/search_bar.tsx +++ b/example_plugins/plugins/ui_grid/src/search-bar.tsx @@ -11,7 +11,7 @@ const results = [ "Mandalorian Culture" ] -export default function Main(): ReactElement { +export default function SearchBarExample(): ReactElement { const [searchText, setSearchText] = useState(""); return ( diff --git a/example_plugins/plugins/ui_grid/src/section.tsx b/example_plugins/plugins/ui_grid/src/section.tsx index 2d0ab75..2ec9c59 100644 --- a/example_plugins/plugins/ui_grid/src/section.tsx +++ b/example_plugins/plugins/ui_grid/src/section.tsx @@ -14,7 +14,7 @@ const vader5 = "https://static.wikia.nocookie.net/starwars/images/a/ab/Darthvade const vader6 = "https://static.wikia.nocookie.net/starwars/images/2/20/DarthVader-DLotS--Solicitation.jpg/revision/latest/scale-to-width-down/150?cb=20171001165404" const vader7 = "https://static.wikia.nocookie.net/starwars/images/f/fa/DarthVader2017-7.jpg/revision/latest/scale-to-width-down/150?cb=20190226233333" -export default function Main(): ReactElement { +export default function SectionExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/gauntlet.toml b/example_plugins/plugins/ui_list/gauntlet.toml index c0e7845..5c61845 100644 --- a/example_plugins/plugins/ui_list/gauntlet.toml +++ b/example_plugins/plugins/ui_list/gauntlet.toml @@ -16,7 +16,7 @@ description = '' [[entrypoint]] id = 'empty-view' name = 'Empty View' -path = 'src/empty_view.tsx' +path = 'src/empty-view.tsx' type = 'view' description = '' # docs-code-segment:end @@ -43,7 +43,7 @@ description = '' [[entrypoint]] id = 'search-bar' name = 'List Search bar' -path = 'src/search_bar.tsx' +path = 'src/search-bar.tsx' type = 'view' description = '' # docs-code-segment:end @@ -70,7 +70,7 @@ description = '' [[entrypoint]] id = 'content-code-block' name = 'List Content Code Block' -path = 'src/content_code_block.tsx' +path = 'src/content-code-block.tsx' type = 'view' description = '' # docs-code-segment:end @@ -79,7 +79,7 @@ description = '' [[entrypoint]] id = 'content-headers' name = 'List Content Headers' -path = 'src/content_headers.tsx' +path = 'src/content-headers.tsx' type = 'view' description = '' # docs-code-segment:end @@ -88,7 +88,7 @@ description = '' [[entrypoint]] id = 'content-horizontal-break' name = 'List Content Horizontal Break' -path = 'src/content_horizontal_break.tsx' +path = 'src/content-horizontal-break.tsx' type = 'view' description = '' # docs-code-segment:end @@ -97,7 +97,7 @@ description = '' [[entrypoint]] id = 'content-image' name = 'List Content Image' -path = 'src/content_image.tsx' +path = 'src/content-image.tsx' type = 'view' description = '' # docs-code-segment:end @@ -106,7 +106,7 @@ description = '' [[entrypoint]] id = 'content-paragraph' name = 'List Content Paragraph' -path = 'src/content_paragraph.tsx' +path = 'src/content-paragraph.tsx' type = 'view' description = '' # docs-code-segment:end @@ -124,7 +124,7 @@ description = '' [[entrypoint]] id = 'metadata-icon' name = 'List Metadata Icon' -path = 'src/metadata_icon.tsx' +path = 'src/metadata-icon.tsx' type = 'view' description = '' # docs-code-segment:end @@ -133,7 +133,7 @@ description = '' [[entrypoint]] id = 'metadata-icon' name = 'List Metadata Icon' -path = 'src/metadata_icon.tsx' +path = 'src/metadata-icon.tsx' type = 'view' description = '' # docs-code-segment:end @@ -142,7 +142,7 @@ description = '' [[entrypoint]] id = 'metadata-link' name = 'List Metadata Link' -path = 'src/metadata_link.tsx' +path = 'src/metadata-link.tsx' type = 'view' description = '' # docs-code-segment:end @@ -151,7 +151,7 @@ description = '' [[entrypoint]] id = 'metadata-separator' name = 'List Metadata Separator' -path = 'src/metadata_separator.tsx' +path = 'src/metadata-separator.tsx' type = 'view' description = '' # docs-code-segment:end @@ -160,7 +160,7 @@ description = '' [[entrypoint]] id = 'metadata-tag-list' name = 'List Metadata Tag List' -path = 'src/metadata_tag_list.tsx' +path = 'src/metadata-tag-list.tsx' type = 'view' description = '' # docs-code-segment:end @@ -169,7 +169,7 @@ description = '' [[entrypoint]] id = 'metadata-value' name = 'List Metadata Value' -path = 'src/metadata_value.tsx' +path = 'src/metadata-value.tsx' type = 'view' description = '' # docs-code-segment:end diff --git a/example_plugins/plugins/ui_list/src/content_code_block.tsx b/example_plugins/plugins/ui_list/src/content-code-block.tsx similarity index 93% rename from example_plugins/plugins/ui_list/src/content_code_block.tsx rename to example_plugins/plugins/ui_list/src/content-code-block.tsx index 11a9a02..6b609c5 100644 --- a/example_plugins/plugins/ui_list/src/content_code_block.tsx +++ b/example_plugins/plugins/ui_list/src/content-code-block.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; import { List } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function ContentCodeBlockExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/src/content_headers.tsx b/example_plugins/plugins/ui_list/src/content-headers.tsx similarity index 95% rename from example_plugins/plugins/ui_list/src/content_headers.tsx rename to example_plugins/plugins/ui_list/src/content-headers.tsx index a811601..aac9a0b 100644 --- a/example_plugins/plugins/ui_list/src/content_headers.tsx +++ b/example_plugins/plugins/ui_list/src/content-headers.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; import { List } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function ContentHeadersExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/src/content_horizontal_break.tsx b/example_plugins/plugins/ui_list/src/content-horizontal-break.tsx similarity index 96% rename from example_plugins/plugins/ui_list/src/content_horizontal_break.tsx rename to example_plugins/plugins/ui_list/src/content-horizontal-break.tsx index 7690bf2..e180569 100644 --- a/example_plugins/plugins/ui_list/src/content_horizontal_break.tsx +++ b/example_plugins/plugins/ui_list/src/content-horizontal-break.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; import { List } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function ContentHorizontalBreakExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/src/content_image.tsx b/example_plugins/plugins/ui_list/src/content-image.tsx similarity index 89% rename from example_plugins/plugins/ui_list/src/content_image.tsx rename to example_plugins/plugins/ui_list/src/content-image.tsx index ed3ca2f..edc46c9 100644 --- a/example_plugins/plugins/ui_list/src/content_image.tsx +++ b/example_plugins/plugins/ui_list/src/content-image.tsx @@ -3,7 +3,7 @@ import { List } from "@project-gauntlet/api/components"; const imgUrl = "https://static.wikia.nocookie.net/starwars/images/e/e4/Ezaraa.png/revision/latest/scale-to-width-down/200?cb=20170511082800" -export default function Main(): ReactElement { +export default function ContentImageExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/src/content_paragraph.tsx b/example_plugins/plugins/ui_list/src/content-paragraph.tsx similarity index 95% rename from example_plugins/plugins/ui_list/src/content_paragraph.tsx rename to example_plugins/plugins/ui_list/src/content-paragraph.tsx index 29b1f8d..a297048 100644 --- a/example_plugins/plugins/ui_list/src/content_paragraph.tsx +++ b/example_plugins/plugins/ui_list/src/content-paragraph.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; import { List } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function ContentParagraphExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/src/content.tsx b/example_plugins/plugins/ui_list/src/content.tsx index 72c7ecd..c1f2435 100644 --- a/example_plugins/plugins/ui_list/src/content.tsx +++ b/example_plugins/plugins/ui_list/src/content.tsx @@ -1,7 +1,7 @@ -import React, { ReactElement } from "react"; +import { ReactElement } from "react"; import { Action, ActionPanel, List } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function ContentExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/src/empty_view.tsx b/example_plugins/plugins/ui_list/src/empty-view.tsx similarity index 87% rename from example_plugins/plugins/ui_list/src/empty_view.tsx rename to example_plugins/plugins/ui_list/src/empty-view.tsx index 0a41444..c487984 100644 --- a/example_plugins/plugins/ui_list/src/empty_view.tsx +++ b/example_plugins/plugins/ui_list/src/empty-view.tsx @@ -3,7 +3,7 @@ import { List } from "@project-gauntlet/api/components"; const alderaanImage = "https://static.wikia.nocookie.net/starwars/images/4/4a/Alderaan.jpg/revision/latest?cb=20061211013805" -export default function Main(): ReactElement { +export default function EmptyViewExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/src/item.tsx b/example_plugins/plugins/ui_list/src/item.tsx index 9b94484..8a19abd 100644 --- a/example_plugins/plugins/ui_list/src/item.tsx +++ b/example_plugins/plugins/ui_list/src/item.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; import { IconAccessory, Icons, List, TextAccessory } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function ItemExample(): ReactElement { return ( ]}/> diff --git a/example_plugins/plugins/ui_list/src/main.tsx b/example_plugins/plugins/ui_list/src/main.tsx index 4fc65b8..6c82394 100644 --- a/example_plugins/plugins/ui_list/src/main.tsx +++ b/example_plugins/plugins/ui_list/src/main.tsx @@ -1,7 +1,7 @@ -import React, { ReactElement } from "react"; +import { ReactElement } from "react"; import { Action, ActionPanel, IconAccessory, Icons, List, TextAccessory } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function MainExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/src/metadata_link.tsx b/example_plugins/plugins/ui_list/src/metadata-link.tsx similarity index 76% rename from example_plugins/plugins/ui_list/src/metadata_link.tsx rename to example_plugins/plugins/ui_list/src/metadata-link.tsx index bc99323..c9f0ff6 100644 --- a/example_plugins/plugins/ui_list/src/metadata_link.tsx +++ b/example_plugins/plugins/ui_list/src/metadata-link.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; -import { Icons, List } from "@project-gauntlet/api/components"; +import { List } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function MetadataLinkExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/src/metadata_separator.tsx b/example_plugins/plugins/ui_list/src/metadata-separator.tsx similarity index 85% rename from example_plugins/plugins/ui_list/src/metadata_separator.tsx rename to example_plugins/plugins/ui_list/src/metadata-separator.tsx index 0cf8e38..62072b7 100644 --- a/example_plugins/plugins/ui_list/src/metadata_separator.tsx +++ b/example_plugins/plugins/ui_list/src/metadata-separator.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; -import { Icons, List } from "@project-gauntlet/api/components"; +import { List } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function MetadataSeparatorExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/src/metadata_tag_list.tsx b/example_plugins/plugins/ui_list/src/metadata-tag-list.tsx similarity index 90% rename from example_plugins/plugins/ui_list/src/metadata_tag_list.tsx rename to example_plugins/plugins/ui_list/src/metadata-tag-list.tsx index 89316aa..c9a83d7 100644 --- a/example_plugins/plugins/ui_list/src/metadata_tag_list.tsx +++ b/example_plugins/plugins/ui_list/src/metadata-tag-list.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; -import { Icons, List } from "@project-gauntlet/api/components"; +import { List } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function MetadataTagListExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/src/metadata_value.tsx b/example_plugins/plugins/ui_list/src/metadata-value.tsx similarity index 76% rename from example_plugins/plugins/ui_list/src/metadata_value.tsx rename to example_plugins/plugins/ui_list/src/metadata-value.tsx index a922c7a..c44eca0 100644 --- a/example_plugins/plugins/ui_list/src/metadata_value.tsx +++ b/example_plugins/plugins/ui_list/src/metadata-value.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; -import { Icons, List } from "@project-gauntlet/api/components"; +import { List } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function MetadataValueExample(): ReactElement { return ( diff --git a/example_plugins/plugins/ui_list/src/metadata.tsx b/example_plugins/plugins/ui_list/src/metadata.tsx index 7d06e3a..dc4a5ea 100644 --- a/example_plugins/plugins/ui_list/src/metadata.tsx +++ b/example_plugins/plugins/ui_list/src/metadata.tsx @@ -1,7 +1,7 @@ -import React, { ReactElement } from "react"; +import { ReactElement } from "react"; import { Action, ActionPanel, List } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function MetadataExample(): ReactElement { return ( (""); return ( diff --git a/example_plugins/plugins/ui_list/src/section.tsx b/example_plugins/plugins/ui_list/src/section.tsx index 9f23846..ded6510 100644 --- a/example_plugins/plugins/ui_list/src/section.tsx +++ b/example_plugins/plugins/ui_list/src/section.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; import { List } from "@project-gauntlet/api/components"; -export default function Main(): ReactElement { +export default function SectionExample(): ReactElement { return ( From 2e2864e88a46fbdd52d0b2655f131f2fc38da72a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 22 Jan 2025 20:38:27 +0100 Subject: [PATCH 398/540] Replace ` with " in docs markdown files --- docs/js/components/checkbox/description.md | 2 +- docs/js/components/date_picker/description.md | 2 +- docs/js/components/detail/props/isLoading.md | 2 +- docs/js/components/form/props/isLoading.md | 2 +- docs/js/components/grid/props/isLoading.md | 2 +- docs/js/components/image/props/source.md | 2 +- docs/js/components/inline/description.md | 2 +- docs/js/components/list/props/isLoading.md | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/js/components/checkbox/description.md b/docs/js/components/checkbox/description.md index 5a364d6..5dd522a 100644 --- a/docs/js/components/checkbox/description.md +++ b/docs/js/components/checkbox/description.md @@ -1 +1 @@ -Checkbox is a type of form input that produces boolean value, either `true` or `false` \ No newline at end of file +Checkbox is a type of form input that produces boolean value, either "true" or "false" \ No newline at end of file diff --git a/docs/js/components/date_picker/description.md b/docs/js/components/date_picker/description.md index b9745ec..de8191c 100644 --- a/docs/js/components/date_picker/description.md +++ b/docs/js/components/date_picker/description.md @@ -1 +1 @@ -Date Picker is a type of form input that produces date value represented as a string in `YYYY-MM-DD` format \ No newline at end of file +Date Picker is a type of form input that produces date value represented as a string in "YYYY-MM-DD" format \ No newline at end of file diff --git a/docs/js/components/detail/props/isLoading.md b/docs/js/components/detail/props/isLoading.md index d1ebe77..fa5cc2d 100644 --- a/docs/js/components/detail/props/isLoading.md +++ b/docs/js/components/detail/props/isLoading.md @@ -1 +1 @@ -If `true` loading bar is shown above content \ No newline at end of file +If "true" loading bar is shown above content \ No newline at end of file diff --git a/docs/js/components/form/props/isLoading.md b/docs/js/components/form/props/isLoading.md index d1ebe77..fa5cc2d 100644 --- a/docs/js/components/form/props/isLoading.md +++ b/docs/js/components/form/props/isLoading.md @@ -1 +1 @@ -If `true` loading bar is shown above content \ No newline at end of file +If "true" loading bar is shown above content \ No newline at end of file diff --git a/docs/js/components/grid/props/isLoading.md b/docs/js/components/grid/props/isLoading.md index d1ebe77..fa5cc2d 100644 --- a/docs/js/components/grid/props/isLoading.md +++ b/docs/js/components/grid/props/isLoading.md @@ -1 +1 @@ -If `true` loading bar is shown above content \ No newline at end of file +If "true" loading bar is shown above content \ No newline at end of file diff --git a/docs/js/components/image/props/source.md b/docs/js/components/image/props/source.md index f5c0a66..a126898 100644 --- a/docs/js/components/image/props/source.md +++ b/docs/js/components/image/props/source.md @@ -1 +1 @@ -Image data. Supported formats: `png`, `gif`, `jpg`, `webp`, `tiff` \ No newline at end of file +Image data. Supported formats: "png", "gif', "jpg", "webp", "tiff" \ No newline at end of file diff --git a/docs/js/components/inline/description.md b/docs/js/components/inline/description.md index 7031f28..3e2c3ca 100644 --- a/docs/js/components/inline/description.md +++ b/docs/js/components/inline/description.md @@ -1,2 +1,2 @@ -Inline is a root component used with `inline-view` entrypoint type. +Inline is a root component used with "inline-view" entrypoint type. Displayed right under search bar in main view \ No newline at end of file diff --git a/docs/js/components/list/props/isLoading.md b/docs/js/components/list/props/isLoading.md index d1ebe77..fa5cc2d 100644 --- a/docs/js/components/list/props/isLoading.md +++ b/docs/js/components/list/props/isLoading.md @@ -1 +1 @@ -If `true` loading bar is shown above content \ No newline at end of file +If "true" loading bar is shown above content \ No newline at end of file From 8fc21934dee3eaf3ddbb93ad1859fe85d8e4f514 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Wed, 22 Jan 2025 21:23:00 +0100 Subject: [PATCH 399/540] Improve ui form plugin examples --- .../plugins/ui_form/src/checkbox.tsx | 3 +- example_plugins/plugins/ui_form/src/main.tsx | 72 +++++++++---------- .../plugins/ui_form/src/password-field.tsx | 2 +- .../plugins/ui_form/src/select.tsx | 14 ++-- .../plugins/ui_form/src/separator.tsx | 16 ++--- .../plugins/ui_form/src/text-field.tsx | 4 +- 6 files changed, 48 insertions(+), 63 deletions(-) diff --git a/example_plugins/plugins/ui_form/src/checkbox.tsx b/example_plugins/plugins/ui_form/src/checkbox.tsx index 5cd0c18..3f5c365 100644 --- a/example_plugins/plugins/ui_form/src/checkbox.tsx +++ b/example_plugins/plugins/ui_form/src/checkbox.tsx @@ -5,7 +5,8 @@ export default function CheckboxExample(): ReactElement { return ( { console.log(`value: ${value}`) diff --git a/example_plugins/plugins/ui_form/src/main.tsx b/example_plugins/plugins/ui_form/src/main.tsx index 3335f37..6afb36f 100644 --- a/example_plugins/plugins/ui_form/src/main.tsx +++ b/example_plugins/plugins/ui_form/src/main.tsx @@ -1,50 +1,35 @@ import { ReactElement } from 'react'; -import { Form } from "@project-gauntlet/api/components"; - -// TODO remake into starwars themed +import { Action, ActionPanel, Form } from "@project-gauntlet/api/components"; export default function MainExample(): ReactElement { return ( - + + {}}/> + + } + > + { + console.log(`First Name: ${value}`) + }}/> + { + console.log(`Last Name: ${value}`) + }}/> - Burger + label="Species" + value="human" + onChange={value => { + console.log(`Last Name: ${value}`) + }}> + Human + Jawa + Hutt + Twi'lek - { - console.log(`value: ${value}`) - }} - /> - { - console.log(`value: ${value}`) - }} - /> - { - console.log(`value: ${value}`) - }} - /> - { - console.log(`value: ${value}`) - }} - /> - { - console.log(`value: ${value}`) - }} - /> + + + { + console.log(`terms of service: ${value}`) + }} + /> ); }; diff --git a/example_plugins/plugins/ui_form/src/password-field.tsx b/example_plugins/plugins/ui_form/src/password-field.tsx index 5ec40d5..b4042fe 100644 --- a/example_plugins/plugins/ui_form/src/password-field.tsx +++ b/example_plugins/plugins/ui_form/src/password-field.tsx @@ -4,7 +4,7 @@ import { Form } from "@project-gauntlet/api/components"; export default function PasswordFieldExample(): ReactElement { return (

- + ); }; diff --git a/example_plugins/plugins/ui_form/src/select.tsx b/example_plugins/plugins/ui_form/src/select.tsx index ef9cb99..6753d63 100644 --- a/example_plugins/plugins/ui_form/src/select.tsx +++ b/example_plugins/plugins/ui_form/src/select.tsx @@ -4,13 +4,13 @@ import { Form } from "@project-gauntlet/api/components"; export default function SelectExample(): ReactElement { return (
- - Burger - Hot Dog - Croissant - Cookies - Steak - Seafood + + Human + Jawa + Hutt + Twi'lek + Wookie + Kaminoan ); diff --git a/example_plugins/plugins/ui_form/src/separator.tsx b/example_plugins/plugins/ui_form/src/separator.tsx index d3622c5..2090282 100644 --- a/example_plugins/plugins/ui_form/src/separator.tsx +++ b/example_plugins/plugins/ui_form/src/separator.tsx @@ -4,19 +4,11 @@ import { Form } from "@project-gauntlet/api/components"; export default function SeparatorExample(): ReactElement { return (
- { - console.log(`value: ${value}`) - }} - /> + + - { - console.log(`value: ${value}`) - }} - /> + + ); }; diff --git a/example_plugins/plugins/ui_form/src/text-field.tsx b/example_plugins/plugins/ui_form/src/text-field.tsx index e35eb21..d50930c 100644 --- a/example_plugins/plugins/ui_form/src/text-field.tsx +++ b/example_plugins/plugins/ui_form/src/text-field.tsx @@ -5,9 +5,9 @@ export default function TextFieldExample(): ReactElement { return (
{ - console.log(`value: ${value}`) + console.log(`homeworld: ${value}`) }} /> From 6e04702903cf3be66cf21fd641665a812e4afaa4 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 23 Jan 2025 19:56:08 +0100 Subject: [PATCH 400/540] Add 3 more grid examples. Tweak others --- docs/js/components/content/description.md | 2 +- docs/js/components/search_bar/description.md | 2 +- example_plugins/plugins/ui_grid/gauntlet.toml | 28 +++++++++++ .../ui_grid/src/content-horizontal-break.tsx | 20 ++++++++ .../plugins/ui_grid/src/content-image.tsx | 37 +------------- .../plugins/ui_grid/src/more-columns.tsx | 29 +++++++++++ .../src/search-bar-set-search-text.tsx | 28 +++++++++++ .../plugins/ui_grid/src/section.tsx | 48 ------------------- .../content-horizontal-break/default.json | 3 ++ .../ui_grid/more-columns/default.json | 3 ++ 10 files changed, 115 insertions(+), 85 deletions(-) create mode 100644 example_plugins/plugins/ui_grid/src/content-horizontal-break.tsx create mode 100644 example_plugins/plugins/ui_grid/src/more-columns.tsx create mode 100644 example_plugins/plugins/ui_grid/src/search-bar-set-search-text.tsx create mode 100644 example_plugins/scenarios/ui_grid/content-horizontal-break/default.json create mode 100644 example_plugins/scenarios/ui_grid/more-columns/default.json diff --git a/docs/js/components/content/description.md b/docs/js/components/content/description.md index 77668fe..ddce9f9 100644 --- a/docs/js/components/content/description.md +++ b/docs/js/components/content/description.md @@ -1,3 +1,3 @@ -A container for a set of non-interactable components. +Content is a container for a set of non-interactable components. Used in a variety of places like , and . By utilizing the power of React the content can also be made dynamic \ No newline at end of file diff --git a/docs/js/components/search_bar/description.md b/docs/js/components/search_bar/description.md index 1ecfae1..dfce5b5 100644 --- a/docs/js/components/search_bar/description.md +++ b/docs/js/components/search_bar/description.md @@ -1 +1 @@ -Adds search bar above the content \ No newline at end of file +Adds search bar above the content. Text in search bar can be read and set \ No newline at end of file diff --git a/example_plugins/plugins/ui_grid/gauntlet.toml b/example_plugins/plugins/ui_grid/gauntlet.toml index a4d408f..56c6b83 100644 --- a/example_plugins/plugins/ui_grid/gauntlet.toml +++ b/example_plugins/plugins/ui_grid/gauntlet.toml @@ -21,6 +21,24 @@ type = 'view' description = '' # docs-code-segment:end +# docs-code-segment:start more-columns +[[entrypoint]] +id = 'more-columns' +name = 'Grid More Columns' +path = 'src/more-columns.tsx' +type = 'view' +description = '' +# docs-code-segment:end + +# docs-code-segment:start search-bar-set-search-text +[[entrypoint]] +id = 'search-bar-set-search-text' +name = 'Grid Search Bar Set Search Text' +path = 'src/search-bar-set-search-text.tsx' +type = 'view' +description = '' +# docs-code-segment:end + # docs-code-segment:start section [[entrypoint]] id = 'section' @@ -75,5 +93,15 @@ type = 'view' description = '' # docs-code-segment:end + +# docs-code-segment:start content-horizontal-break +[[entrypoint]] +id = 'content-horizontal-break' +name = 'Grid Content Horizontal Break' +path = 'src/content-horizontal-break.tsx' +type = 'view' +description = '' +# docs-code-segment:end + [permissions] network = ["static.wikia.nocookie.net"] diff --git a/example_plugins/plugins/ui_grid/src/content-horizontal-break.tsx b/example_plugins/plugins/ui_grid/src/content-horizontal-break.tsx new file mode 100644 index 0000000..7a40967 --- /dev/null +++ b/example_plugins/plugins/ui_grid/src/content-horizontal-break.tsx @@ -0,0 +1,20 @@ +import { ReactElement } from "react"; +import { Grid } from "@project-gauntlet/api/components"; + +export default function ContentHorizontalBreak(): ReactElement { + return ( + + + + + C-3PO + + + + BB-8 + + + + + ) +} \ No newline at end of file diff --git a/example_plugins/plugins/ui_grid/src/content-image.tsx b/example_plugins/plugins/ui_grid/src/content-image.tsx index fd71682..b07b68a 100644 --- a/example_plugins/plugins/ui_grid/src/content-image.tsx +++ b/example_plugins/plugins/ui_grid/src/content-image.tsx @@ -1,47 +1,14 @@ import { ReactElement } from "react"; import { Grid } from "@project-gauntlet/api/components"; -const items = [ - { - title: "Naboo", - image: "https://static.wikia.nocookie.net/star-wars-canon/images/2/24/NabooFull-SW.png/revision/latest/scale-to-width-down/150?cb=20151218205422" - }, - { - title: "Ryloth", - image: "https://static.wikia.nocookie.net/star-wars-canon/images/b/b7/Ryloth_Rebels.png/revision/latest/scale-to-width-down/150?cb=20161103040944" - }, - { - title: "Tatooine", - image: "https://static.wikia.nocookie.net/star-wars-canon/images/b/b0/Tatooine_TPM.png/revision/latest/scale-to-width-down/150?cb=20151124205032" - }, - { - title: "Dagobah", - image: "https://static.wikia.nocookie.net/star-wars-canon/images/4/48/Dagobah_ep3.jpg/revision/latest/scale-to-width-down/150?cb=20161103221846" - }, - { - title: "Endor", - image: "https://static.wikia.nocookie.net/star-wars-canon/images/9/96/Endor-DB.png/revision/latest/scale-to-width-down/150?cb=20160711234205" - }, - { - title: "Dathomir", - image: "https://static.wikia.nocookie.net/starwars/images/3/34/DathomirJFO.jpg/revision/latest/scale-to-width-down/150?cb=20200222032237" - }, - { - title: "Dantooine", - image: "https://static.wikia.nocookie.net/starwars/images/a/a5/Dantooine_Resistance.jpg/revision/latest/scale-to-width-down/150?cb=20200120190043" - }, -] - export default function ContentImageExample(): ReactElement { return ( - {items.map(value => ( - + - + - ))} ) } \ No newline at end of file diff --git a/example_plugins/plugins/ui_grid/src/more-columns.tsx b/example_plugins/plugins/ui_grid/src/more-columns.tsx new file mode 100644 index 0000000..c3f8958 --- /dev/null +++ b/example_plugins/plugins/ui_grid/src/more-columns.tsx @@ -0,0 +1,29 @@ +import { ReactElement } from "react"; +import { Grid } from "@project-gauntlet/api/components"; + +const items = [ + "🥹", + "🤣", + "🥵", + "🤕", + "🫥", + "🤬", + "🥱", + "🤮", + "🙄", + "🤠" +] + +export default function MoreColumnsExample(): ReactElement { + return ( + + {items.map(value => ( + + + {value} + + + ))} + + ) +} \ No newline at end of file diff --git a/example_plugins/plugins/ui_grid/src/search-bar-set-search-text.tsx b/example_plugins/plugins/ui_grid/src/search-bar-set-search-text.tsx new file mode 100644 index 0000000..ac42644 --- /dev/null +++ b/example_plugins/plugins/ui_grid/src/search-bar-set-search-text.tsx @@ -0,0 +1,28 @@ +import { ReactElement, useState } from "react"; +import { Action, ActionPanel, Grid } from "@project-gauntlet/api/components"; + +export default function SearchBarSetSearchTextExample(): ReactElement { + const [searchText, setSearchText] = useState(""); + + return ( + + setSearchText(id)}/> +
+ } + > + + + + + Click me! + + + + + ) +} diff --git a/example_plugins/plugins/ui_grid/src/section.tsx b/example_plugins/plugins/ui_grid/src/section.tsx index 2ec9c59..9687bdc 100644 --- a/example_plugins/plugins/ui_grid/src/section.tsx +++ b/example_plugins/plugins/ui_grid/src/section.tsx @@ -3,16 +3,8 @@ import { Grid } from "@project-gauntlet/api/components"; const theBlade1 = "https://static.wikia.nocookie.net/starwars/images/a/a4/The-Blade-1-final-cover.jpg/revision/latest/scale-to-width-down/150?cb=20221215195606" const theBlade2 = "https://static.wikia.nocookie.net/starwars/images/f/fd/The-Blade-2-Final-Cover.jpg/revision/latest/scale-to-width-down/150?cb=20230120033002" -const theBlade3 = "https://static.wikia.nocookie.net/starwars/images/0/02/The-Blade-3-Final-Cover.jpg/revision/latest/scale-to-width-down/150?cb=20230227203337" -const theBlade4 = "https://static.wikia.nocookie.net/starwars/images/6/6c/The-Blade-4-Final-Cover.jpg/revision/latest/scale-to-width-down/150?cb=20230321223753" const vader1 = "https://static.wikia.nocookie.net/starwars/images/9/9a/Darth_VaderDark_Lord_of_the_Sith.jpg/revision/latest/scale-to-width-down/150?cb=20190223230434" -const vader2 = "https://static.wikia.nocookie.net/starwars/images/2/2e/Darth_Vader_2_cover_art.jpg/revision/latest/scale-to-width-down/150?cb=20190223234228" -const vader3 = "https://static.wikia.nocookie.net/starwars/images/d/df/DarthVader2017-3.jpg/revision/latest/scale-to-width-down/150?cb=20190224013414" -const vader4 = "https://static.wikia.nocookie.net/starwars/images/c/c9/Darthvader-dlots-4-final.jpg/revision/latest/scale-to-width-down/150?cb=20190226024707" -const vader5 = "https://static.wikia.nocookie.net/starwars/images/a/ab/Darthvader-dlots-5.jpg/revision/latest/scale-to-width-down/150?cb=20170826121053" -const vader6 = "https://static.wikia.nocookie.net/starwars/images/2/20/DarthVader-DLotS--Solicitation.jpg/revision/latest/scale-to-width-down/150?cb=20171001165404" -const vader7 = "https://static.wikia.nocookie.net/starwars/images/f/fa/DarthVader2017-7.jpg/revision/latest/scale-to-width-down/150?cb=20190226233333" export default function SectionExample(): ReactElement { return ( @@ -28,16 +20,6 @@ export default function SectionExample(): ReactElement {
- - - - - - - - - -
@@ -45,36 +27,6 @@ export default function SectionExample(): ReactElement { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) diff --git a/example_plugins/scenarios/ui_grid/content-horizontal-break/default.json b/example_plugins/scenarios/ui_grid/content-horizontal-break/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_grid/content-horizontal-break/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_grid/more-columns/default.json b/example_plugins/scenarios/ui_grid/more-columns/default.json new file mode 100644 index 0000000..904b00b --- /dev/null +++ b/example_plugins/scenarios/ui_grid/more-columns/default.json @@ -0,0 +1,3 @@ +{ + "type": "RequestViewRender" +} \ No newline at end of file From 71284cbca8b285819ada8c1175ea31908aa6501a Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 23 Jan 2025 20:13:51 +0100 Subject: [PATCH 401/540] Add search-bar-set-search-text list example --- .../src/search-bar-set-search-text.tsx | 5 +---- example_plugins/plugins/ui_list/gauntlet.toml | 9 +++++++++ .../src/search-bar-set-search-text.tsx | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 example_plugins/plugins/ui_list/src/search-bar-set-search-text.tsx diff --git a/example_plugins/plugins/ui_grid/src/search-bar-set-search-text.tsx b/example_plugins/plugins/ui_grid/src/search-bar-set-search-text.tsx index ac42644..94853c1 100644 --- a/example_plugins/plugins/ui_grid/src/search-bar-set-search-text.tsx +++ b/example_plugins/plugins/ui_grid/src/search-bar-set-search-text.tsx @@ -12,10 +12,7 @@ export default function SearchBarSetSearchTextExample(): ReactElement { } > - + diff --git a/example_plugins/plugins/ui_list/gauntlet.toml b/example_plugins/plugins/ui_list/gauntlet.toml index 5c61845..9ca4bf4 100644 --- a/example_plugins/plugins/ui_list/gauntlet.toml +++ b/example_plugins/plugins/ui_list/gauntlet.toml @@ -48,6 +48,15 @@ type = 'view' description = '' # docs-code-segment:end +# docs-code-segment:start search-bar-set-search-text +[[entrypoint]] +id = 'search-bar-set-search-text' +name = 'List Search Bar Set Search Text' +path = 'src/search-bar-set-search-text.tsx' +type = 'view' +description = '' +# docs-code-segment:end + # docs-code-segment:start item [[entrypoint]] id = 'item' diff --git a/example_plugins/plugins/ui_list/src/search-bar-set-search-text.tsx b/example_plugins/plugins/ui_list/src/search-bar-set-search-text.tsx new file mode 100644 index 0000000..6670bc7 --- /dev/null +++ b/example_plugins/plugins/ui_list/src/search-bar-set-search-text.tsx @@ -0,0 +1,19 @@ +import { ReactElement, useState } from "react"; +import { Action, ActionPanel, List } from "@project-gauntlet/api/components"; + +export default function SearchBarSetSearchTextExample(): ReactElement { + const [searchText, setSearchText] = useState(""); + + return ( + + setSearchText(id)}/> + + } + > + + + + ) +} From 90957f214abb2890e814cf4dddd24ae25f978208 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 26 Jan 2025 13:06:04 +0100 Subject: [PATCH 402/540] Use pull_request instead of pull_request_target --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e9364a7..d5b20cf 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,7 +2,7 @@ name: build on: push: - pull_request_target: + pull_request: branches: - main From d721bda736a88874643bd2951b69f44571ad95bb Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 28 Jan 2025 21:44:05 +0100 Subject: [PATCH 403/540] Inline view examples --- dev_plugin/src/hooks-view.tsx | 7 ++--- .../plugins/ui_detail/package.json | 2 +- .../plugins/ui_grid/src/content-image.tsx | 11 ++++--- .../.gitignore | 0 .../gauntlet.toml | 0 .../package.json | 2 +- .../plugins/ui_inline_code_block/src/main.tsx | 19 ++++++++++++ .../tsconfig.json | 0 .../plugins/ui_inline_header/.gitignore | 2 ++ .../plugins/ui_inline_header/gauntlet.toml | 16 ++++++++++ .../plugins/ui_inline_header/package.json | 17 ++++++++++ .../plugins/ui_inline_header/src/main.tsx | 19 ++++++++++++ .../plugins/ui_inline_header/tsconfig.json | 11 +++++++ .../ui_inline_horizontal_break/.gitignore | 2 ++ .../ui_inline_horizontal_break/gauntlet.toml | 16 ++++++++++ .../ui_inline_horizontal_break/package.json | 17 ++++++++++ .../ui_inline_horizontal_break/src/main.tsx | 23 ++++++++++++++ .../ui_inline_horizontal_break/tsconfig.json | 11 +++++++ .../plugins/ui_inline_image/.gitignore | 2 ++ .../plugins/ui_inline_image/gauntlet.toml | 16 ++++++++++ .../plugins/ui_inline_image/package.json | 17 ++++++++++ .../plugins/ui_inline_image/src/main.tsx | 17 ++++++++++ .../plugins/ui_inline_image/tsconfig.json | 11 +++++++ .../plugins/ui_inline_main/.gitignore | 2 ++ .../plugins/ui_inline_main/gauntlet.toml | 16 ++++++++++ .../plugins/ui_inline_main/package.json | 17 ++++++++++ .../plugins/ui_inline_main/src/main.tsx | 31 +++++++++++++++++++ .../plugins/ui_inline_main/tsconfig.json | 11 +++++++ .../plugins/ui_inline_paragraph/.gitignore | 2 ++ .../plugins/ui_inline_paragraph/gauntlet.toml | 16 ++++++++++ .../plugins/ui_inline_paragraph/package.json | 17 ++++++++++ .../plugins/ui_inline_paragraph/src/main.tsx | 19 ++++++++++++ .../plugins/ui_inline_paragraph/tsconfig.json | 11 +++++++ .../plugins/ui_inline_separator/.gitignore | 2 ++ .../plugins/ui_inline_separator/gauntlet.toml | 16 ++++++++++ .../plugins/ui_inline_separator/package.json | 17 ++++++++++ .../src/main.tsx | 7 ++++- .../plugins/ui_inline_separator/tsconfig.json | 11 +++++++ .../ui_inline_three_sections/src/main.tsx | 9 ++++-- .../main/default.json} | 2 +- .../ui_inline_header/main/default.json | 4 +++ .../main/default.json | 4 +++ .../ui_inline_image/main/default.json | 4 +++ .../ui_inline_main/main/default.json | 4 +++ .../ui_inline_paragraph/main/default.json | 4 +++ .../ui_inline_separator/main/default.json | 4 +++ .../main/default.json | 4 +++ .../main/three-sections.json | 4 --- .../ui_inline_two_sections/main/default.json | 4 +++ .../main/two-sections.json | 4 --- 50 files changed, 459 insertions(+), 25 deletions(-) rename example_plugins/plugins/{ui_inline_separators => ui_inline_code_block}/.gitignore (100%) rename example_plugins/plugins/{ui_inline_separators => ui_inline_code_block}/gauntlet.toml (100%) rename example_plugins/plugins/{ui_inline_separators => ui_inline_code_block}/package.json (85%) create mode 100644 example_plugins/plugins/ui_inline_code_block/src/main.tsx rename example_plugins/plugins/{ui_inline_separators => ui_inline_code_block}/tsconfig.json (100%) create mode 100644 example_plugins/plugins/ui_inline_header/.gitignore create mode 100644 example_plugins/plugins/ui_inline_header/gauntlet.toml create mode 100644 example_plugins/plugins/ui_inline_header/package.json create mode 100644 example_plugins/plugins/ui_inline_header/src/main.tsx create mode 100644 example_plugins/plugins/ui_inline_header/tsconfig.json create mode 100644 example_plugins/plugins/ui_inline_horizontal_break/.gitignore create mode 100644 example_plugins/plugins/ui_inline_horizontal_break/gauntlet.toml create mode 100644 example_plugins/plugins/ui_inline_horizontal_break/package.json create mode 100644 example_plugins/plugins/ui_inline_horizontal_break/src/main.tsx create mode 100644 example_plugins/plugins/ui_inline_horizontal_break/tsconfig.json create mode 100644 example_plugins/plugins/ui_inline_image/.gitignore create mode 100644 example_plugins/plugins/ui_inline_image/gauntlet.toml create mode 100644 example_plugins/plugins/ui_inline_image/package.json create mode 100644 example_plugins/plugins/ui_inline_image/src/main.tsx create mode 100644 example_plugins/plugins/ui_inline_image/tsconfig.json create mode 100644 example_plugins/plugins/ui_inline_main/.gitignore create mode 100644 example_plugins/plugins/ui_inline_main/gauntlet.toml create mode 100644 example_plugins/plugins/ui_inline_main/package.json create mode 100644 example_plugins/plugins/ui_inline_main/src/main.tsx create mode 100644 example_plugins/plugins/ui_inline_main/tsconfig.json create mode 100644 example_plugins/plugins/ui_inline_paragraph/.gitignore create mode 100644 example_plugins/plugins/ui_inline_paragraph/gauntlet.toml create mode 100644 example_plugins/plugins/ui_inline_paragraph/package.json create mode 100644 example_plugins/plugins/ui_inline_paragraph/src/main.tsx create mode 100644 example_plugins/plugins/ui_inline_paragraph/tsconfig.json create mode 100644 example_plugins/plugins/ui_inline_separator/.gitignore create mode 100644 example_plugins/plugins/ui_inline_separator/gauntlet.toml create mode 100644 example_plugins/plugins/ui_inline_separator/package.json rename example_plugins/plugins/{ui_inline_separators => ui_inline_separator}/src/main.tsx (89%) create mode 100644 example_plugins/plugins/ui_inline_separator/tsconfig.json rename example_plugins/scenarios/{ui_inline_separators/main/separator.json => ui_inline_code_block/main/default.json} (51%) create mode 100644 example_plugins/scenarios/ui_inline_header/main/default.json create mode 100644 example_plugins/scenarios/ui_inline_horizontal_break/main/default.json create mode 100644 example_plugins/scenarios/ui_inline_image/main/default.json create mode 100644 example_plugins/scenarios/ui_inline_main/main/default.json create mode 100644 example_plugins/scenarios/ui_inline_paragraph/main/default.json create mode 100644 example_plugins/scenarios/ui_inline_separator/main/default.json create mode 100644 example_plugins/scenarios/ui_inline_three_sections/main/default.json delete mode 100644 example_plugins/scenarios/ui_inline_three_sections/main/three-sections.json create mode 100644 example_plugins/scenarios/ui_inline_two_sections/main/default.json delete mode 100644 example_plugins/scenarios/ui_inline_two_sections/main/two-sections.json diff --git a/dev_plugin/src/hooks-view.tsx b/dev_plugin/src/hooks-view.tsx index 9596f9e..3112ac1 100644 --- a/dev_plugin/src/hooks-view.tsx +++ b/dev_plugin/src/hooks-view.tsx @@ -1,20 +1,17 @@ import { Action, ActionPanel, Element, Icons, List } from "@project-gauntlet/api/components"; -import React, { ReactElement, ReactNode, useRef, useState } from "react"; +import React, { ReactElement, ReactNode, useRef } from "react"; import { useCachedPromise, useFetch, useNavigation, usePromise } from "@project-gauntlet/api/hooks"; export default function ListView(): ReactElement { const { pushView } = useNavigation(); - const [id, setId] = useState(undefined); - return ( - pushPrimaryAction(id, pushView)}/> + pushPrimaryAction(id, pushView)}/> } - onItemFocusChange={itemId => setId(itemId)} > diff --git a/example_plugins/plugins/ui_detail/package.json b/example_plugins/plugins/ui_detail/package.json index 694c863..2953d0c 100644 --- a/example_plugins/plugins/ui_detail/package.json +++ b/example_plugins/plugins/ui_detail/package.json @@ -1,5 +1,5 @@ { - "name": "@project-gauntlet/docs-detailt", + "name": "@project-gauntlet/docs-detail", "private": true, "scripts": { "build": "gauntlet build", diff --git a/example_plugins/plugins/ui_grid/src/content-image.tsx b/example_plugins/plugins/ui_grid/src/content-image.tsx index b07b68a..2e14906 100644 --- a/example_plugins/plugins/ui_grid/src/content-image.tsx +++ b/example_plugins/plugins/ui_grid/src/content-image.tsx @@ -4,11 +4,12 @@ import { Grid } from "@project-gauntlet/api/components"; export default function ContentImageExample(): ReactElement { return ( - - - - - + + + + + ) } \ No newline at end of file diff --git a/example_plugins/plugins/ui_inline_separators/.gitignore b/example_plugins/plugins/ui_inline_code_block/.gitignore similarity index 100% rename from example_plugins/plugins/ui_inline_separators/.gitignore rename to example_plugins/plugins/ui_inline_code_block/.gitignore diff --git a/example_plugins/plugins/ui_inline_separators/gauntlet.toml b/example_plugins/plugins/ui_inline_code_block/gauntlet.toml similarity index 100% rename from example_plugins/plugins/ui_inline_separators/gauntlet.toml rename to example_plugins/plugins/ui_inline_code_block/gauntlet.toml diff --git a/example_plugins/plugins/ui_inline_separators/package.json b/example_plugins/plugins/ui_inline_code_block/package.json similarity index 85% rename from example_plugins/plugins/ui_inline_separators/package.json rename to example_plugins/plugins/ui_inline_code_block/package.json index 7049d90..e485da9 100644 --- a/example_plugins/plugins/ui_inline_separators/package.json +++ b/example_plugins/plugins/ui_inline_code_block/package.json @@ -1,5 +1,5 @@ { - "name": "@project-gauntlet/docs-inline-separators", + "name": "@project-gauntlet/docs-inline-code-block", "private": true, "scripts": { "build": "gauntlet build", diff --git a/example_plugins/plugins/ui_inline_code_block/src/main.tsx b/example_plugins/plugins/ui_inline_code_block/src/main.tsx new file mode 100644 index 0000000..456298c --- /dev/null +++ b/example_plugins/plugins/ui_inline_code_block/src/main.tsx @@ -0,0 +1,19 @@ +import { ReactElement } from "react"; +import { Inline } from "@project-gauntlet/api/components"; + +export default function Main({ text }: { text: string }): ReactElement | null { + + if (text != "example") { + return null + } + + return ( + + + + {"() => void"} + + + + ) +} diff --git a/example_plugins/plugins/ui_inline_separators/tsconfig.json b/example_plugins/plugins/ui_inline_code_block/tsconfig.json similarity index 100% rename from example_plugins/plugins/ui_inline_separators/tsconfig.json rename to example_plugins/plugins/ui_inline_code_block/tsconfig.json diff --git a/example_plugins/plugins/ui_inline_header/.gitignore b/example_plugins/plugins/ui_inline_header/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/ui_inline_header/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/ui_inline_header/gauntlet.toml b/example_plugins/plugins/ui_inline_header/gauntlet.toml new file mode 100644 index 0000000..2521a6c --- /dev/null +++ b/example_plugins/plugins/ui_inline_header/gauntlet.toml @@ -0,0 +1,16 @@ +[gauntlet] +name = 'Docs Inline' +description = '' + +# docs-code-segment:start main +[[entrypoint]] +id = 'main' +name = 'Main' +path = 'src/main.tsx' +type = 'inline-view' +description = '' +# docs-code-segment:end + +[permissions] +main_search_bar = ["read"] + diff --git a/example_plugins/plugins/ui_inline_header/package.json b/example_plugins/plugins/ui_inline_header/package.json new file mode 100644 index 0000000..e338daa --- /dev/null +++ b/example_plugins/plugins/ui_inline_header/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-inline-header", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/ui_inline_header/src/main.tsx b/example_plugins/plugins/ui_inline_header/src/main.tsx new file mode 100644 index 0000000..8a20b04 --- /dev/null +++ b/example_plugins/plugins/ui_inline_header/src/main.tsx @@ -0,0 +1,19 @@ +import { ReactElement } from "react"; +import { Inline } from "@project-gauntlet/api/components"; + +export default function Main({ text }: { text: string }): ReactElement | null { + + if (text != "example") { + return null + } + + return ( + + + + Header Text + + + + ) +} diff --git a/example_plugins/plugins/ui_inline_header/tsconfig.json b/example_plugins/plugins/ui_inline_header/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/ui_inline_header/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/ui_inline_horizontal_break/.gitignore b/example_plugins/plugins/ui_inline_horizontal_break/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/ui_inline_horizontal_break/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/ui_inline_horizontal_break/gauntlet.toml b/example_plugins/plugins/ui_inline_horizontal_break/gauntlet.toml new file mode 100644 index 0000000..2521a6c --- /dev/null +++ b/example_plugins/plugins/ui_inline_horizontal_break/gauntlet.toml @@ -0,0 +1,16 @@ +[gauntlet] +name = 'Docs Inline' +description = '' + +# docs-code-segment:start main +[[entrypoint]] +id = 'main' +name = 'Main' +path = 'src/main.tsx' +type = 'inline-view' +description = '' +# docs-code-segment:end + +[permissions] +main_search_bar = ["read"] + diff --git a/example_plugins/plugins/ui_inline_horizontal_break/package.json b/example_plugins/plugins/ui_inline_horizontal_break/package.json new file mode 100644 index 0000000..3d6e2e1 --- /dev/null +++ b/example_plugins/plugins/ui_inline_horizontal_break/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-inline-horizontal-break", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/ui_inline_horizontal_break/src/main.tsx b/example_plugins/plugins/ui_inline_horizontal_break/src/main.tsx new file mode 100644 index 0000000..13258db --- /dev/null +++ b/example_plugins/plugins/ui_inline_horizontal_break/src/main.tsx @@ -0,0 +1,23 @@ +import { ReactElement } from "react"; +import { Inline } from "@project-gauntlet/api/components"; + +export default function Main({ text }: { text: string }): ReactElement | null { + + if (text != "example") { + return null + } + + return ( + + + + Above + + + + Below + + + + ) +} diff --git a/example_plugins/plugins/ui_inline_horizontal_break/tsconfig.json b/example_plugins/plugins/ui_inline_horizontal_break/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/ui_inline_horizontal_break/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/ui_inline_image/.gitignore b/example_plugins/plugins/ui_inline_image/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/ui_inline_image/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/ui_inline_image/gauntlet.toml b/example_plugins/plugins/ui_inline_image/gauntlet.toml new file mode 100644 index 0000000..2521a6c --- /dev/null +++ b/example_plugins/plugins/ui_inline_image/gauntlet.toml @@ -0,0 +1,16 @@ +[gauntlet] +name = 'Docs Inline' +description = '' + +# docs-code-segment:start main +[[entrypoint]] +id = 'main' +name = 'Main' +path = 'src/main.tsx' +type = 'inline-view' +description = '' +# docs-code-segment:end + +[permissions] +main_search_bar = ["read"] + diff --git a/example_plugins/plugins/ui_inline_image/package.json b/example_plugins/plugins/ui_inline_image/package.json new file mode 100644 index 0000000..62e21b9 --- /dev/null +++ b/example_plugins/plugins/ui_inline_image/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-inline-image", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/ui_inline_image/src/main.tsx b/example_plugins/plugins/ui_inline_image/src/main.tsx new file mode 100644 index 0000000..ae29c63 --- /dev/null +++ b/example_plugins/plugins/ui_inline_image/src/main.tsx @@ -0,0 +1,17 @@ +import { ReactElement } from "react"; +import { Icons, Inline } from "@project-gauntlet/api/components"; + +export default function Main({ text }: { text: string }): ReactElement | null { + + if (text != "example") { + return null + } + + return ( + + + + + + ) +} diff --git a/example_plugins/plugins/ui_inline_image/tsconfig.json b/example_plugins/plugins/ui_inline_image/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/ui_inline_image/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/ui_inline_main/.gitignore b/example_plugins/plugins/ui_inline_main/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/ui_inline_main/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/ui_inline_main/gauntlet.toml b/example_plugins/plugins/ui_inline_main/gauntlet.toml new file mode 100644 index 0000000..2521a6c --- /dev/null +++ b/example_plugins/plugins/ui_inline_main/gauntlet.toml @@ -0,0 +1,16 @@ +[gauntlet] +name = 'Docs Inline' +description = '' + +# docs-code-segment:start main +[[entrypoint]] +id = 'main' +name = 'Main' +path = 'src/main.tsx' +type = 'inline-view' +description = '' +# docs-code-segment:end + +[permissions] +main_search_bar = ["read"] + diff --git a/example_plugins/plugins/ui_inline_main/package.json b/example_plugins/plugins/ui_inline_main/package.json new file mode 100644 index 0000000..c8eacb7 --- /dev/null +++ b/example_plugins/plugins/ui_inline_main/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-inline-main", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/ui_inline_main/src/main.tsx b/example_plugins/plugins/ui_inline_main/src/main.tsx new file mode 100644 index 0000000..2901b87 --- /dev/null +++ b/example_plugins/plugins/ui_inline_main/src/main.tsx @@ -0,0 +1,31 @@ +import { ReactElement } from "react"; +import { Inline, Icons, ActionPanel, Action } from "@project-gauntlet/api/components"; + +export default function Main({ text }: { text: string }): ReactElement | null { + + if (text != "1 + 2") { + return null + } + + return ( + + {/* */}}/> + + } + > + + + 1 + 2 + + + + + + 3 + + + + ) +} diff --git a/example_plugins/plugins/ui_inline_main/tsconfig.json b/example_plugins/plugins/ui_inline_main/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/ui_inline_main/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/ui_inline_paragraph/.gitignore b/example_plugins/plugins/ui_inline_paragraph/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/ui_inline_paragraph/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/ui_inline_paragraph/gauntlet.toml b/example_plugins/plugins/ui_inline_paragraph/gauntlet.toml new file mode 100644 index 0000000..2521a6c --- /dev/null +++ b/example_plugins/plugins/ui_inline_paragraph/gauntlet.toml @@ -0,0 +1,16 @@ +[gauntlet] +name = 'Docs Inline' +description = '' + +# docs-code-segment:start main +[[entrypoint]] +id = 'main' +name = 'Main' +path = 'src/main.tsx' +type = 'inline-view' +description = '' +# docs-code-segment:end + +[permissions] +main_search_bar = ["read"] + diff --git a/example_plugins/plugins/ui_inline_paragraph/package.json b/example_plugins/plugins/ui_inline_paragraph/package.json new file mode 100644 index 0000000..c4aed9e --- /dev/null +++ b/example_plugins/plugins/ui_inline_paragraph/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-inline-paragraph", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/ui_inline_paragraph/src/main.tsx b/example_plugins/plugins/ui_inline_paragraph/src/main.tsx new file mode 100644 index 0000000..72da13e --- /dev/null +++ b/example_plugins/plugins/ui_inline_paragraph/src/main.tsx @@ -0,0 +1,19 @@ +import { ReactElement } from "react"; +import { Inline } from "@project-gauntlet/api/components"; + +export default function Main({ text }: { text: string }): ReactElement | null { + + if (text != "example") { + return null + } + + return ( + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + + + + ) +} diff --git a/example_plugins/plugins/ui_inline_paragraph/tsconfig.json b/example_plugins/plugins/ui_inline_paragraph/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/ui_inline_paragraph/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/ui_inline_separator/.gitignore b/example_plugins/plugins/ui_inline_separator/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/ui_inline_separator/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/ui_inline_separator/gauntlet.toml b/example_plugins/plugins/ui_inline_separator/gauntlet.toml new file mode 100644 index 0000000..2521a6c --- /dev/null +++ b/example_plugins/plugins/ui_inline_separator/gauntlet.toml @@ -0,0 +1,16 @@ +[gauntlet] +name = 'Docs Inline' +description = '' + +# docs-code-segment:start main +[[entrypoint]] +id = 'main' +name = 'Main' +path = 'src/main.tsx' +type = 'inline-view' +description = '' +# docs-code-segment:end + +[permissions] +main_search_bar = ["read"] + diff --git a/example_plugins/plugins/ui_inline_separator/package.json b/example_plugins/plugins/ui_inline_separator/package.json new file mode 100644 index 0000000..555ed2d --- /dev/null +++ b/example_plugins/plugins/ui_inline_separator/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-inline-separator", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/ui_inline_separators/src/main.tsx b/example_plugins/plugins/ui_inline_separator/src/main.tsx similarity index 89% rename from example_plugins/plugins/ui_inline_separators/src/main.tsx rename to example_plugins/plugins/ui_inline_separator/src/main.tsx index c2b4bb9..7570e4a 100644 --- a/example_plugins/plugins/ui_inline_separators/src/main.tsx +++ b/example_plugins/plugins/ui_inline_separator/src/main.tsx @@ -1,7 +1,12 @@ import { ReactElement } from "react"; import { Inline, Icons } from "@project-gauntlet/api/components"; -export default function Main({ text }: { text: string }): ReactElement { +export default function Main({ text }: { text: string }): ReactElement | null { + + if (text != "example") { + return null + } + return ( diff --git a/example_plugins/plugins/ui_inline_separator/tsconfig.json b/example_plugins/plugins/ui_inline_separator/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/ui_inline_separator/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/ui_inline_three_sections/src/main.tsx b/example_plugins/plugins/ui_inline_three_sections/src/main.tsx index a0ff3fa..71d7bc3 100644 --- a/example_plugins/plugins/ui_inline_three_sections/src/main.tsx +++ b/example_plugins/plugins/ui_inline_three_sections/src/main.tsx @@ -1,8 +1,11 @@ import { ReactElement } from "react"; import { Inline } from "@project-gauntlet/api/components"; -export default function Main(props: { text: string }): ReactElement { - const type = props.text; +export default function Main({ text }: { text: string }): ReactElement | null { + + if (text != "example") { + return null + } return ( @@ -13,7 +16,7 @@ export default function Main(props: { text: string }): ReactElement { - Center: {type} + Center diff --git a/example_plugins/scenarios/ui_inline_separators/main/separator.json b/example_plugins/scenarios/ui_inline_code_block/main/default.json similarity index 51% rename from example_plugins/scenarios/ui_inline_separators/main/separator.json rename to example_plugins/scenarios/ui_inline_code_block/main/default.json index 056de6b..d880710 100644 --- a/example_plugins/scenarios/ui_inline_separators/main/separator.json +++ b/example_plugins/scenarios/ui_inline_code_block/main/default.json @@ -1,4 +1,4 @@ { "type": "Search", - "text": "separator" + "text": "example" } \ No newline at end of file diff --git a/example_plugins/scenarios/ui_inline_header/main/default.json b/example_plugins/scenarios/ui_inline_header/main/default.json new file mode 100644 index 0000000..d880710 --- /dev/null +++ b/example_plugins/scenarios/ui_inline_header/main/default.json @@ -0,0 +1,4 @@ +{ + "type": "Search", + "text": "example" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_inline_horizontal_break/main/default.json b/example_plugins/scenarios/ui_inline_horizontal_break/main/default.json new file mode 100644 index 0000000..d880710 --- /dev/null +++ b/example_plugins/scenarios/ui_inline_horizontal_break/main/default.json @@ -0,0 +1,4 @@ +{ + "type": "Search", + "text": "example" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_inline_image/main/default.json b/example_plugins/scenarios/ui_inline_image/main/default.json new file mode 100644 index 0000000..d880710 --- /dev/null +++ b/example_plugins/scenarios/ui_inline_image/main/default.json @@ -0,0 +1,4 @@ +{ + "type": "Search", + "text": "example" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_inline_main/main/default.json b/example_plugins/scenarios/ui_inline_main/main/default.json new file mode 100644 index 0000000..b9bad38 --- /dev/null +++ b/example_plugins/scenarios/ui_inline_main/main/default.json @@ -0,0 +1,4 @@ +{ + "type": "Search", + "text": "1 + 2" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_inline_paragraph/main/default.json b/example_plugins/scenarios/ui_inline_paragraph/main/default.json new file mode 100644 index 0000000..d880710 --- /dev/null +++ b/example_plugins/scenarios/ui_inline_paragraph/main/default.json @@ -0,0 +1,4 @@ +{ + "type": "Search", + "text": "example" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_inline_separator/main/default.json b/example_plugins/scenarios/ui_inline_separator/main/default.json new file mode 100644 index 0000000..d880710 --- /dev/null +++ b/example_plugins/scenarios/ui_inline_separator/main/default.json @@ -0,0 +1,4 @@ +{ + "type": "Search", + "text": "example" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_inline_three_sections/main/default.json b/example_plugins/scenarios/ui_inline_three_sections/main/default.json new file mode 100644 index 0000000..d880710 --- /dev/null +++ b/example_plugins/scenarios/ui_inline_three_sections/main/default.json @@ -0,0 +1,4 @@ +{ + "type": "Search", + "text": "example" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_inline_three_sections/main/three-sections.json b/example_plugins/scenarios/ui_inline_three_sections/main/three-sections.json deleted file mode 100644 index a01b23a..0000000 --- a/example_plugins/scenarios/ui_inline_three_sections/main/three-sections.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Search", - "text": "three-sections" -} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_inline_two_sections/main/default.json b/example_plugins/scenarios/ui_inline_two_sections/main/default.json new file mode 100644 index 0000000..d880710 --- /dev/null +++ b/example_plugins/scenarios/ui_inline_two_sections/main/default.json @@ -0,0 +1,4 @@ +{ + "type": "Search", + "text": "example" +} \ No newline at end of file diff --git a/example_plugins/scenarios/ui_inline_two_sections/main/two-sections.json b/example_plugins/scenarios/ui_inline_two_sections/main/two-sections.json deleted file mode 100644 index 38eaee1..0000000 --- a/example_plugins/scenarios/ui_inline_two_sections/main/two-sections.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Search", - "text": "two-sections" -} \ No newline at end of file From 709f8337d4a115e21761a9151c94484c97c70475 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Mar 2025 20:05:17 +0100 Subject: [PATCH 404/540] Fix scenario runner --- rust/client/src/ui/mod.rs | 45 ++++++++++------------- rust/scenario_runner/src/frontend_mock.rs | 8 +++- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 056acae..9e66585 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -883,10 +883,10 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { AppMsg::PromptChanged(mut new_prompt) => { match &mut state.global_state { GlobalState::MainView { - focused_search_result, - sub_state, - .. - } => { + focused_search_result, + sub_state, + .. + } => { new_prompt.truncate(100); // search query uses regex so just to be safe truncate the prompt state.prompt = new_prompt.clone(); @@ -897,8 +897,8 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } GlobalState::ErrorView { .. } => {} GlobalState::PluginView { .. } => {} - GlobalState::PendingPluginView { .. } => {} - } + GlobalState::PendingPluginView { .. } => {} + } state.search(new_prompt, true) } @@ -1242,30 +1242,23 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { AppMsg::ScreenshotDone { save_path, screenshot } => { println!("Saving screenshot at: {}", save_path); - Task::perform( - async move { - tokio::task::spawn_blocking(move || { - let save_dir = Path::new(&save_path); + let save_dir = Path::new(&save_path); - let save_parent_dir = save_dir.parent().expect("save_path has no parent"); + let save_parent_dir = save_dir.parent().expect("save_path has no parent"); - fs::create_dir_all(save_parent_dir).expect("unable to create save_parent_dir"); + fs::create_dir_all(save_parent_dir).expect("unable to create save_parent_dir"); - image::save_buffer_with_format( - &save_path, - &screenshot.bytes, - screenshot.size.width, - screenshot.size.height, - image::ColorType::Rgba8, - image::ImageFormat::Png, - ) - }) - .await - .expect("Unable to save screenshot") - }, - |_| (), + image::save_buffer_with_format( + &save_path, + &screenshot.bytes, + screenshot.size.width, + screenshot.size.height, + image::ColorType::Rgba8, + image::ImageFormat::Png, ) - .then(|_| iced::exit()) + .expect("Unable to save screenshot"); + + std::process::exit(0); } AppMsg::ToggleActionPanel { keyboard } => { match &mut state.global_state { diff --git a/rust/scenario_runner/src/frontend_mock.rs b/rust/scenario_runner/src/frontend_mock.rs index 29431e1..e3d2d19 100644 --- a/rust/scenario_runner/src/frontend_mock.rs +++ b/rust/scenario_runner/src/frontend_mock.rs @@ -157,10 +157,14 @@ async fn request_loop( | UiRequestData::ShowWindow | UiRequestData::HideWindow | UiRequestData::ClearInlineView { .. } - | UiRequestData::SetTheme { .. } => { + | UiRequestData::SetTheme { .. } + | UiRequestData::ShowPluginView { .. } + | UiRequestData::ShowGeneratedPluginView { .. } => { unreachable!() } - UiRequestData::SetGlobalShortcut { .. } | UiRequestData::SetWindowPositionMode { .. } | UiRequestData::RequestSearchResultUpdate => { + UiRequestData::SetGlobalShortcut { .. } + | UiRequestData::SetWindowPositionMode { .. } + | UiRequestData::RequestSearchResultUpdate => { // noop } UiRequestData::ReplaceView { From 99f7ecec116cdccb28c2acb6c266573e266579b2 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Mar 2025 20:16:19 +0100 Subject: [PATCH 405/540] Fix formatting --- rust/scenario_runner/src/lib.rs | 9 ++++----- rust/server/src/lib.rs | 6 +++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/rust/scenario_runner/src/lib.rs b/rust/scenario_runner/src/lib.rs index 854a703..7df44f0 100644 --- a/rust/scenario_runner/src/lib.rs +++ b/rust/scenario_runner/src/lib.rs @@ -1,10 +1,10 @@ use gauntlet_common::model::BackendRequestData; -use gauntlet_common::model::UiSetupData; -use gauntlet_common::model::UiTheme; -use gauntlet_common::model::WindowPositionMode; use gauntlet_common::model::BackendResponseData; use gauntlet_common::model::UiRequestData; use gauntlet_common::model::UiResponseData; +use gauntlet_common::model::UiSetupData; +use gauntlet_common::model::UiTheme; +use gauntlet_common::model::WindowPositionMode; use gauntlet_utils::channel::RequestReceiver; use gauntlet_utils::channel::RequestSender; @@ -23,9 +23,8 @@ pub async fn run_scenario_runner_frontend_mock( pub async fn run_scenario_runner_mock_server( _request_sender: RequestSender, mut backend_receiver: RequestReceiver, - theme: UiTheme + theme: UiTheme, ) -> anyhow::Result<()> { - let (_data, responder) = backend_receiver.recv().await; responder.respond(BackendResponseData::SetupData { data: UiSetupData { diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index 4c8b800..842e724 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -173,7 +173,11 @@ fn start_server( } #[cfg(feature = "scenario_runner")] -fn start_mock_server(request_sender: RequestSender, backend_receiver: RequestReceiver, theme: UiTheme) { +fn start_mock_server( + request_sender: RequestSender, + backend_receiver: RequestReceiver, + theme: UiTheme, +) { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() From c39ac08178fc66bd605dbf5445605df3dfa2e45f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sat, 15 Mar 2025 20:22:34 +0100 Subject: [PATCH 406/540] Update package-lock.json --- package-lock.json | 257 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 234 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index be00d1a..3575bb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "example_plugins/js/api": {}, "example_plugins/plugins/docs_detail": { "name": "@project-gauntlet/docs-detailt", + "extraneous": true, "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -61,11 +62,101 @@ "typescript": "*" } }, - "example_plugins/plugins/docs_detail/node_modules/@project-gauntlet/api": { + "example_plugins/plugins/docs_form": { + "name": "@project-gauntlet/docs-form", + "extraneous": true, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/docs_grid": { + "name": "@project-gauntlet/docs-grid", + "extraneous": true, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/docs_inline_separators": { + "name": "@project-gauntlet/docs-inline-separators", + "extraneous": true, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/docs_inline_three_sections": { + "name": "@project-gauntlet/docs-inline-three-sections", + "extraneous": true, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/docs_inline_two_sections": { + "name": "@project-gauntlet/docs-inline-two-sections", + "extraneous": true, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/docs_list": { + "name": "@project-gauntlet/docs-list", + "extraneous": true, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/ui_detail": { + "name": "@project-gauntlet/docs-detail", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/ui_detail/node_modules/@project-gauntlet/api": { "resolved": "example_plugins/js/api", "link": true }, - "example_plugins/plugins/docs_form": { + "example_plugins/plugins/ui_form": { "name": "@project-gauntlet/docs-form", "dependencies": { "@project-gauntlet/api": "file:../../js/api" @@ -77,11 +168,11 @@ "typescript": "*" } }, - "example_plugins/plugins/docs_form/node_modules/@project-gauntlet/api": { + "example_plugins/plugins/ui_form/node_modules/@project-gauntlet/api": { "resolved": "example_plugins/js/api", "link": true }, - "example_plugins/plugins/docs_grid": { + "example_plugins/plugins/ui_grid": { "name": "@project-gauntlet/docs-grid", "dependencies": { "@project-gauntlet/api": "file:../../js/api" @@ -93,12 +184,12 @@ "typescript": "*" } }, - "example_plugins/plugins/docs_grid/node_modules/@project-gauntlet/api": { + "example_plugins/plugins/ui_grid/node_modules/@project-gauntlet/api": { "resolved": "example_plugins/js/api", "link": true }, - "example_plugins/plugins/docs_inline_separators": { - "name": "@project-gauntlet/docs-inline-separators", + "example_plugins/plugins/ui_inline_code_block": { + "name": "@project-gauntlet/docs-inline-code-block", "dependencies": { "@project-gauntlet/api": "file:../../js/api" }, @@ -109,11 +200,107 @@ "typescript": "*" } }, - "example_plugins/plugins/docs_inline_separators/node_modules/@project-gauntlet/api": { + "example_plugins/plugins/ui_inline_code_block/node_modules/@project-gauntlet/api": { "resolved": "example_plugins/js/api", "link": true }, - "example_plugins/plugins/docs_inline_three_sections": { + "example_plugins/plugins/ui_inline_header": { + "name": "@project-gauntlet/docs-inline-header", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/ui_inline_header/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/ui_inline_horizontal_break": { + "name": "@project-gauntlet/docs-inline-horizontal-break", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/ui_inline_horizontal_break/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/ui_inline_image": { + "name": "@project-gauntlet/docs-inline-image", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/ui_inline_image/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/ui_inline_main": { + "name": "@project-gauntlet/docs-inline-main", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/ui_inline_main/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/ui_inline_paragraph": { + "name": "@project-gauntlet/docs-inline-paragraph", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/ui_inline_paragraph/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/ui_inline_separator": { + "name": "@project-gauntlet/docs-inline-separator", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/ui_inline_separator/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/ui_inline_three_sections": { "name": "@project-gauntlet/docs-inline-three-sections", "dependencies": { "@project-gauntlet/api": "file:../../js/api" @@ -125,11 +312,11 @@ "typescript": "*" } }, - "example_plugins/plugins/docs_inline_three_sections/node_modules/@project-gauntlet/api": { + "example_plugins/plugins/ui_inline_three_sections/node_modules/@project-gauntlet/api": { "resolved": "example_plugins/js/api", "link": true }, - "example_plugins/plugins/docs_inline_two_sections": { + "example_plugins/plugins/ui_inline_two_sections": { "name": "@project-gauntlet/docs-inline-two-sections", "dependencies": { "@project-gauntlet/api": "file:../../js/api" @@ -141,11 +328,11 @@ "typescript": "*" } }, - "example_plugins/plugins/docs_inline_two_sections/node_modules/@project-gauntlet/api": { + "example_plugins/plugins/ui_inline_two_sections/node_modules/@project-gauntlet/api": { "resolved": "example_plugins/js/api", "link": true }, - "example_plugins/plugins/docs_list": { + "example_plugins/plugins/ui_list": { "name": "@project-gauntlet/docs-list", "dependencies": { "@project-gauntlet/api": "file:../../js/api" @@ -157,7 +344,7 @@ "typescript": "*" } }, - "example_plugins/plugins/docs_list/node_modules/@project-gauntlet/api": { + "example_plugins/plugins/ui_list/node_modules/@project-gauntlet/api": { "resolved": "example_plugins/js/api", "link": true }, @@ -787,32 +974,56 @@ "resolved": "dev_plugin", "link": true }, - "node_modules/@project-gauntlet/docs-detailt": { - "resolved": "example_plugins/plugins/docs_detail", + "node_modules/@project-gauntlet/docs-detail": { + "resolved": "example_plugins/plugins/ui_detail", "link": true }, "node_modules/@project-gauntlet/docs-form": { - "resolved": "example_plugins/plugins/docs_form", + "resolved": "example_plugins/plugins/ui_form", "link": true }, "node_modules/@project-gauntlet/docs-grid": { - "resolved": "example_plugins/plugins/docs_grid", + "resolved": "example_plugins/plugins/ui_grid", "link": true }, - "node_modules/@project-gauntlet/docs-inline-separators": { - "resolved": "example_plugins/plugins/docs_inline_separators", + "node_modules/@project-gauntlet/docs-inline-code-block": { + "resolved": "example_plugins/plugins/ui_inline_code_block", + "link": true + }, + "node_modules/@project-gauntlet/docs-inline-header": { + "resolved": "example_plugins/plugins/ui_inline_header", + "link": true + }, + "node_modules/@project-gauntlet/docs-inline-horizontal-break": { + "resolved": "example_plugins/plugins/ui_inline_horizontal_break", + "link": true + }, + "node_modules/@project-gauntlet/docs-inline-image": { + "resolved": "example_plugins/plugins/ui_inline_image", + "link": true + }, + "node_modules/@project-gauntlet/docs-inline-main": { + "resolved": "example_plugins/plugins/ui_inline_main", + "link": true + }, + "node_modules/@project-gauntlet/docs-inline-paragraph": { + "resolved": "example_plugins/plugins/ui_inline_paragraph", + "link": true + }, + "node_modules/@project-gauntlet/docs-inline-separator": { + "resolved": "example_plugins/plugins/ui_inline_separator", "link": true }, "node_modules/@project-gauntlet/docs-inline-three-sections": { - "resolved": "example_plugins/plugins/docs_inline_three_sections", + "resolved": "example_plugins/plugins/ui_inline_three_sections", "link": true }, "node_modules/@project-gauntlet/docs-inline-two-sections": { - "resolved": "example_plugins/plugins/docs_inline_two_sections", + "resolved": "example_plugins/plugins/ui_inline_two_sections", "link": true }, "node_modules/@project-gauntlet/docs-list": { - "resolved": "example_plugins/plugins/docs_list", + "resolved": "example_plugins/plugins/ui_list", "link": true }, "node_modules/@project-gauntlet/react": { From 6d37684fc894207dd273538590c5d0cfe2f2b1b3 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 16 Mar 2025 11:05:37 +0100 Subject: [PATCH 407/540] Tweak descriptions of fields in plugin manifest schema --- docs/schema/plugin_manifest.schema.json | 147 +++++++++++++-------- rust/manifest_schema/src/main.rs | 10 +- rust/server/src/plugins/plugin_manifest.rs | 144 ++++++++++++-------- 3 files changed, 193 insertions(+), 108 deletions(-) diff --git a/docs/schema/plugin_manifest.schema.json b/docs/schema/plugin_manifest.schema.json index a6cacdb..11f9fbd 100644 --- a/docs/schema/plugin_manifest.schema.json +++ b/docs/schema/plugin_manifest.schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "PluginManifest", - "description": "Manifest structure for a plugin.", + "description": "Plugin Manifest definition", "type": "object", "required": [ "entrypoint", @@ -15,14 +15,14 @@ ] }, "entrypoint": { - "description": "Entrypoints for the plugin.", + "description": "Plugin entrypoints, all plugin will have at least one entrypoint", "type": "array", "items": { "$ref": "#/definitions/PluginManifestEntrypoint" } }, "gauntlet": { - "description": "Metadata about the plugin.", + "description": "General plugin metadata", "allOf": [ { "$ref": "#/definitions/PluginManifestMetadata" @@ -30,7 +30,7 @@ ] }, "permissions": { - "description": "Permissions required by the plugin.", + "description": "Permissions required by the plugin", "default": { "environment": [], "network": [], @@ -53,7 +53,7 @@ ] }, "preferences": { - "description": "Preferences that can be configured by the user in the settings view.", + "description": "Preferences that can be configured by the user in the settings view", "default": [], "type": "array", "items": { @@ -61,7 +61,7 @@ } }, "supported_system": { - "description": "List of supported operating systems.", + "description": "List of supported operating systems", "default": [], "type": "array", "items": { @@ -71,7 +71,7 @@ }, "definitions": { "PluginManifestAction": { - "description": "Action that can be performed by the plugin.", + "description": "Action definition", "type": "object", "required": [ "description", @@ -80,15 +80,15 @@ ], "properties": { "description": { - "description": "Description of what the action does.", + "description": "Description of what the action does", "type": "string" }, "id": { - "description": "Unique identifier for the action.", + "description": "Unique identifier for the action, can only contain letters and numbers", "type": "string" }, "shortcut": { - "description": "Keyboard shortcut to trigger the action.", + "description": "Default keyboard shortcut to trigger the action", "allOf": [ { "$ref": "#/definitions/PluginManifestActionShortcut" @@ -98,7 +98,7 @@ } }, "PluginManifestActionShortcut": { - "description": "Keyboard shortcut configuration for a plugin action.", + "description": "Keyboard shortcut for a plugin action", "type": "object", "required": [ "key", @@ -106,7 +106,7 @@ ], "properties": { "key": { - "description": "The key to be pressed for this shortcut.", + "description": "The main key to be pressed for this shortcut", "allOf": [ { "$ref": "#/definitions/PluginManifestActionShortcutKey" @@ -114,7 +114,7 @@ ] }, "kind": { - "description": "The type of shortcut.", + "description": "The kind of shortcut, defines required modifiers", "allOf": [ { "$ref": "#/definitions/PluginManifestActionShortcutKind" @@ -221,17 +221,17 @@ ] }, "PluginManifestActionShortcutKind": { - "description": "The type of shortcut.", + "description": "The kind of shortcut", "oneOf": [ { - "description": "Main shortcut for the action (e.g. cmd).", + "description": "Main kind shortcuts require Ctrl modifier on Windows/Linux or Cmd on macOS", "type": "string", "enum": [ "main" ] }, { - "description": "Alternative shortcut for the action (e.g. opt).", + "description": "Alternative kind shortcuts require Alt modifier on Windows/Linux or Opt on macOS", "type": "string", "enum": [ "alternative" @@ -240,24 +240,24 @@ ] }, "PluginManifestClipboardPermissions": { - "description": "Clipboard permissions for the plugin.", + "description": "Clipboard permissions for the plugin", "oneOf": [ { - "description": "Allows the plugin to read from the clipboard.", + "description": "Allows the plugin to read from the clipboard", "type": "string", "enum": [ "read" ] }, { - "description": "Allows the plugin to write to the clipboard.", + "description": "Allows the plugin to write to the clipboard", "type": "string", "enum": [ "write" ] }, { - "description": "Allows the plugin to clear the clipboard contents.", + "description": "Allows the plugin to clear the clipboard contents", "type": "string", "enum": [ "clear" @@ -266,7 +266,7 @@ ] }, "PluginManifestEntrypoint": { - "description": "An entrypoint for the plugin.", + "description": "Plugin entrypoint definition", "type": "object", "required": [ "description", @@ -277,6 +277,7 @@ ], "properties": { "actions": { + "description": "List of definitions of plugin actions", "default": [], "type": "array", "items": { @@ -284,24 +285,30 @@ } }, "description": { + "description": "Entrypoint description", "type": "string" }, "icon": { + "description": "Entrypoint icon, path to file in assets relative to it", "type": [ "string", "null" ] }, "id": { + "description": "Unique identifier of the entrypoint, can only contain small letters, numbers and dash", "type": "string" }, "name": { + "description": "Entrypoint name", "type": "string" }, "path": { + "description": "Path to TypeScript file relative to package directory", "type": "string" }, "preferences": { + "description": "List of definitions of plugin preferences", "default": [], "type": "array", "items": { @@ -309,36 +316,41 @@ } }, "type": { - "$ref": "#/definitions/PluginManifestEntrypointTypes" + "description": "Type of the entrypoint", + "allOf": [ + { + "$ref": "#/definitions/PluginManifestEntrypointTypes" + } + ] } } }, "PluginManifestEntrypointTypes": { - "description": "Types of plugin entrypoints.", + "description": "Types of plugin entrypoints", "oneOf": [ { - "description": "A command entrypoint.", + "description": "A function-based entrypoint", "type": "string", "enum": [ "command" ] }, { - "description": "A view-based entrypoint.", + "description": "A view-based entrypoint", "type": "string", "enum": [ "view" ] }, { - "description": "An inline view entrypoint.", + "description": "A view-based entrypoint displayed under main search bar", "type": "string", "enum": [ "inline-view" ] }, { - "description": "Generates new entrypoints dynamically.", + "description": "Entrypoint that dynamically generatepointsd", "type": "string", "enum": [ "entrypoint-generator" @@ -358,7 +370,7 @@ ] }, "PluginManifestMetadata": { - "description": "Metadata for the plugin manifest.", + "description": "General plugin metadata", "type": "object", "required": [ "description", @@ -366,21 +378,21 @@ ], "properties": { "description": { - "description": "Description of the plugin.", + "description": "Description of the plugin", "type": "string" }, "name": { - "description": "Name of the plugin.", + "description": "Name of the plugin", "type": "string" } } }, "PluginManifestPermissions": { - "description": "Permissions required by the plugin.", + "description": "Permissions required by the plugin", "type": "object", "properties": { "clipboard": { - "description": "Clipboard permissions for the plugin.", + "description": "Clipboard permissions for the plugin", "default": [], "type": "array", "items": { @@ -388,7 +400,7 @@ } }, "environment": { - "description": "Environment variables that the plugin can access.", + "description": "Environment variables that the plugin can access", "default": [], "type": "array", "items": { @@ -396,7 +408,7 @@ } }, "exec": { - "description": "Execution permissions for the plugin.", + "description": "Execution permissions for the plugin", "default": { "command": [], "executable": [] @@ -408,7 +420,7 @@ ] }, "filesystem": { - "description": "Filesystem permissions for the plugin.", + "description": "Filesystem permissions for the plugin", "default": { "read": [], "write": [] @@ -420,7 +432,7 @@ ] }, "main_search_bar": { - "description": "Permissions for the main search bar.", + "description": "Permissions for the main search bar", "default": [], "type": "array", "items": { @@ -428,7 +440,7 @@ } }, "network": { - "description": "Network domains that the plugin can access.", + "description": "Network address (domain or ip address + optional port) that the plugin can access", "default": [], "type": "array", "items": { @@ -436,7 +448,7 @@ } }, "system": { - "description": "System permissions for the plugin.", + "description": "Deno system permissions for the plugin", "default": [], "type": "array", "items": { @@ -446,11 +458,11 @@ } }, "PluginManifestPermissionsExec": { - "description": "Execution permissions for the plugin.", + "description": "Execution permissions for the plugin", "type": "object", "properties": { "command": { - "description": "Commands that the plugin can execute.", + "description": "List of commands on PATH that the plugin can execute", "default": [], "type": "array", "items": { @@ -458,7 +470,7 @@ } }, "executable": { - "description": "Executables that the plugin can run.", + "description": "List of paths to executables that the plugin can run", "default": [], "type": "array", "items": { @@ -468,11 +480,11 @@ } }, "PluginManifestPermissionsFileSystem": { - "description": "Filesystem permissions for the plugin.", + "description": "Filesystem permissions for the plugin", "type": "object", "properties": { "read": { - "description": "Paths that the plugin can read from.", + "description": "Paths that the plugin can read from", "default": [], "type": "array", "items": { @@ -480,7 +492,7 @@ } }, "write": { - "description": "Paths that the plugin can write to.", + "description": "Paths that the plugin can write to", "default": [], "type": "array", "items": { @@ -490,10 +502,10 @@ } }, "PluginManifestPreference": { - "description": "User-configurable preference options.", + "description": "User-configurable preference options", "oneOf": [ { - "description": "A numeric preference.", + "description": "A numeric preference", "type": "object", "required": [ "description", @@ -503,6 +515,7 @@ ], "properties": { "default": { + "description": "Default value", "type": [ "number", "null" @@ -510,12 +523,15 @@ "format": "double" }, "description": { + "description": "Description of the preference", "type": "string" }, "id": { + "description": "Unique identifier of the preference, can only contain letters and numbers", "type": "string" }, "name": { + "description": "Display name of the preference", "type": "string" }, "type": { @@ -527,7 +543,7 @@ } }, { - "description": "A string preference.", + "description": "A string preference", "type": "object", "required": [ "description", @@ -537,18 +553,22 @@ ], "properties": { "default": { + "description": "Default value", "type": [ "string", "null" ] }, "description": { + "description": "Description of the preference", "type": "string" }, "id": { + "description": "Unique identifier of the preference, can only contain letters and numbers", "type": "string" }, "name": { + "description": "Display name of the preference", "type": "string" }, "type": { @@ -560,7 +580,7 @@ } }, { - "description": "An enum preference with selectable values.", + "description": "An enum preference with selectable values", "type": "object", "required": [ "description", @@ -571,24 +591,29 @@ ], "properties": { "default": { + "description": "Default value", "type": [ "string", "null" ] }, "description": { + "description": "Description of the preference", "type": "string" }, "enum_values": { + "description": "List of allowed enum values", "type": "array", "items": { "$ref": "#/definitions/PluginManifestPreferenceEnumValue" } }, "id": { + "description": "Unique identifier of the preference, can only contain letters and numbers", "type": "string" }, "name": { + "description": "Display name of the preference", "type": "string" }, "type": { @@ -600,7 +625,7 @@ } }, { - "description": "A boolean preference.", + "description": "A boolean preference", "type": "object", "required": [ "description", @@ -610,18 +635,22 @@ ], "properties": { "default": { + "description": "Default value", "type": [ "boolean", "null" ] }, "description": { + "description": "Description of the preference", "type": "string" }, "id": { + "description": "Unique identifier of the preference, can only contain letters and numbers", "type": "string" }, "name": { + "description": "Display name of the preference", "type": "string" }, "type": { @@ -633,7 +662,7 @@ } }, { - "description": "A list of strings preference.", + "description": "A list of strings preference", "type": "object", "required": [ "description", @@ -643,12 +672,15 @@ ], "properties": { "description": { + "description": "Description of the preference", "type": "string" }, "id": { + "description": "Unique identifier of the preference, can only contain letters and numbers", "type": "string" }, "name": { + "description": "Display name of the preference", "type": "string" }, "type": { @@ -660,7 +692,7 @@ } }, { - "description": "A list of numbers preference.", + "description": "A list of numbers preference", "type": "object", "required": [ "description", @@ -670,12 +702,15 @@ ], "properties": { "description": { + "description": "Description of the preference", "type": "string" }, "id": { + "description": "Unique identifier of the preference, can only contain letters and numbers", "type": "string" }, "name": { + "description": "Display name of the preference", "type": "string" }, "type": { @@ -687,7 +722,7 @@ } }, { - "description": "A list of enumerated preference values.", + "description": "A list of enumerated preference values", "type": "object", "required": [ "description", @@ -698,18 +733,22 @@ ], "properties": { "description": { + "description": "Description of the preference", "type": "string" }, "enum_values": { + "description": "List of allowed enum values", "type": "array", "items": { "$ref": "#/definitions/PluginManifestPreferenceEnumValue" } }, "id": { + "description": "Unique identifier of the preference, can only contain letters and numbers", "type": "string" }, "name": { + "description": "Display name of the preference", "type": "string" }, "type": { @@ -723,7 +762,7 @@ ] }, "PluginManifestPreferenceEnumValue": { - "description": "An enumerated preference value.", + "description": "Definition of the values available in enumerated preference", "type": "object", "required": [ "label", @@ -731,9 +770,11 @@ ], "properties": { "label": { + "description": "Displayed name", "type": "string" }, "value": { + "description": "Internal enum value", "type": "string" } } diff --git a/rust/manifest_schema/src/main.rs b/rust/manifest_schema/src/main.rs index 7ffbc1c..1ec6401 100644 --- a/rust/manifest_schema/src/main.rs +++ b/rust/manifest_schema/src/main.rs @@ -1,4 +1,5 @@ use std::io::Write; +use std::path::PathBuf; use gauntlet_server::plugins::plugin_manifest::PluginManifest; use schemars::schema_for; @@ -7,8 +8,13 @@ fn main() { let schema = schema_for!(PluginManifest); let json = serde_json::to_string_pretty(&schema).unwrap(); - std::fs::create_dir_all("../../docs/schema").expect("Failed to create directory"); - std::fs::write("../../docs/schema/plugin_manifest.schema.json", json.as_bytes()).expect("Failed to write schema"); + let schema_path = PathBuf::from(concat!( + env!("CARGO_MANIFEST_DIR"), + "../../../docs/schema/plugin_manifest.schema.json" + )); + + std::fs::create_dir_all(schema_path.parent().unwrap()).expect("Failed to create directory"); + std::fs::write(schema_path, json.as_bytes()).expect("Failed to write schema"); println!("Schema generated and saved to schema.json"); } diff --git a/rust/server/src/plugins/plugin_manifest.rs b/rust/server/src/plugins/plugin_manifest.rs index 0665ca2..6e8726e 100644 --- a/rust/server/src/plugins/plugin_manifest.rs +++ b/rust/server/src/plugins/plugin_manifest.rs @@ -5,147 +5,185 @@ use serde::Serialize; use crate::model::ActionShortcutKey; #[derive(Debug, Deserialize, Serialize, JsonSchema)] -#[schemars(description = "Manifest structure for a plugin.")] +#[schemars(description = "Plugin Manifest definition")] pub struct PluginManifest { #[serde(rename = "$schema")] + #[allow(unused)] schema: Option, - #[schemars(description = "Metadata about the plugin.")] + #[schemars(description = "General plugin metadata")] pub gauntlet: PluginManifestMetadata, - #[schemars(description = "Entrypoints for the plugin.")] + #[schemars(description = "Plugin entrypoints, all plugin will have at least one entrypoint")] pub entrypoint: Vec, #[serde(default)] - #[schemars(description = "List of supported operating systems.")] + #[schemars(description = "List of supported operating systems")] pub supported_system: Vec, #[serde(default)] - #[schemars(description = "Permissions required by the plugin.")] + #[schemars(description = "Permissions required by the plugin")] pub permissions: PluginManifestPermissions, #[serde(default)] - #[schemars(description = "Preferences that can be configured by the user in the settings view.")] + #[schemars(description = "Preferences that can be configured by the user in the settings view")] pub preferences: Vec, } #[derive(Debug, Deserialize, Serialize, JsonSchema)] -#[schemars(description = "An entrypoint for the plugin.")] +#[schemars(description = "Plugin entrypoint definition")] pub struct PluginManifestEntrypoint { + #[schemars(description = "Unique identifier of the entrypoint, can only contain small letters, numbers and dash")] pub id: String, + #[schemars(description = "Entrypoint name")] pub name: String, + #[schemars(description = "Entrypoint description")] pub description: String, #[allow(unused)] // Used during plugin build - pub path: String, + #[schemars(description = "Path to TypeScript file relative to package directory")] + path: String, + #[schemars(description = "Entrypoint icon, path to file in assets relative to it")] pub icon: Option, #[serde(rename = "type")] + #[schemars(description = "Type of the entrypoint")] pub entrypoint_type: PluginManifestEntrypointTypes, #[serde(default)] + #[schemars(description = "List of definitions of plugin preferences")] pub preferences: Vec, #[serde(default)] + #[schemars(description = "List of definitions of plugin actions")] pub actions: Vec, } #[derive(Debug, Deserialize, Serialize, JsonSchema)] #[serde(tag = "type")] -#[schemars(description = "User-configurable preference options.")] +#[schemars(description = "User-configurable preference options")] pub enum PluginManifestPreference { #[serde(rename = "number")] - #[schemars(description = "A numeric preference.")] + #[schemars(description = "A numeric preference")] Number { + #[schemars(description = "Unique identifier of the preference, can only contain letters and numbers")] id: String, + #[schemars(description = "Display name of the preference")] name: String, + #[schemars(description = "Default value")] default: Option, + #[schemars(description = "Description of the preference")] description: String, }, #[serde(rename = "string")] - #[schemars(description = "A string preference.")] + #[schemars(description = "A string preference")] String { + #[schemars(description = "Unique identifier of the preference, can only contain letters and numbers")] id: String, + #[schemars(description = "Display name of the preference")] name: String, + #[schemars(description = "Default value")] default: Option, + #[schemars(description = "Description of the preference")] description: String, }, #[serde(rename = "enum")] - #[schemars(description = "An enum preference with selectable values.")] + #[schemars(description = "An enum preference with selectable values")] Enum { + #[schemars(description = "Unique identifier of the preference, can only contain letters and numbers")] id: String, + #[schemars(description = "Display name of the preference")] name: String, + #[schemars(description = "Default value")] default: Option, + #[schemars(description = "Description of the preference")] description: String, + #[schemars(description = "List of allowed enum values")] enum_values: Vec, }, #[serde(rename = "bool")] - #[schemars(description = "A boolean preference.")] + #[schemars(description = "A boolean preference")] Bool { + #[schemars(description = "Unique identifier of the preference, can only contain letters and numbers")] id: String, + #[schemars(description = "Display name of the preference")] name: String, + #[schemars(description = "Default value")] default: Option, + #[schemars(description = "Description of the preference")] description: String, }, #[serde(rename = "list_of_strings")] - #[schemars(description = "A list of strings preference.")] + #[schemars(description = "A list of strings preference")] ListOfStrings { + #[schemars(description = "Unique identifier of the preference, can only contain letters and numbers")] id: String, + #[schemars(description = "Display name of the preference")] name: String, // default: Option>, + #[schemars(description = "Description of the preference")] description: String, }, #[serde(rename = "list_of_numbers")] - #[schemars(description = "A list of numbers preference.")] + #[schemars(description = "A list of numbers preference")] ListOfNumbers { + #[schemars(description = "Unique identifier of the preference, can only contain letters and numbers")] id: String, + #[schemars(description = "Display name of the preference")] name: String, // default: Option>, + #[schemars(description = "Description of the preference")] description: String, }, #[serde(rename = "list_of_enums")] - #[schemars(description = "A list of enumerated preference values.")] + #[schemars(description = "A list of enumerated preference values")] ListOfEnums { + #[schemars(description = "Unique identifier of the preference, can only contain letters and numbers")] id: String, + #[schemars(description = "Display name of the preference")] name: String, // default: Option>, + #[schemars(description = "List of allowed enum values")] enum_values: Vec, + #[schemars(description = "Description of the preference")] description: String, }, } #[derive(Debug, Deserialize, Serialize, JsonSchema)] -#[schemars(description = "An enumerated preference value.")] +#[schemars(description = "Definition of the values available in enumerated preference")] pub struct PluginManifestPreferenceEnumValue { + #[schemars(description = "Displayed name")] pub label: String, + #[schemars(description = "Internal enum value")] pub value: String, } #[derive(Debug, Deserialize, Serialize, JsonSchema)] -#[schemars(description = "Types of plugin entrypoints.")] +#[schemars(description = "Types of plugin entrypoints")] pub enum PluginManifestEntrypointTypes { #[serde(rename = "command")] - #[schemars(description = "A command entrypoint.")] + #[schemars(description = "A function-based entrypoint")] Command, #[serde(rename = "view")] - #[schemars(description = "A view-based entrypoint.")] + #[schemars(description = "A view-based entrypoint")] View, #[serde(rename = "inline-view")] - #[schemars(description = "An inline view entrypoint.")] + #[schemars(description = "A view-based entrypoint displayed under main search bar")] InlineView, #[serde(rename = "entrypoint-generator")] - #[schemars(description = "Generates new entrypoints dynamically.")] + #[schemars(description = "Entrypoint that dynamically generatepointsd")] EntrypointGenerator, } #[derive(Debug, Deserialize, Serialize, JsonSchema)] -#[schemars(description = "Action that can be performed by the plugin.")] +#[schemars(description = "Action definition")] pub struct PluginManifestAction { - #[schemars(description = "Unique identifier for the action.")] + #[schemars(description = "Unique identifier for the action, can only contain letters and numbers")] pub id: String, - #[schemars(description = "Description of what the action does.")] + #[schemars(description = "Description of what the action does")] pub description: String, - #[schemars(description = "Keyboard shortcut to trigger the action.")] + #[schemars(description = "Default keyboard shortcut to trigger the action")] pub shortcut: PluginManifestActionShortcut, } #[derive(Debug, Deserialize, Serialize, JsonSchema)] -#[schemars(description = "Keyboard shortcut configuration for a plugin action.")] +#[schemars(description = "Keyboard shortcut for a plugin action")] pub struct PluginManifestActionShortcut { - #[schemars(description = "The key to be pressed for this shortcut.")] + #[schemars(description = "The main key to be pressed for this shortcut")] pub key: PluginManifestActionShortcutKey, - #[schemars(description = "The type of shortcut.")] + #[schemars(description = "The kind of shortcut, defines required modifiers")] pub kind: PluginManifestActionShortcutKind, } @@ -443,13 +481,13 @@ impl PluginManifestActionShortcutKey { } #[derive(Debug, Deserialize, Serialize, JsonSchema)] -#[schemars(description = "The type of shortcut.")] +#[schemars(description = "The kind of shortcut")] pub enum PluginManifestActionShortcutKind { #[serde(rename = "main")] - #[schemars(description = "Main shortcut for the action (e.g. cmd).")] + #[schemars(description = "Main kind shortcuts require Ctrl modifier on Windows/Linux or Cmd on macOS")] Main, #[serde(rename = "alternative")] - #[schemars(description = "Alternative shortcut for the action (e.g. opt).")] + #[schemars(description = "Alternative kind shortcuts require Alt modifier on Windows/Linux or Opt on macOS")] Alternative, } @@ -475,73 +513,73 @@ impl std::fmt::Display for PluginManifestSupportedSystem { } #[derive(Debug, Deserialize, Serialize, JsonSchema)] -#[schemars(description = "Metadata for the plugin manifest.")] +#[schemars(description = "General plugin metadata")] pub struct PluginManifestMetadata { - #[schemars(description = "Name of the plugin.")] + #[schemars(description = "Name of the plugin")] pub name: String, - #[schemars(description = "Description of the plugin.")] + #[schemars(description = "Description of the plugin")] pub description: String, } #[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] -#[schemars(description = "Permissions required by the plugin.")] +#[schemars(description = "Permissions required by the plugin")] pub struct PluginManifestPermissions { #[serde(default)] - #[schemars(description = "Environment variables that the plugin can access.")] + #[schemars(description = "Environment variables that the plugin can access")] pub environment: Vec, #[serde(default)] - #[schemars(description = "Network domains that the plugin can access.")] + #[schemars(description = "Network address (domain or ip address + optional port) that the plugin can access")] pub network: Vec, #[serde(default)] - #[schemars(description = "Filesystem permissions for the plugin.")] + #[schemars(description = "Filesystem permissions for the plugin")] pub filesystem: PluginManifestPermissionsFileSystem, #[serde(default)] - #[schemars(description = "Execution permissions for the plugin.")] + #[schemars(description = "Execution permissions for the plugin")] pub exec: PluginManifestPermissionsExec, #[serde(default)] - #[schemars(description = "System permissions for the plugin.")] + #[schemars(description = "Deno system permissions for the plugin")] pub system: Vec, #[serde(default)] - #[schemars(description = "Clipboard permissions for the plugin.")] + #[schemars(description = "Clipboard permissions for the plugin")] pub clipboard: Vec, #[serde(default)] - #[schemars(description = "Permissions for the main search bar.")] + #[schemars(description = "Permissions for the main search bar")] pub main_search_bar: Vec, } #[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] -#[schemars(description = "Filesystem permissions for the plugin.")] +#[schemars(description = "Filesystem permissions for the plugin")] pub struct PluginManifestPermissionsFileSystem { #[serde(default)] - #[schemars(description = "Paths that the plugin can read from.")] + #[schemars(description = "Paths that the plugin can read from")] pub read: Vec, #[serde(default)] - #[schemars(description = "Paths that the plugin can write to.")] + #[schemars(description = "Paths that the plugin can write to")] pub write: Vec, } #[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] -#[schemars(description = "Execution permissions for the plugin.")] +#[schemars(description = "Execution permissions for the plugin")] pub struct PluginManifestPermissionsExec { #[serde(default)] - #[schemars(description = "Commands that the plugin can execute.")] + #[schemars(description = "List of commands on PATH that the plugin can execute")] pub command: Vec, #[serde(default)] - #[schemars(description = "Executables that the plugin can run.")] + #[schemars(description = "List of paths to executables that the plugin can run")] pub executable: Vec, } #[derive(Debug, Deserialize, Serialize, JsonSchema)] -#[schemars(description = "Clipboard permissions for the plugin.")] +#[schemars(description = "Clipboard permissions for the plugin")] pub enum PluginManifestClipboardPermissions { #[serde(rename = "read")] - #[schemars(description = "Allows the plugin to read from the clipboard.")] + #[schemars(description = "Allows the plugin to read from the clipboard")] Read, #[serde(rename = "write")] - #[schemars(description = "Allows the plugin to write to the clipboard.")] + #[schemars(description = "Allows the plugin to write to the clipboard")] Write, #[serde(rename = "clear")] - #[schemars(description = "Allows the plugin to clear the clipboard contents.")] + #[schemars(description = "Allows the plugin to clear the clipboard contents")] Clear, } From 3d962f34050de0eae247f5edaa802d76804eb80f Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 16 Mar 2025 11:56:28 +0100 Subject: [PATCH 408/540] Add authors field to plugin manifest --- rust/server/src/plugins/plugin_manifest.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/rust/server/src/plugins/plugin_manifest.rs b/rust/server/src/plugins/plugin_manifest.rs index 6e8726e..00f9af0 100644 --- a/rust/server/src/plugins/plugin_manifest.rs +++ b/rust/server/src/plugins/plugin_manifest.rs @@ -519,6 +519,21 @@ pub struct PluginManifestMetadata { pub name: String, #[schemars(description = "Description of the plugin")] pub description: String, + #[schemars(description = "Description of the plugin")] + #[serde(default)] + pub authors: Vec, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[schemars(description = "Plugin author")] +pub struct PluginManifestMetadataAuthor { + #[schemars(description = "Author name")] + pub name: String, + #[schemars( + description = "URIs that identify the author. Can be a link to social media page or an email (if email it should begin with mailto: schema)" + )] + #[serde(default)] + pub uris: Vec, } #[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] From b47906fbad6ccc6b8df42a689fb8120b170f113d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 16 Mar 2025 12:08:16 +0100 Subject: [PATCH 409/540] Fix author field schema description --- docs/schema/plugin_manifest.schema.json | 29 ++++++++++++++++++++++ rust/server/src/plugins/plugin_manifest.rs | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/schema/plugin_manifest.schema.json b/docs/schema/plugin_manifest.schema.json index 11f9fbd..7189275 100644 --- a/docs/schema/plugin_manifest.schema.json +++ b/docs/schema/plugin_manifest.schema.json @@ -377,6 +377,14 @@ "name" ], "properties": { + "authors": { + "description": "List of plugin authors", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/PluginManifestMetadataAuthor" + } + }, "description": { "description": "Description of the plugin", "type": "string" @@ -387,6 +395,27 @@ } } }, + "PluginManifestMetadataAuthor": { + "description": "Plugin author", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Author name", + "type": "string" + }, + "uris": { + "description": "URIs that identify the author. Can be a link to social media page or an email (if email it should begin with mailto: schema)", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, "PluginManifestPermissions": { "description": "Permissions required by the plugin", "type": "object", diff --git a/rust/server/src/plugins/plugin_manifest.rs b/rust/server/src/plugins/plugin_manifest.rs index 00f9af0..133bb25 100644 --- a/rust/server/src/plugins/plugin_manifest.rs +++ b/rust/server/src/plugins/plugin_manifest.rs @@ -519,7 +519,7 @@ pub struct PluginManifestMetadata { pub name: String, #[schemars(description = "Description of the plugin")] pub description: String, - #[schemars(description = "Description of the plugin")] + #[schemars(description = "List of plugin authors")] #[serde(default)] pub authors: Vec, } From aacf78a377593c11c93732e7356c22f0ab378a99 Mon Sep 17 00:00:00 2001 From: DaRacci Date: Thu, 20 Mar 2025 23:20:00 +1100 Subject: [PATCH 410/540] fix(flake): update npmDepsHash --- nix/overlay.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/overlay.nix b/nix/overlay.nix index 0918940..37cf374 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -6,7 +6,7 @@ flake.overlays.default = final: _: let # These must be updated following the instructions in ./nix/README.md when dependencies are updated or the version bumped version = "v17"; - npmDepsHash = "sha256-rk5aLdXoSpqMNcSVIRJTKN1KqtddVPiKkZ1YWZ+n5m8="; + npmDepsHash = "sha256-BOnKpFS0ofIZbmXcyEzHzHgvbgeg3QUXnC79BwKO6P8="; RUSTY_V8_ARCHIVE = fetchRustyV8 "130.0.2" { aarch64-darwin = "sha256-aWZ/4Q4Wttx37xOdBmTCPGP+eYGhr4CM1UkYq8pC7Qs="; aarch64-linux = "sha256-p9+tHmKIM5wBABubHIAstpwfzO19ypPzOuaV4b6loCU="; From ae0f833f15e22b3757a63f303aef6158445faf8d Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 30 Mar 2025 19:25:02 +0200 Subject: [PATCH 411/540] Rework shortcut selector ui in settings --- rust/client/src/ui/widget_container.rs | 2 +- .../src/components/shortcut_selector.rs | 363 +++++++++++++----- rust/management_client/src/ui.rs | 30 +- rust/management_client/src/views/general.rs | 136 +++---- rust/management_client/src/views/plugins.rs | 156 ++++---- .../src/views/plugins/table.rs | 19 +- 6 files changed, 416 insertions(+), 290 deletions(-) diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs index 3aa92f0..8416711 100644 --- a/rust/client/src/ui/widget_container.rs +++ b/rust/client/src/ui/widget_container.rs @@ -76,7 +76,7 @@ impl PluginWidgetContainer { self.images = images; // use new state with values from old state but only widget ids which exists in new state - // so we this way we use already existing values but remove state for removed widgets + // so this way we use already existing values but remove state for removed widgets let old_state = mem::replace(&mut self.state, create_state(&container)); for (key, value) in old_state.into_iter() { diff --git a/rust/management_client/src/components/shortcut_selector.rs b/rust/management_client/src/components/shortcut_selector.rs index df54fe5..f46dcdc 100644 --- a/rust/management_client/src/components/shortcut_selector.rs +++ b/rust/management_client/src/components/shortcut_selector.rs @@ -1,10 +1,13 @@ +use gauntlet_common::model::EntrypointId; use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; use gauntlet_common_ui::physical_key_model; use gauntlet_common_ui::shortcut_to_text; use iced::advanced::graphics::core::event; use iced::advanced::graphics::core::keyboard; use iced::advanced::layout; use iced::advanced::mouse; +use iced::advanced::overlay; use iced::advanced::renderer; use iced::advanced::widget::tree; use iced::advanced::widget::Tree; @@ -12,95 +15,123 @@ use iced::advanced::Clipboard; use iced::advanced::Layout; use iced::advanced::Shell; use iced::advanced::Widget; -use iced::alignment; use iced::keyboard::key::Physical; use iced::mouse::Button; +use iced::widget::column; use iced::widget::container; use iced::widget::container::draw_background; -use iced::widget::container::layout; use iced::widget::row; use iced::widget::text; -use iced::Element; +use iced::Alignment; use iced::Event; use iced::Length; use iced::Padding; +use iced::Point; use iced::Rectangle; use iced::Renderer; use iced::Size; +use iced::Vector; -pub struct ShortcutSelector<'a, Message, Theme> -where - Theme: Catalog + text::Catalog + container::Catalog, -{ - padding: Padding, - width: Length, - height: Length, - max_width: f32, - max_height: f32, - horizontal_alignment: alignment::Horizontal, - vertical_alignment: alignment::Vertical, +use crate::theme::text::TextStyle; +use crate::theme::Element; +use crate::theme::GauntletSettingsTheme; - on_shortcut_captured: Box) -> Message + 'a>, - on_capturing_change: Box Message + 'a>, - - content: Element<'a, Message, Theme>, +pub struct ShortcutData { + pub shortcut: Option, + pub error: Option, } -impl<'a, Message: 'a, Theme> ShortcutSelector<'a, Message, Theme> +pub fn shortcut_selector<'a, 'b: 'a, 'c, Message: 'a, Id: 'a, F>( + shortcut_id: Id, + current_shortcut: &'b ShortcutData, + on_shortcut_captured: F, + overlay_class: ::Class<'a>, +) -> Element<'a, Message> where - Theme: Catalog + text::Catalog + container::Catalog + 'a, + F: 'a + Fn(Id, Option) -> Message, + Id: Clone, { - pub fn new( - current_shortcut: &Option, + Element::new(ShortcutSelector::new::( + shortcut_id, + current_shortcut, + on_shortcut_captured, + overlay_class, + )) +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum ShortcutId { + Global, + Entrypoint { + plugin_id: PluginId, + entrypoint_id: EntrypointId, + }, +} + +pub struct ShortcutSelector<'a, 'b, Message, Id> +where + Id: Clone, +{ + on_shortcut_captured: Box) -> Message + 'a>, + + shortcut_id: Id, + current_shortcut: &'b ShortcutData, + + content: Element<'a, Message>, + popup: Element<'a, Message>, + overlay_class: ::Class<'a>, +} + +impl<'a, 'b, 'c, Message: 'a, Id> ShortcutSelector<'a, 'b, Message, Id> +where + Id: Clone, +{ + pub fn new( + shortcut_id: Id, + current_shortcut: &'b ShortcutData, on_shortcut_captured: F, - on_capturing_change: F2, + overlay_class: ::Class<'a>, ) -> Self where - F: 'a + Fn(Option) -> Message, - F2: 'a + Fn(bool) -> Message, + F: 'a + Fn(Id, Option) -> Message, { - let mut content: Vec> = vec![]; + let content = render_shortcut(¤t_shortcut.shortcut); - if let Some(current_shortcut) = current_shortcut { - let (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) = - shortcut_to_text(current_shortcut); + let content = container(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x(Length::Fill) + .center_y(Length::Fill) + .into(); - if let Some(meta_modifier_text) = meta_modifier_text { - content.push(meta_modifier_text); - } + let recording_text: Element<_> = text("Recording shortcut...").into(); - if let Some(control_modifier_text) = control_modifier_text { - content.push(control_modifier_text); - } + let backspace_test: Element<_> = text("Backspace - Unset Shortcut").class(TextStyle::Subtitle).into(); - if let Some(shift_modifier_text) = shift_modifier_text { - content.push(shift_modifier_text); - } + let escape_test: Element<_> = text("Escape - Stop Capturing").class(TextStyle::Subtitle).into(); - if let Some(alt_modifier_text) = alt_modifier_text { - content.push(alt_modifier_text); - } + let popup: Element<_> = column(vec![recording_text, backspace_test, escape_test]) + .align_x(Alignment::Center) + .into(); - content.push(key_name); - } - - let content: Element<_, _> = row(content).spacing(8.0).into(); - - let content = container(content).into(); + let popup = container(popup) + .max_height(80) + .center_x(Length::Fill) + .center_y(Length::Fill) + .max_width(300) + .width(Length::Fill) + .height(Length::Fill) + .into(); Self { - padding: Padding::ZERO, - width: Length::Fill, - height: Length::Fill, - max_width: f32::INFINITY, - max_height: f32::INFINITY, - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, - on_shortcut_captured: Box::new(on_shortcut_captured), - on_capturing_change: Box::new(on_capturing_change), + shortcut_id, + current_shortcut, content, + popup, + + overlay_class, } } } @@ -110,36 +141,26 @@ struct State { is_capturing: bool, } -impl<'a, Message, Theme> Widget for ShortcutSelector<'a, Message, Theme> +impl<'a, 'b, Message: 'a, Id> Widget for ShortcutSelector<'a, 'b, Message, Id> where - Theme: Catalog + text::Catalog + container::Catalog, + Id: Clone, { fn size(&self) -> Size { Size { - width: self.width, - height: self.height, + width: Length::Fill, + height: Length::Fill, } } fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - layout( - limits, - self.width, - self.height, - self.max_width, - self.max_height, - self.padding, - self.horizontal_alignment, - self.vertical_alignment, - |limits| self.content.as_widget().layout(tree, renderer, limits), - ) + self.content.as_widget().layout(&mut tree.children[0], renderer, limits) } fn draw( &self, tree: &Tree, renderer: &mut Renderer, - theme: &Theme, + theme: &GauntletSettingsTheme, renderer_style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, @@ -153,18 +174,16 @@ where Status::Active }; - let style = Catalog::style(theme, &::default(), style); + let style = Catalog::style(theme, &::default(), style); draw_background(renderer, &style, layout.bounds()); self.content.as_widget().draw( - tree, + &tree.children[0], renderer, theme, - &renderer::Style { - text_color: renderer_style.text_color, - }, - layout.children().next().unwrap(), + renderer_style, + layout, cursor, viewport, ); @@ -179,11 +198,11 @@ where } fn children(&self) -> Vec { - self.content.as_widget().children() + vec![Tree::new(&self.content), Tree::new(&self.popup)] } fn diff(&self, tree: &mut Tree) { - self.content.as_widget().diff(tree); + tree.diff_children(&[self.content.as_widget(), self.popup.as_widget()]); } fn on_event( @@ -211,21 +230,18 @@ where match physical_key { Physical::Code(code) => { match code { - keyboard::key::Code::Backspace => { + keyboard::key::Code::Backspace if modifiers.is_empty() => { state.is_capturing = false; - let message = (self.on_capturing_change)(false); - shell.publish(message); - - let message = (self.on_shortcut_captured)(None); + let message = (self.on_shortcut_captured)(self.shortcut_id.clone(), None); shell.publish(message); event::Status::Ignored } - keyboard::key::Code::Escape => { + keyboard::key::Code::Escape if modifiers.is_empty() => { state.is_capturing = false; - let message = (self.on_capturing_change)(false); + let message = (self.on_shortcut_captured)(self.shortcut_id.clone(), None); shell.publish(message); event::Status::Ignored @@ -236,10 +252,10 @@ where Some(shortcut) => { state.is_capturing = false; - let message = (self.on_capturing_change)(false); - shell.publish(message); - - let message = (self.on_shortcut_captured)(Some(shortcut)); + let message = (self.on_shortcut_captured)( + self.shortcut_id.clone(), + Some(shortcut), + ); shell.publish(message); event::Status::Captured @@ -263,16 +279,10 @@ where if cursor.is_over(layout.bounds()) { state.is_capturing = true; - let message = (self.on_capturing_change)(true); - shell.publish(message); - event::Status::Captured } else { state.is_capturing = false; - let message = (self.on_capturing_change)(false); - shell.publish(message); - event::Status::Ignored } } @@ -297,15 +307,40 @@ where mouse::Interaction::default() } } -} -impl<'a, Message, Theme> From> for Element<'a, Message, Theme> -where - Message: 'a, - Theme: Catalog + text::Catalog + container::Catalog + 'a, -{ - fn from(shortcut_selector: ShortcutSelector<'a, Message, Theme>) -> Self { - Self::new(shortcut_selector) + fn overlay<'c>( + &'c mut self, + tree: &'c mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + let state = tree.state.downcast_ref::(); + + let mut children = tree.children.iter_mut(); + + let content = self + .content + .as_widget_mut() + .overlay(children.next().unwrap(), layout, renderer, translation); + + let popup = if state.is_capturing { + Some(overlay::Element::new(Box::new(Overlay { + position: layout.position() + translation, + popup: &self.popup, + state: children.next().unwrap(), + content_bounds: layout.bounds(), + class: &self.overlay_class, + }))) + } else { + None + }; + + if content.is_some() || popup.is_some() { + Some(overlay::Group::with_children(content.into_iter().chain(popup).collect()).overlay()) + } else { + None + } } } @@ -322,3 +357,121 @@ pub trait Catalog { fn style(&self, class: &Self::Class<'_>, status: Status) -> container::Style; } + +pub fn render_shortcut<'a, Message: 'a>(shortcut: &Option) -> Element<'a, Message> { + let mut content: Vec> = vec![]; + + if let Some(current_shortcut) = shortcut { + let (key_name, alt_modifier_text, meta_modifier_text, control_modifier_text, shift_modifier_text) = + shortcut_to_text(current_shortcut); + + if let Some(meta_modifier_text) = meta_modifier_text { + content.push(meta_modifier_text); + } + + if let Some(control_modifier_text) = control_modifier_text { + content.push(control_modifier_text); + } + + if let Some(shift_modifier_text) = shift_modifier_text { + content.push(shift_modifier_text); + } + + if let Some(alt_modifier_text) = alt_modifier_text { + content.push(alt_modifier_text); + } + + content.push(key_name); + } + + let content: Element = row(content).spacing(8.0).into(); + + content +} + +struct Overlay<'a, 'b, Message> { + position: Point, + popup: &'b Element<'a, Message>, + state: &'b mut Tree, + content_bounds: Rectangle, + class: &'b ::Class<'a>, +} + +impl<'a, 'b, Message> overlay::Overlay for Overlay<'a, 'b, Message> { + fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { + let padding = 2.0; + let gap = 10.0; + + let viewport = Rectangle::with_size(bounds); + + let popup_layout = self.popup.as_widget().layout( + self.state, + renderer, + &layout::Limits::new(Size::ZERO, viewport.size()).shrink(Padding::new(padding)), + ); + + let text_bounds = popup_layout.bounds(); + let x_center = self.position.x + (self.content_bounds.width - text_bounds.width) / 2.0; + + let mut tooltip_bounds = { + let offset = Vector::new(x_center, self.position.y + self.content_bounds.height + gap + padding); + + Rectangle { + x: offset.x - padding, + y: offset.y - padding, + width: text_bounds.width + padding * 2.0, + height: text_bounds.height + padding * 2.0, + } + }; + + // snap_within_viewport + if tooltip_bounds.x < viewport.x { + tooltip_bounds.x = viewport.x; + } else if viewport.x + viewport.width < tooltip_bounds.x + tooltip_bounds.width { + tooltip_bounds.x = viewport.x + viewport.width - tooltip_bounds.width; + } + + if tooltip_bounds.y < viewport.y { + tooltip_bounds.y = viewport.y; + } else if viewport.y + viewport.height < tooltip_bounds.y + tooltip_bounds.height { + tooltip_bounds.y = viewport.y + viewport.height - tooltip_bounds.height; + } + + layout::Node::with_children( + tooltip_bounds.size(), + vec![popup_layout.translate(Vector::new(padding, padding))], + ) + .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y)) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &GauntletSettingsTheme, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + ) { + let style = ::style(theme, self.class); + + draw_background(renderer, &style, layout.bounds()); + + let defaults = renderer::Style { + text_color: style.text_color.unwrap_or(inherited_style.text_color), + }; + + self.popup.as_widget().draw( + self.state, + renderer, + theme, + &defaults, + layout.children().next().unwrap(), + cursor_position, + &Rectangle::with_size(Size::INFINITY), + ); + } + + fn is_over(&self, _layout: Layout<'_>, _renderer: &Renderer, _cursor_position: Point) -> bool { + false + } +} diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index 7d6fdcf..79436ba 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -79,7 +79,7 @@ struct ManagementAppModel { } #[derive(Debug, Clone)] -enum ManagementAppMsg { +pub enum ManagementAppMsg { FontLoaded(Result<(), font::Error>), General(ManagementAppGeneralMsgIn), Plugin(ManagementAppPluginMsgIn), @@ -191,34 +191,16 @@ fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task { state.plugins_state.update(message).map(|msg| { match msg { - ManagementAppPluginMsgOut::PluginsReloaded(plugins) => { - ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::PluginsFetched(plugins)) - } - ManagementAppPluginMsgOut::Noop => ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::Noop), - ManagementAppPluginMsgOut::DownloadPlugin { plugin_id } => { - ManagementAppMsg::DownloadPlugin { plugin_id } - } - ManagementAppPluginMsgOut::SelectedItem(selected_item) => { - ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::SelectItem(selected_item)) - } - ManagementAppPluginMsgOut::HandleBackendError(err) => ManagementAppMsg::HandleBackendError(err), + ManagementAppPluginMsgOut::Inner(msg) => ManagementAppMsg::Plugin(msg), + ManagementAppPluginMsgOut::Outer(msg) => msg, } }) } ManagementAppMsg::General(message) => { state.general_state.update(message).map(|msg| { match msg { - ManagementAppGeneralMsgOut::Noop => ManagementAppMsg::General(ManagementAppGeneralMsgIn::Noop), - ManagementAppGeneralMsgOut::HandleBackendError(err) => ManagementAppMsg::HandleBackendError(err), - ManagementAppGeneralMsgOut::SetGlobalShortcutResponse { - shortcut, - shortcut_error, - } => { - ManagementAppMsg::General(ManagementAppGeneralMsgIn::SetGlobalShortcutResponse { - shortcut, - shortcut_error, - }) - } + ManagementAppGeneralMsgOut::Inner(msg) => ManagementAppMsg::General(msg), + ManagementAppGeneralMsgOut::Outer(msg) => msg, } }) } @@ -266,7 +248,7 @@ fn update(state: &mut ManagementAppModel, message: ManagementAppMsg) -> Task, theme: SettingsTheme, window_position_mode: WindowPositionMode, - current_shortcut: Option, - current_shortcut_error: Option, - currently_capturing: bool, + current_shortcut: ShortcutData, } #[derive(Debug, Clone)] pub enum ManagementAppGeneralMsgIn { - ShortcutCaptured(Option), - CapturingChanged(bool), + ShortcutCaptured(ShortcutId, Option), ThemeChanged(SettingsTheme), WindowPositionModeChanged(WindowPositionMode), - SetGlobalShortcutResponse { + HandleShortcutResponse { + id: ShortcutId, shortcut: Option, shortcut_error: Option, }, @@ -57,12 +58,8 @@ pub enum ManagementAppGeneralMsgIn { #[derive(Debug, Clone)] pub enum ManagementAppGeneralMsgOut { - Noop, - SetGlobalShortcutResponse { - shortcut: Option, - shortcut_error: Option, - }, - HandleBackendError(BackendApiError), + Inner(ManagementAppGeneralMsgIn), + Outer(ManagementAppMsg), } impl ManagementAppGeneralState { @@ -71,9 +68,10 @@ impl ManagementAppGeneralState { backend_api, theme: SettingsTheme::AutoDetect, window_position_mode: WindowPositionMode::Static, - current_shortcut: None, - current_shortcut_error: None, - currently_capturing: false, + current_shortcut: ShortcutData { + shortcut: None, + error: None, + }, } } @@ -84,7 +82,7 @@ impl ManagementAppGeneralState { }; match message { - ManagementAppGeneralMsgIn::ShortcutCaptured(shortcut) => { + ManagementAppGeneralMsgIn::ShortcutCaptured(id, shortcut) => { let mut backend_api = backend_api.clone(); Task::perform( @@ -99,12 +97,14 @@ impl ManagementAppGeneralState { }, move |result| { let shortcut = shortcut.clone(); + let id = id.clone(); handle_backend_error(result, move |shortcut_error| { - ManagementAppGeneralMsgOut::SetGlobalShortcutResponse { + ManagementAppGeneralMsgOut::Inner(ManagementAppGeneralMsgIn::HandleShortcutResponse { + id, shortcut, shortcut_error, - } + }) }) }, ) @@ -118,13 +118,10 @@ impl ManagementAppGeneralState { } => { self.theme = theme; self.window_position_mode = window_position_mode; - self.current_shortcut = shortcut; - self.current_shortcut_error = shortcut_error; - - Task::done(ManagementAppGeneralMsgOut::Noop) - } - ManagementAppGeneralMsgIn::CapturingChanged(capturing) => { - self.currently_capturing = capturing; + self.current_shortcut = ShortcutData { + shortcut, + error: shortcut_error, + }; Task::none() } @@ -139,7 +136,9 @@ impl ManagementAppGeneralState { Ok(()) }, - |result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop), + |result| { + handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Outer(ManagementAppMsg::Noop)) + }, ) } ManagementAppGeneralMsgIn::WindowPositionModeChanged(mode) => { @@ -153,15 +152,20 @@ impl ManagementAppGeneralState { Ok(()) }, - |result| handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Noop), + |result| { + handle_backend_error(result, |()| ManagementAppGeneralMsgOut::Outer(ManagementAppMsg::Noop)) + }, ) } - ManagementAppGeneralMsgIn::SetGlobalShortcutResponse { + ManagementAppGeneralMsgIn::HandleShortcutResponse { + id, shortcut, shortcut_error, } => { - self.current_shortcut = shortcut; - self.current_shortcut_error = shortcut_error; + self.current_shortcut = ShortcutData { + shortcut, + error: shortcut_error, + }; Task::none() } @@ -169,12 +173,12 @@ impl ManagementAppGeneralState { } pub fn view(&self) -> Element { - let global_shortcut_selector: Element<_> = ShortcutSelector::new( + let global_shortcut_selector = shortcut_selector( + ShortcutId::Global, &self.current_shortcut, - move |value| ManagementAppGeneralMsgIn::ShortcutCaptured(value), - move |value| ManagementAppGeneralMsgIn::CapturingChanged(value), - ) - .into(); + move |id, shortcut| ManagementAppGeneralMsgIn::ShortcutCaptured(id, shortcut), + ContainerStyle::Box, + ); let global_shortcut_field: Element<_> = container(global_shortcut_selector) .width(Length::Fill) @@ -200,8 +204,6 @@ impl ManagementAppGeneralState { let content: Element<_> = container(content).width(Length::Fill).into(); - let content: Element<_> = container(content).width(Length::Fill).into(); - content } @@ -290,59 +292,41 @@ impl ManagementAppGeneralState { } fn shortcut_capture_after(&self) -> Element { - if self.currently_capturing { - let hint1: Element<_> = text("Backspace - Unset Shortcut") - .width(Length::Fill) - .class(TextStyle::Subtitle) + if let Some(current_shortcut_error) = &self.current_shortcut.error { + let error_icon: Element<_> = value(Bootstrap::ExclamationTriangleFill) + .font(BOOTSTRAP_FONT) + .class(TextStyle::Destructive) .into(); - let hint2: Element<_> = text("Escape - Stop Capturing") - .width(Length::Fill) - .class(TextStyle::Subtitle) + let error_text: Element<_> = text(current_shortcut_error).class(TextStyle::Destructive).into(); + + let error_text: Element<_> = container(error_text) + .padding(16.0) + .max_width(300) + .class(ContainerStyle::Box) .into(); - column(vec![hint1, hint2]) + let tooltip: Element<_> = tooltip(error_icon, error_text, Position::Bottom).into(); + + let content = container(tooltip) .width(Length::FillPortion(3)) - .align_x(Alignment::Center) + .align_y(alignment::Vertical::Center) .padding(Padding::from([0.0, 8.0])) - .into() + .into(); + + content } else { - if let Some(current_shortcut_error) = &self.current_shortcut_error { - let error_icon: Element<_> = value(Bootstrap::ExclamationTriangleFill) - .font(BOOTSTRAP_FONT) - .class(TextStyle::Destructive) - .into(); - - let error_text: Element<_> = text(current_shortcut_error).class(TextStyle::Destructive).into(); - - let error_text: Element<_> = container(error_text) - .padding(16.0) - .max_width(300) - .class(ContainerStyle::Box) - .into(); - - let tooltip: Element<_> = tooltip(error_icon, error_text, Position::Bottom).into(); - - let content = container(tooltip) - .width(Length::FillPortion(3)) - .align_y(alignment::Vertical::Center) - .padding(Padding::from([0.0, 8.0])) - .into(); - - content - } else { - Space::with_width(Length::FillPortion(3)).into() - } + Space::with_width(Length::FillPortion(3)).into() } } } -pub fn handle_backend_error( +fn handle_backend_error( result: Result, convert: impl FnOnce(T) -> ManagementAppGeneralMsgOut, ) -> ManagementAppGeneralMsgOut { match result { Ok(val) => convert(val), - Err(err) => ManagementAppGeneralMsgOut::HandleBackendError(err), + Err(err) => ManagementAppGeneralMsgOut::Outer(ManagementAppMsg::HandleBackendError(err)), } } diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index 9147edb..5cf806a 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -32,13 +32,13 @@ use iced_fonts::BOOTSTRAP_FONT; use crate::theme::button::ButtonStyle; use crate::theme::text::TextStyle; use crate::theme::Element; +use crate::ui::ManagementAppMsg; use crate::views::plugins::preferences::preferences_ui; use crate::views::plugins::preferences::PluginPreferencesMsg; use crate::views::plugins::preferences::SelectItem; use crate::views::plugins::table::PluginTableMsgIn; use crate::views::plugins::table::PluginTableMsgOut; use crate::views::plugins::table::PluginTableState; -use crate::views::plugins::table::PluginTableUpdateResult; mod preferences; mod table; @@ -48,19 +48,17 @@ pub enum ManagementAppPluginMsgIn { PluginTableMsg(PluginTableMsgIn), PluginPreferenceMsg(PluginPreferencesMsg), FetchPlugins, - PluginsFetched(HashMap), + PluginsReloaded(HashMap), RemovePlugin { plugin_id: PluginId }, + ToggleShowEntrypoint { plugin_id: PluginId }, DownloadPlugin { plugin_id: PluginId }, SelectItem(SelectedItem), Noop, } pub enum ManagementAppPluginMsgOut { - PluginsReloaded(HashMap), - SelectedItem(SelectedItem), - DownloadPlugin { plugin_id: PluginId }, - HandleBackendError(BackendApiError), - Noop, + Inner(ManagementAppPluginMsgIn), + Outer(ManagementAppMsg), } pub struct ManagementAppPluginsState { @@ -99,7 +97,7 @@ impl ManagementAppPluginsState { tracing::debug!("Opening selected item: {:?}", select_item); Self { - backend_api, + backend_api: backend_api.clone(), plugin_data: Rc::new(RefCell::new(PluginDataContainer::new())), preference_user_data: HashMap::new(), selected_item: select_item, @@ -115,71 +113,77 @@ impl ManagementAppPluginsState { match message { ManagementAppPluginMsgIn::PluginTableMsg(message) => { - match self.table_state.update(message) { - PluginTableUpdateResult::Command(command) => command.map(|_| ManagementAppPluginMsgOut::Noop), - PluginTableUpdateResult::Value(msg) => { - match msg { - PluginTableMsgOut::SetPluginState { enabled, plugin_id } => { - let mut backend_client = backend_api.clone(); + self.table_state.update(message).then(move |msg| { + match msg { + PluginTableMsgOut::SetPluginState { enabled, plugin_id } => { + let mut backend_client = backend_api.clone(); - Task::perform( - async move { - backend_client.set_plugin_state(plugin_id, enabled).await?; + Task::perform( + async move { + backend_client.set_plugin_state(plugin_id, enabled).await?; - let plugins = backend_client.plugins().await?; + let plugins = backend_client.plugins().await?; - Ok(plugins) - }, - |result| { - handle_backend_error(result, |plugins| { - ManagementAppPluginMsgOut::PluginsReloaded(plugins) - }) - }, - ) - } - PluginTableMsgOut::SetEntrypointState { - enabled, - plugin_id, - entrypoint_id, - } => { - let mut backend_client = backend_api.clone(); + Ok(plugins) + }, + |result| { + handle_backend_error(result, |plugins| { + ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded( + plugins, + )) + }) + }, + ) + } + PluginTableMsgOut::SetEntrypointState { + enabled, + plugin_id, + entrypoint_id, + } => { + let mut backend_client = backend_api.clone(); - Task::perform( - async move { - backend_client - .set_entrypoint_state(plugin_id, entrypoint_id, enabled) - .await?; + Task::perform( + async move { + backend_client + .set_entrypoint_state(plugin_id, entrypoint_id, enabled) + .await?; - let plugins = backend_client.plugins().await?; + let plugins = backend_client.plugins().await?; - Ok(plugins) - }, - |result| { - handle_backend_error(result, |plugins| { - ManagementAppPluginMsgOut::PluginsReloaded(plugins) - }) - }, - ) - } - PluginTableMsgOut::SelectItem(selected_item) => { - Task::done(ManagementAppPluginMsgOut::SelectedItem(selected_item)) - } - PluginTableMsgOut::ToggleShowEntrypoints { plugin_id } => { - let plugins = { - let mut plugin_data = self.plugin_data.borrow_mut(); - let settings_plugin_data = plugin_data.plugins_state.get_mut(&plugin_id).unwrap(); - settings_plugin_data.show_entrypoints = !settings_plugin_data.show_entrypoints; - - plugin_data.plugins.clone() - }; - - self.apply_plugin_fetch(plugins); - - Task::none() - } + Ok(plugins) + }, + |result| { + handle_backend_error(result, |plugins| { + ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded( + plugins, + )) + }) + }, + ) + } + PluginTableMsgOut::SelectItem(selected_item) => { + Task::done(ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::SelectItem( + selected_item, + ))) + } + PluginTableMsgOut::ToggleShowEntrypoints { plugin_id } => { + Task::done(ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::Noop)) } } - } + }) + } + ManagementAppPluginMsgIn::ToggleShowEntrypoint { plugin_id } => { + let plugins = { + let mut plugin_data = self.plugin_data.borrow_mut(); + let settings_plugin_data = plugin_data.plugins_state.get_mut(&plugin_id).unwrap(); + settings_plugin_data.show_entrypoints = !settings_plugin_data.show_entrypoints; + + plugin_data.plugins.clone() + }; + + self.apply_plugin_fetch(plugins); + + Task::none() } ManagementAppPluginMsgIn::PluginPreferenceMsg(msg) => { match msg { @@ -204,7 +208,11 @@ impl ManagementAppPluginsState { Ok(()) }, - |result| handle_backend_error(result, |()| ManagementAppPluginMsgOut::Noop), + |result| { + handle_backend_error(result, |()| { + ManagementAppPluginMsgOut::Outer(ManagementAppMsg::Noop) + }) + }, ) } } @@ -219,11 +227,13 @@ impl ManagementAppPluginsState { Ok(plugins) }, |result| { - handle_backend_error(result, |plugins| ManagementAppPluginMsgOut::PluginsReloaded(plugins)) + handle_backend_error(result, |plugins| { + ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded(plugins)) + }) }, ) } - ManagementAppPluginMsgIn::PluginsFetched(plugins) => { + ManagementAppPluginMsgIn::PluginsReloaded(plugins) => { self.apply_plugin_fetch(plugins); Task::none() @@ -242,12 +252,16 @@ impl ManagementAppPluginsState { Ok(plugins) }, |result| { - handle_backend_error(result, |plugins| ManagementAppPluginMsgOut::PluginsReloaded(plugins)) + handle_backend_error(result, |plugins| { + ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded(plugins)) + }) }, ) } ManagementAppPluginMsgIn::DownloadPlugin { plugin_id } => { - Task::done(ManagementAppPluginMsgOut::DownloadPlugin { plugin_id }) + Task::done(ManagementAppPluginMsgOut::Outer(ManagementAppMsg::DownloadPlugin { + plugin_id, + })) } ManagementAppPluginMsgIn::SelectItem(selected_item) => { self.selected_item = selected_item; @@ -696,6 +710,6 @@ pub fn handle_backend_error( ) -> ManagementAppPluginMsgOut { match result { Ok(val) => convert(val), - Err(err) => ManagementAppPluginMsgOut::HandleBackendError(err), + Err(err) => ManagementAppPluginMsgOut::Outer(ManagementAppMsg::HandleBackendError(err)), } } diff --git a/rust/management_client/src/views/plugins/table.rs b/rust/management_client/src/views/plugins/table.rs index bb35d10..99438f7 100644 --- a/rust/management_client/src/views/plugins/table.rs +++ b/rust/management_client/src/views/plugins/table.rs @@ -62,11 +62,6 @@ pub struct PluginTableState { body: Id, } -pub enum PluginTableUpdateResult { - Command(Task<()>), - Value(PluginTableMsgOut), -} - impl PluginTableState { pub fn new() -> Self { Self { @@ -82,22 +77,20 @@ impl PluginTableState { } } - pub fn update(&mut self, message: PluginTableMsgIn) -> PluginTableUpdateResult { + pub fn update(&mut self, message: PluginTableMsgIn) -> Task { match message { - PluginTableMsgIn::TableSyncHeader(offset) => { - PluginTableUpdateResult::Command(scrollable::scroll_to(self.header.clone(), offset)) - } + PluginTableMsgIn::TableSyncHeader(offset) => scrollable::scroll_to(self.header.clone(), offset), PluginTableMsgIn::EnabledToggleItem(item) => { match item { EnabledItem::Plugin { enabled, plugin_id } => { - PluginTableUpdateResult::Value(PluginTableMsgOut::SetPluginState { enabled, plugin_id }) + Task::done(PluginTableMsgOut::SetPluginState { enabled, plugin_id }) } EnabledItem::Entrypoint { enabled, plugin_id, entrypoint_id, } => { - PluginTableUpdateResult::Value(PluginTableMsgOut::SetEntrypointState { + Task::done(PluginTableMsgOut::SetEntrypointState { enabled, plugin_id, entrypoint_id, @@ -105,9 +98,9 @@ impl PluginTableState { } } } - PluginTableMsgIn::SelectItem(item) => PluginTableUpdateResult::Value(PluginTableMsgOut::SelectItem(item)), + PluginTableMsgIn::SelectItem(item) => Task::done(PluginTableMsgOut::SelectItem(item)), PluginTableMsgIn::ToggleShowEntrypoints { plugin_id } => { - PluginTableUpdateResult::Value(PluginTableMsgOut::ToggleShowEntrypoints { plugin_id }) + Task::done(PluginTableMsgOut::ToggleShowEntrypoints { plugin_id }) } } } From 9f057c09dcf7484a8591ca6605dcdb907e76cdf0 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 3 Apr 2025 19:37:57 +0200 Subject: [PATCH 412/540] Show generated entrypoints in settings view --- Cargo.toml | 2 +- rust/common/src/model.rs | 7 + rust/common/src/rpc/backend_api.rs | 13 + rust/common/src/rpc/backend_server.rs | 14 + rust/management_client/src/views/plugins.rs | 152 ++++++++++- .../src/views/plugins/table.rs | 241 ++++++++++++++---- rust/server/src/plugins/js.rs | 15 +- rust/server/src/plugins/mod.rs | 55 +++- rust/server/src/search.rs | 17 +- schema/backend.proto | 5 + 10 files changed, 435 insertions(+), 86 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ad4095d..52021b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ edition = "2021" [workspace.dependencies] # iced #iced = { version = "0.13.99", features = ["tiny-skia", "wgpu", "tokio", "lazy", "advanced", "image"] } -iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", default-features = false, features = ["tiny-skia", "wgpu", "tokio", "advanced", "image"] } +iced = { git = "https://github.com/project-gauntlet/iced.git", branch = "gauntlet-0.13", default-features = false, features = ["tiny-skia", "wgpu", "tokio", "advanced", "image", "web-colors"] } #iced_aw = { version = "0.11.99", features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } iced_aw = { git = "https://github.com/project-gauntlet/iced_aw.git", branch = "gauntlet-0.13", default-features = false, features = ["date_picker", "wrap", "number_input", "grid", "spinner"] } #iced_table = "0.13.99" diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 330278c..a95ec26 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -676,6 +676,13 @@ pub struct SettingsEntrypoint { pub enabled: bool, pub preferences: HashMap, pub preferences_user_data: HashMap, + pub generated_entrypoints: HashMap, +} + +#[derive(Debug, Clone)] +pub struct SettingsGeneratedEntrypoint { + pub entrypoint_id: EntrypointId, + pub entrypoint_name: String, } #[derive(Debug, Clone)] diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index ddcd5bd..a006288 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -20,6 +20,7 @@ use crate::model::PluginPreferenceUserData; use crate::model::SearchResult; use crate::model::SettingsEntrypoint; use crate::model::SettingsEntrypointType; +use crate::model::SettingsGeneratedEntrypoint; use crate::model::SettingsPlugin; use crate::model::SettingsTheme; use crate::model::UiPropertyValue; @@ -418,6 +419,18 @@ impl BackendApi { .into_iter() .map(|(key, value)| (key, plugin_preference_user_data_from_rpc(value))) .collect(), + generated_entrypoints: entrypoint + .generated_entrypoints + .into_iter() + .map(|(entrypoint_id, data)| { + let generated_entrypoint = SettingsGeneratedEntrypoint { + entrypoint_id: EntrypointId::from_string(data.entrypoint_id), + entrypoint_name: data.entrypoint_name, + }; + + (EntrypointId::from_string(entrypoint_id), generated_entrypoint) + }) + .collect(), }; (id, entrypoint) }) diff --git a/rust/common/src/rpc/backend_server.rs b/rust/common/src/rpc/backend_server.rs index f03cfc3..9da185c 100644 --- a/rust/common/src/rpc/backend_server.rs +++ b/rust/common/src/rpc/backend_server.rs @@ -29,6 +29,7 @@ use crate::rpc::grpc::RpcDownloadStatusResponse; use crate::rpc::grpc::RpcDownloadStatusValue; use crate::rpc::grpc::RpcEntrypoint; use crate::rpc::grpc::RpcEntrypointTypeSettings; +use crate::rpc::grpc::RpcGeneratedEntrypoint; use crate::rpc::grpc::RpcGetGlobalShortcutRequest; use crate::rpc::grpc::RpcGetGlobalShortcutResponse; use crate::rpc::grpc::RpcGetThemeRequest; @@ -238,6 +239,19 @@ impl RpcBackend for RpcBackendServerImpl { .into_iter() .map(|(key, value)| (key, plugin_preference_user_data_to_rpc(value))) .collect(), + generated_entrypoints: entrypoint + .generated_entrypoints + .into_iter() + .map(|(entrypoint_id, data)| { + ( + entrypoint_id.to_string(), + RpcGeneratedEntrypoint { + entrypoint_id: data.entrypoint_id.to_string(), + entrypoint_name: data.entrypoint_name, + }, + ) + }) + .collect(), } }) .collect(); diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index 5cf806a..ba3e620 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -5,16 +5,19 @@ use std::rc::Rc; use gauntlet_common::model::EntrypointId; use gauntlet_common::model::PluginId; use gauntlet_common::model::PluginPreferenceUserData; +use gauntlet_common::model::SettingsEntrypointType; use gauntlet_common::model::SettingsPlugin; use gauntlet_common::rpc::backend_api::BackendApi; use gauntlet_common::rpc::backend_api::BackendApiError; use gauntlet_common::settings_env_data_from_string; use gauntlet_common::SettingsEnvData; use gauntlet_common::SETTINGS_ENV; +use iced::alignment; use iced::padding; use iced::widget::button; use iced::widget::column; use iced::widget::container; +use iced::widget::pane_grid::Edge; use iced::widget::row; use iced::widget::scrollable; use iced::widget::text; @@ -49,9 +52,19 @@ pub enum ManagementAppPluginMsgIn { PluginPreferenceMsg(PluginPreferencesMsg), FetchPlugins, PluginsReloaded(HashMap), - RemovePlugin { plugin_id: PluginId }, - ToggleShowEntrypoint { plugin_id: PluginId }, - DownloadPlugin { plugin_id: PluginId }, + RemovePlugin { + plugin_id: PluginId, + }, + ToggleShowEntrypoint { + plugin_id: PluginId, + }, + ToggleShowGeneratedEntrypoint { + plugin_id: PluginId, + entrypoint_id: EntrypointId, + }, + DownloadPlugin { + plugin_id: PluginId, + }, SelectItem(SelectedItem), Noop, } @@ -167,7 +180,20 @@ impl ManagementAppPluginsState { ))) } PluginTableMsgOut::ToggleShowEntrypoints { plugin_id } => { - Task::done(ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::Noop)) + Task::done(ManagementAppPluginMsgOut::Inner( + ManagementAppPluginMsgIn::ToggleShowEntrypoint { plugin_id }, + )) + } + PluginTableMsgOut::ToggleShowGeneratedEntrypoints { + plugin_id, + entrypoint_id, + } => { + Task::done(ManagementAppPluginMsgOut::Inner( + ManagementAppPluginMsgIn::ToggleShowGeneratedEntrypoint { + plugin_id, + entrypoint_id, + }, + )) } } }) @@ -185,6 +211,27 @@ impl ManagementAppPluginsState { Task::none() } + ManagementAppPluginMsgIn::ToggleShowGeneratedEntrypoint { + plugin_id, + entrypoint_id, + } => { + let plugins = { + let mut plugin_data = self.plugin_data.borrow_mut(); + let settings_plugin_data = plugin_data.plugins_state.get_mut(&plugin_id).unwrap(); + let settings_entrypoint_data = settings_plugin_data + .generator_entrypoint_state + .get_mut(&entrypoint_id) + .unwrap(); + + settings_entrypoint_data.show_entrypoints = !settings_entrypoint_data.show_entrypoints; + + plugin_data.plugins.clone() + }; + + self.apply_plugin_fetch(plugins); + + Task::none() + } ManagementAppPluginMsgIn::PluginPreferenceMsg(msg) => { match msg { PluginPreferencesMsg::UpdatePreferenceValue { @@ -303,14 +350,37 @@ impl ManagementAppPluginsState { plugin_data.plugins_state = plugins .iter() - .map(|(id, _plugin)| { - let show_entrypoints = plugin_data - .plugins_state - .get(&id) - .map(|data| data.show_entrypoints) - .unwrap_or(true); + .map(|(id, plugin)| { + let plugin_data = plugin_data.plugins_state.get(&id); - (id.clone(), SettingsPluginData { show_entrypoints }) + let show_entrypoints = plugin_data.map(|data| data.show_entrypoints).unwrap_or(true); + + let mut generator_entrypoint_state_old = plugin_data + .map(|data| data.generator_entrypoint_state.clone()) + .unwrap_or_default(); + + let generator_entrypoint_state = plugin + .entrypoints + .iter() + .filter(|(_, entrypoint)| { + matches!(entrypoint.entrypoint_type, SettingsEntrypointType::EntrypointGenerator) + }) + .map(|(_, entrypoint)| { + let generator_data = generator_entrypoint_state_old + .remove(&entrypoint.entrypoint_id) + .unwrap_or(SettingsGeneratorData { show_entrypoints: true }); + + (entrypoint.entrypoint_id.clone(), generator_data) + }) + .collect(); + + ( + id.clone(), + SettingsPluginData { + show_entrypoints, + generator_entrypoint_state, + }, + ) }) .collect(); @@ -377,7 +447,7 @@ impl ManagementAppPluginsState { .class(TextStyle::Subtitle) .into(); - let id = container(id).padding(padding::bottom(8.0)).into(); + let id = container(id).padding(padding::all(8.0).top(0)).into(); let mut column_content = vec![name, id]; @@ -385,7 +455,8 @@ impl ManagementAppPluginsState { let description_label: Element<_> = text("Description").size(14).class(TextStyle::Subtitle).into(); - let description_label = container(description_label).padding(padding::bottom(8.0)).into(); + let description_label = + container(description_label).padding(padding::all(8.0).top(0)).into(); let description = text(plugin.plugin_description.to_string()).shaping(Shaping::Advanced); @@ -485,7 +556,8 @@ impl ManagementAppPluginsState { let description_label: Element<_> = text("Description").size(14).class(TextStyle::Subtitle).into(); - let description_label = container(description_label).padding(padding::bottom(8.0)).into(); + let description_label = + container(description_label).padding(padding::all(8.0).top(0)).into(); let description = container(text(entrypoint.entrypoint_description.to_string())) .padding(Padding::new(8.0)) @@ -542,6 +614,47 @@ impl ManagementAppPluginsState { .align_x(Alignment::Center) .into() } + SelectedItem::GeneratedEntrypoint { + plugin_id, + generated_entrypoint_id, + generator_entrypoint_id, + } => { + let plugin_data = self.plugin_data.borrow(); + + let entrypoint = plugin_data + .plugins + .get(&plugin_id) + .map(|plugin| plugin.entrypoints.get(generator_entrypoint_id)) + .flatten() + .map(|entrypoint| entrypoint.generated_entrypoints.get(generated_entrypoint_id)) + .flatten(); + + match entrypoint { + None => { + let loading_text: Element<_> = text("Loading...").into(); + + container(loading_text) + .align_y(Alignment::Center) + .align_x(Alignment::Center) + .height(Length::Fill) + .width(Length::Fill) + .into() + } + Some(entrypoint) => { + let name: Element<_> = text(entrypoint.entrypoint_name.to_string()) + .shaping(Shaping::Advanced) + .into(); + + let name: Element<_> = container(name).padding(padding::all(8.0)).into(); + + container(name) + .padding(Padding::from([4.0, 0.0])) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + } + } }; let plugin_url = if let SelectedItem::NewPlugin { repository_url } = &self.selected_item { @@ -630,11 +743,22 @@ pub enum SelectedItem { plugin_id: PluginId, entrypoint_id: EntrypointId, }, + GeneratedEntrypoint { + plugin_id: PluginId, + generator_entrypoint_id: EntrypointId, + generated_entrypoint_id: EntrypointId, + }, } #[derive(Debug, Clone)] struct SettingsPluginData { show_entrypoints: bool, + generator_entrypoint_state: HashMap, +} + +#[derive(Debug, Clone)] +struct SettingsGeneratorData { + show_entrypoints: bool, } #[derive(Debug, Clone)] diff --git a/rust/management_client/src/views/plugins/table.rs b/rust/management_client/src/views/plugins/table.rs index 99438f7..c2c2bee 100644 --- a/rust/management_client/src/views/plugins/table.rs +++ b/rust/management_client/src/views/plugins/table.rs @@ -6,6 +6,7 @@ use gauntlet_common::model::PluginId; use gauntlet_common::model::SettingsEntrypointType; use gauntlet_common::model::SettingsPlugin; use iced::advanced::text::Shaping; +use iced::padding; use iced::widget::button; use iced::widget::checkbox; use iced::widget::container; @@ -36,7 +37,13 @@ pub enum PluginTableMsgIn { TableSyncHeader(scrollable::AbsoluteOffset), SelectItem(SelectedItem), EnabledToggleItem(EnabledItem), - ToggleShowEntrypoints { plugin_id: PluginId }, + ToggleShowEntrypoints { + plugin_id: PluginId, + }, + ToggleShowGeneratedEntrypoints { + plugin_id: PluginId, + entrypoint_id: EntrypointId, + }, } pub enum PluginTableMsgOut { @@ -53,6 +60,10 @@ pub enum PluginTableMsgOut { ToggleShowEntrypoints { plugin_id: PluginId, }, + ToggleShowGeneratedEntrypoints { + plugin_id: PluginId, + entrypoint_id: EntrypointId, + }, } pub struct PluginTableState { @@ -66,7 +77,6 @@ impl PluginTableState { pub fn new() -> Self { Self { columns: vec![ - Column::new(ColumnKind::ShowEntrypointsToggle), Column::new(ColumnKind::Name), Column::new(ColumnKind::Type), Column::new(ColumnKind::EnableToggle), @@ -102,6 +112,15 @@ impl PluginTableState { PluginTableMsgIn::ToggleShowEntrypoints { plugin_id } => { Task::done(PluginTableMsgOut::ToggleShowEntrypoints { plugin_id }) } + PluginTableMsgIn::ToggleShowGeneratedEntrypoints { + plugin_id, + entrypoint_id, + } => { + Task::done(PluginTableMsgOut::ToggleShowGeneratedEntrypoints { + plugin_id, + entrypoint_id, + }) + } } } @@ -125,18 +144,42 @@ impl PluginTableState { entrypoints.sort_by_key(|entrypoint| &entrypoint.entrypoint_name); - let mut entrypoints: Vec<_> = entrypoints - .iter() - .map(|entrypoint| { - Row::Entrypoint { - plugin_data: plugin_data.clone(), - plugin_id: plugin.plugin_id.clone(), - entrypoint_id: entrypoint.entrypoint_id.clone(), - } - }) - .collect(); + for entrypoint in entrypoints { + let entrypoint_row = Row::Entrypoint { + plugin_data: plugin_data.clone(), + plugin_id: plugin.plugin_id.clone(), + entrypoint_id: entrypoint.entrypoint_id.clone(), + }; - result.append(&mut entrypoints); + result.push(entrypoint_row); + + let show_generated_entrypoints = plugin_state + .generator_entrypoint_state + .get(&entrypoint.entrypoint_id) + .map(|data| data.show_entrypoints) + .unwrap_or(true); + + if show_generated_entrypoints { + let mut generated_entrypoints: Vec<_> = entrypoint + .generated_entrypoints + .iter() + .map(|(_, entrypoint)| entrypoint) + .collect(); + + generated_entrypoints.sort_by_key(|entrypoint| &entrypoint.entrypoint_name); + + for data in generated_entrypoints { + let generated_entrypoint_row = Row::GeneratedEntrypoint { + plugin_data: plugin_data.clone(), + plugin_id: plugin.plugin_id.clone(), + generator_entrypoint_id: entrypoint.entrypoint_id.clone(), + generated_entrypoint_id: data.entrypoint_id.clone(), + }; + + result.push(generated_entrypoint_row); + } + } + } } result @@ -180,10 +223,15 @@ enum Row { plugin_id: PluginId, entrypoint_id: EntrypointId, }, + GeneratedEntrypoint { + plugin_data: Rc>, + plugin_id: PluginId, + generator_entrypoint_id: EntrypointId, + generated_entrypoint_id: EntrypointId, + }, } enum ColumnKind { - ShowEntrypointsToggle, Name, Type, EnableToggle, @@ -204,7 +252,6 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo fn header(&'a self, _col_index: usize) -> Element<'a, PluginTableMsgIn> { match self.kind { - ColumnKind::ShowEntrypointsToggle => horizontal_space().into(), ColumnKind::Name => { container(text("Name")) .height(Length::Fixed(30.0)) @@ -228,8 +275,8 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo fn cell(&'a self, _col_index: usize, _row_index: usize, row_entry: &'a Self::Row) -> Element<'a, PluginTableMsgIn> { match self.kind { - ColumnKind::ShowEntrypointsToggle => { - match row_entry { + ColumnKind::Name => { + let toggle = match row_entry { Row::Plugin { plugin_data, plugin_id } => { let plugin_data = plugin_data.borrow(); let plugin_data = plugin_data.plugins_state.get(&plugin_id).unwrap(); @@ -246,16 +293,58 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo .on_press(PluginTableMsgIn::ToggleShowEntrypoints { plugin_id: plugin_id.clone(), }) - .width(Length::Fill) + .width(Length::Shrink) .height(Length::Fixed(40.0)) .padding(8.0) .class(ButtonStyle::TableRow) .into() } - Row::Entrypoint { .. } => horizontal_space().into(), - } - } - ColumnKind::Name => { + Row::GeneratedEntrypoint { .. } => horizontal_space().width(Length::Shrink).into(), + Row::Entrypoint { + plugin_data, + plugin_id, + entrypoint_id, + } => { + let plugin_data = plugin_data.borrow(); + let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); + let plugin_data = plugin_data.plugins_state.get(&plugin_id).unwrap(); + + let entrypoint = plugin.entrypoints.get(&entrypoint_id).unwrap(); + + if matches!(entrypoint.entrypoint_type, SettingsEntrypointType::EntrypointGenerator) { + let icon = if plugin_data + .generator_entrypoint_state + .get(&entrypoint_id) + .unwrap() + .show_entrypoints + { + Bootstrap::CaretDown + } else { + Bootstrap::CaretRight + }; + + let icon: Element<_> = value(icon).font(BOOTSTRAP_FONT).into(); + + let content = button(icon) + .on_press(PluginTableMsgIn::ToggleShowGeneratedEntrypoints { + plugin_id: plugin_id.clone(), + entrypoint_id: entrypoint_id.clone(), + }) + .width(Length::Shrink) + .height(Length::Fixed(40.0)) + .padding(8.0) + .class(ButtonStyle::TableRow) + .into(); + + let space: Element<_> = Space::with_width(Length::Fixed(10.0)).into(); + + row(vec![space, content]).into() + } else { + horizontal_space().width(Length::Shrink).into() + } + } + }; + let content: Element<_> = match row_entry { Row::Plugin { plugin_data, plugin_id } => { let plugin_data = plugin_data.borrow(); @@ -278,44 +367,80 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo .shaping(Shaping::Advanced) .into(); - let text: Element<_> = row(vec![Space::with_width(Length::Fixed(30.0)).into(), text]).into(); + let space: Element<_> = + if let SettingsEntrypointType::EntrypointGenerator = entrypoint.entrypoint_type { + Space::with_width(Length::Fixed(4.0)).into() + } else { + Space::with_width(Length::Fixed(45.0)).into() + }; + + let text: Element<_> = row(vec![space, text]).into(); + + container(text).align_y(Alignment::Center).into() + } + Row::GeneratedEntrypoint { + plugin_data, + plugin_id, + generator_entrypoint_id, + generated_entrypoint_id, + } => { + let plugin_data = plugin_data.borrow(); + let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); + let entrypoint = plugin.entrypoints.get(&generator_entrypoint_id).unwrap(); + let generated_entrypoint = + entrypoint.generated_entrypoints.get(&generated_entrypoint_id).unwrap(); + + let text: Element<_> = text(generated_entrypoint.entrypoint_name.to_string()) + .shaping(Shaping::Advanced) + .into(); + + let space: Element<_> = Space::with_width(Length::Fixed(65.0)).into(); + + let text: Element<_> = row(vec![space, text]).into(); container(text).align_y(Alignment::Center).into() } }; let msg = match &row_entry { - Row::Plugin { plugin_data, plugin_id } => { - let plugin_data = plugin_data.borrow(); - let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); - + Row::Plugin { plugin_id, .. } => { SelectedItem::Plugin { - plugin_id: plugin.plugin_id.clone(), + plugin_id: plugin_id.clone(), } } Row::Entrypoint { - plugin_data, entrypoint_id, plugin_id, + .. } => { - let plugin_data = plugin_data.borrow(); - let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); - let entrypoint = plugin.entrypoints.get(&entrypoint_id).unwrap(); - SelectedItem::Entrypoint { - plugin_id: plugin.plugin_id.clone(), - entrypoint_id: entrypoint.entrypoint_id.clone(), + plugin_id: plugin_id.clone(), + entrypoint_id: entrypoint_id.clone(), + } + } + Row::GeneratedEntrypoint { + plugin_id, + generator_entrypoint_id, + generated_entrypoint_id, + .. + } => { + SelectedItem::GeneratedEntrypoint { + plugin_id: plugin_id.clone(), + generator_entrypoint_id: generator_entrypoint_id.clone(), + generated_entrypoint_id: generated_entrypoint_id.clone(), } } }; - button(content) + let content = button(content) .class(ButtonStyle::TableRow) .on_press(PluginTableMsgIn::SelectItem(msg)) .width(Length::Fill) .height(Length::Fixed(40.0)) - .padding(8.0) - .into() + .padding(padding::all(8).left(0)) + .into(); + + row(vec![toggle, content]).into() } ColumnKind::Type => { let content: Element<_> = match row_entry { @@ -332,37 +457,43 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo let entrypoint_type = match entrypoint.entrypoint_type { SettingsEntrypointType::Command => "Command", SettingsEntrypointType::View => "View", - SettingsEntrypointType::InlineView => "Inline View", - SettingsEntrypointType::EntrypointGenerator => "Entrypoint Generator", + SettingsEntrypointType::InlineView => "Inline", + SettingsEntrypointType::EntrypointGenerator => "Generator", }; container(text(entrypoint_type.to_string())) .align_y(Alignment::Center) .into() } + Row::GeneratedEntrypoint { .. } => horizontal_space().into(), }; let msg = match &row_entry { - Row::Plugin { plugin_data, plugin_id } => { - let plugin_data = plugin_data.borrow(); - let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); - + Row::Plugin { plugin_id, .. } => { SelectedItem::Plugin { - plugin_id: plugin.plugin_id.clone(), + plugin_id: plugin_id.clone(), } } Row::Entrypoint { - plugin_data, entrypoint_id, plugin_id, + .. } => { - let plugin_data = plugin_data.borrow(); - let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); - let entrypoint = plugin.entrypoints.get(&entrypoint_id).unwrap(); - SelectedItem::Entrypoint { - plugin_id: plugin.plugin_id.clone(), - entrypoint_id: entrypoint.entrypoint_id.clone(), + plugin_id: plugin_id.clone(), + entrypoint_id: entrypoint_id.clone(), + } + } + Row::GeneratedEntrypoint { + plugin_id, + generated_entrypoint_id, + generator_entrypoint_id, + .. + } => { + SelectedItem::GeneratedEntrypoint { + plugin_id: plugin_id.clone(), + generator_entrypoint_id: generator_entrypoint_id.clone(), + generated_entrypoint_id: generated_entrypoint_id.clone(), } } }; @@ -399,6 +530,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo Some(entrypoint.entrypoint_id.clone()), ) } + Row::GeneratedEntrypoint { .. } => return horizontal_space().into(), }; let on_toggle = if show_checkbox { @@ -438,9 +570,8 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo fn width(&self) -> f32 { match self.kind { - ColumnKind::ShowEntrypointsToggle => 35.0, - ColumnKind::Name => 350.0, - ColumnKind::Type => 200.0, + ColumnKind::Name => 300.0, + ColumnKind::Type => 300.0, ColumnKind::EnableToggle => 75.0, } } diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 2615a0f..472f48d 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -888,9 +888,12 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { }) .collect(); - let entrypoint_generator_name = generator_names - .get(&item.generator_entrypoint_id) - .map(|name| name.to_string()); + let entrypoint_generator = generator_names.get(&item.generator_entrypoint_id).map(|name| { + ( + EntrypointId::from_string(item.generator_entrypoint_id), + name.to_string(), + ) + }); Ok(SearchIndexItem { entrypoint_type: SearchResultEntrypointType::Generated, @@ -900,7 +903,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { entrypoint_frecency, entrypoint_actions, entrypoint_accessories, - entrypoint_generator_name, + entrypoint_generator, }) }) .collect::>>()?; @@ -946,7 +949,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { Ok(Some(SearchIndexItem { entrypoint_type: SearchResultEntrypointType::Command, entrypoint_name: entrypoint.name, - entrypoint_generator_name: None, + entrypoint_generator: None, entrypoint_id, entrypoint_icon, entrypoint_frecency, @@ -958,7 +961,7 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl { Ok(Some(SearchIndexItem { entrypoint_type: SearchResultEntrypointType::View, entrypoint_name: entrypoint.name, - entrypoint_generator_name: None, + entrypoint_generator: None, entrypoint_id, entrypoint_icon, entrypoint_frecency, diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index b282ee2..1d193f2 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -23,6 +23,7 @@ use gauntlet_common::model::SearchResult; use gauntlet_common::model::SearchResultEntrypointType; use gauntlet_common::model::SettingsEntrypoint; use gauntlet_common::model::SettingsEntrypointType; +use gauntlet_common::model::SettingsGeneratedEntrypoint; use gauntlet_common::model::SettingsPlugin; use gauntlet_common::model::SettingsTheme; use gauntlet_common::model::UiPropertyValue; @@ -43,6 +44,7 @@ use gauntlet_plugin_runtime::JsPluginPermissionsMainSearchBar; use gauntlet_utils::channel::RequestSender; use include_dir::include_dir; use include_dir::Dir; +use itertools::Itertools; use tokio::runtime::Handle; use crate::model::ActionShortcutKey; @@ -196,7 +198,7 @@ impl ApplicationManager { entrypoint_id: EntrypointId, action_id: String, ) -> anyhow::Result<()> { - let data = self.search_index.plugin_entrypoint_actions(); + let data = self.search_index.plugin_entrypoint_data(); let Some(data) = data.get(&plugin_id) else { return Err(anyhow!("Unable to find plugin with id: {}", plugin_id)); @@ -213,9 +215,9 @@ impl ApplicationManager { let EntrypointDataView { entrypoint_name, + entrypoint_generator: _, entrypoint_type, actions, - .. } = entrypoint_data; match action_id.as_str() { @@ -381,12 +383,46 @@ impl ApplicationManager { } pub async fn plugins(&self) -> anyhow::Result> { + let mut generator_data: HashMap<_, _> = self + .search_index + .plugin_entrypoint_data() + .into_iter() + .flat_map(|(plugin_id, plugin_data)| { + let generator_data: HashMap<_, HashMap<_, _>> = plugin_data + .entrypoints + .into_iter() + .filter_map(|(entrypoint_id, entrypoint_data)| { + match &entrypoint_data.entrypoint_generator { + None => None, + Some((generator_entrypoint_id, _)) => { + Some((generator_entrypoint_id.clone(), entrypoint_id, entrypoint_data)) + } + } + }) + .chunk_by(|(generator_entrypoint_id, entrypoint_id, entrypoint_data)| { + generator_entrypoint_id.clone() + }) + .into_iter() + .map(|(generator_id, data)| { + let data: HashMap<_, _> = data + .map(|(_, entrypoint_id, entrypoint_data)| (entrypoint_id, entrypoint_data)) + .collect(); + ((plugin_id.clone(), generator_id.clone()), data) + }) + .collect(); + + generator_data + }) + .collect(); + let result = self .db_repository .list_plugins_and_entrypoints() .await? .into_iter() .map(|(plugin, entrypoints)| { + let plugin_id = PluginId::from_string(plugin.id); + let entrypoints = entrypoints .into_iter() .map(|entrypoint| { @@ -419,6 +455,19 @@ impl ApplicationManager { .into_iter() .map(|(key, value)| (key, plugin_preference_user_data_from_db(value))) .collect(), + generated_entrypoints: generator_data + .remove(&(plugin_id.clone(), entrypoint_id.clone())) + .unwrap_or_default() + .into_iter() + .map(|(entrypoint_id, data)| { + let generated_entrypoint = SettingsGeneratedEntrypoint { + entrypoint_id: entrypoint_id.clone(), + entrypoint_name: data.entrypoint_name, + }; + + (entrypoint_id, generated_entrypoint) + }) + .collect(), }; (entrypoint_id, entrypoint) @@ -426,7 +475,7 @@ impl ApplicationManager { .collect(); SettingsPlugin { - plugin_id: PluginId::from_string(plugin.id), + plugin_id, plugin_name: plugin.name, plugin_description: plugin.description, enabled: plugin.enabled, diff --git a/rust/server/src/search.rs b/rust/server/src/search.rs index 95219bb..df5d1d2 100644 --- a/rust/server/src/search.rs +++ b/rust/server/src/search.rs @@ -51,7 +51,7 @@ struct PluginData { struct EntrypointData { entrypoint_name: String, - entrypoint_generator_name: Option, + entrypoint_generator: Option<(EntrypointId, String)>, entrypoint_type: SearchResultEntrypointType, icon: Option, frecency: f64, @@ -79,7 +79,7 @@ pub struct PluginDataView { pub struct EntrypointDataView { pub entrypoint_name: String, - pub entrypoint_generator_name: Option, + pub entrypoint_generator: Option<(EntrypointId, String)>, pub entrypoint_type: SearchResultEntrypointType, pub actions: Vec, } @@ -95,7 +95,7 @@ pub struct EntrypointActionDataView { pub struct SearchIndexItem { pub entrypoint_type: SearchResultEntrypointType, pub entrypoint_name: String, - pub entrypoint_generator_name: Option, + pub entrypoint_generator: Option<(EntrypointId, String)>, pub entrypoint_id: EntrypointId, pub entrypoint_icon: Option, pub entrypoint_frecency: f64, @@ -228,7 +228,7 @@ impl SearchIndex { let data = EntrypointData { entrypoint_name: item.entrypoint_name, - entrypoint_generator_name: item.entrypoint_generator_name, + entrypoint_generator: item.entrypoint_generator, entrypoint_type: item.entrypoint_type, icon: item.entrypoint_icon, frecency: item.entrypoint_frecency, @@ -267,7 +267,7 @@ impl SearchIndex { Ok(()) } - pub fn plugin_entrypoint_actions(&self) -> HashMap { + pub fn plugin_entrypoint_data(&self) -> HashMap { let entrypoint_data = self.entrypoint_data.lock().expect("lock is poisoned"); entrypoint_data @@ -294,7 +294,7 @@ impl SearchIndex { entrypoint_id.clone(), EntrypointDataView { entrypoint_name: data.entrypoint_name.clone(), - entrypoint_generator_name: data.entrypoint_generator_name.clone(), + entrypoint_generator: data.entrypoint_generator.clone(), entrypoint_type: data.entrypoint_type.clone(), actions, }, @@ -425,7 +425,10 @@ impl SearchIndex { let result_item = SearchResult { entrypoint_type: entrypoint_data.entrypoint_type.clone(), entrypoint_name, - entrypoint_generator_name: entrypoint_data.entrypoint_generator_name.clone(), + entrypoint_generator_name: entrypoint_data + .entrypoint_generator + .as_ref() + .map(|(_, name)| name.clone()), entrypoint_id, entrypoint_icon: entrypoint_data.icon.clone(), plugin_name, diff --git a/schema/backend.proto b/schema/backend.proto index 1404a2f..1ea2351 100644 --- a/schema/backend.proto +++ b/schema/backend.proto @@ -210,8 +210,13 @@ message RpcEntrypoint { RpcEntrypointTypeSettings entrypoint_type = 5; map preferences = 6; map preferences_user_data = 7; + map generated_entrypoints = 8; } +message RpcGeneratedEntrypoint { + string entrypoint_id = 1; + string entrypoint_name = 2; +} message RpcEventRenderView { string entrypoint_id = 1; From a258bdf6743c1590bd52324992577f5a155f9aff Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 3 Apr 2025 19:54:01 +0200 Subject: [PATCH 413/540] Do not panic when global shortcut unregistration fails --- rust/client/src/ui/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 9e66585..3143d3a 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -2220,7 +2220,9 @@ fn assign_global_shortcut( let mut hotkey_guard = current_hotkey.lock().expect("lock is poisoned"); if let Some(current_hotkey) = *hotkey_guard { - global_hotkey_manager.unregister(current_hotkey)?; + if let Err(err) = global_hotkey_manager.unregister(current_hotkey) { + tracing::warn!("error occurred when unregistering global shortcut {:?}: {:?}", current_hotkey, err) + } } if let Some(shortcut) = shortcut { From 2bb69f8add17717445961cc1fd9132b22853ef30 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 3 Apr 2025 20:38:03 +0200 Subject: [PATCH 414/540] Fix formatting --- rust/client/src/ui/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 3143d3a..03e4853 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -2221,7 +2221,11 @@ fn assign_global_shortcut( if let Some(current_hotkey) = *hotkey_guard { if let Err(err) = global_hotkey_manager.unregister(current_hotkey) { - tracing::warn!("error occurred when unregistering global shortcut {:?}: {:?}", current_hotkey, err) + tracing::warn!( + "error occurred when unregistering global shortcut {:?}: {:?}", + current_hotkey, + err + ) } } From d8b6010ad1b02afdff10c1fd1b871e177817ec5b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:45:49 +0200 Subject: [PATCH 415/540] Global shortcuts for entrypoints --- README.md | 1 - rust/client/src/global_shortcut.rs | 2 +- rust/client/src/ui/mod.rs | 205 ++++++++++++--- rust/common/src/model.rs | 11 + rust/common/src/rpc/backend_api.rs | 88 ++++++- rust/common/src/rpc/backend_server.rs | 78 ++++++ rust/common/src/rpc/frontend_api.rs | 24 ++ .../src/components/shortcut_selector.rs | 98 +++---- .../src/theme/shortcut_selector.rs | 25 +- rust/management_client/src/ui.rs | 55 ++-- rust/management_client/src/views/general.rs | 13 +- rust/management_client/src/views/plugins.rs | 95 +++++-- .../src/views/plugins/table.rs | 109 +++++++- rust/scenario_runner/src/frontend_mock.rs | 1 + .../13_remove_old_global_shortcut.sql | 1 + rust/server/src/lib.rs | 19 +- rust/server/src/plugins/data_db_repository.rs | 242 ++++++++++-------- rust/server/src/plugins/mod.rs | 41 ++- rust/server/src/plugins/settings.rs | 222 +++++++++++++--- rust/server/src/rpc.rs | 18 ++ schema/backend.proto | 27 ++ 21 files changed, 1083 insertions(+), 292 deletions(-) create mode 100644 rust/server/db_migrations/13_remove_old_global_shortcut.sql diff --git a/README.md b/README.md index 33efdc7..e0b7428 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ Web-first cross-platform application launcher with React-based plugins > [!WARNING] > Launcher is being developed by single developer in their free time. > Changes may be few and far between. -> If you face any issues they will likely not get fixed unless they become a problem for that developer > > There will probably be breaking changes which will be documented in [changelog](CHANGELOG.md). diff --git a/rust/client/src/global_shortcut.rs b/rust/client/src/global_shortcut.rs index 7d253b1..e0ee697 100644 --- a/rust/client/src/global_shortcut.rs +++ b/rust/client/src/global_shortcut.rs @@ -17,7 +17,7 @@ pub fn register_listener(msg_sender: Sender) { if let global_hotkey::HotKeyState::Pressed = e.state() { handle.spawn(async move { - if let Err(err) = msg_sender.send(AppMsg::ToggleWindow).await { + if let Err(err) = msg_sender.send(AppMsg::HandleGlobalShortcut(e.id)).await { tracing::warn!(target = "rpc", "error occurred when receiving shortcut event {:?}", err) } }); diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 03e4853..5cff3b4 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -92,6 +92,7 @@ use iced::Subscription; use iced::Task; use iced_fonts::BOOTSTRAP_FONT_BYTES; use serde::Deserialize; +use serde_json::map::Entry; use tokio::runtime::Handle; use tokio::sync::oneshot; use tokio::sync::Mutex as TokioMutex; @@ -145,8 +146,9 @@ use crate::ui::widget_container::PluginWidgetContainer; pub struct AppModel { // logic backend_api: BackendForFrontendApi, - global_hotkey_manager: Arc>, - current_hotkey: Arc>>, + global_hotkey_manager: GlobalHotKeyManager, + current_global_hotkey: Option, + current_entrypoint_global_hotkeys: HashMap<(PluginId, EntrypointId), HotKey>, frontend_receiver: Arc>>, main_window_id: window::Id, focused: bool, @@ -254,6 +256,7 @@ pub enum AppMsg { ShowWindow, HideWindow, ToggleWindow, + HandleGlobalShortcut(u32), ToggleActionPanel { keyboard: bool, }, @@ -332,6 +335,12 @@ pub enum AppMsg { shortcut: Option, responder: Arc>>>, }, + SetGlobalEntrypointShortcut { + plugin_id: PluginId, + entrypoint_id: EntrypointId, + shortcut: Option, + responder: Arc>>>, + }, UpdateLoadingBar { plugin_id: PluginId, entrypoint_id: EntrypointId, @@ -357,6 +366,10 @@ pub enum AppMsg { X11ActiveWindowChanged { window: u32, }, + RunEntrypoint { + plugin_id: PluginId, + entrypoint_id: EntrypointId, + }, } #[cfg(target_os = "linux")] @@ -554,15 +567,30 @@ fn new( GauntletComplexTheme::set_global(theme.clone()); - let current_hotkey = Arc::new(StdMutex::new(None)); - let global_hotkey_manager = GlobalHotKeyManager::new().expect("unable to create global hot key manager"); - let assignment_result = assign_global_shortcut(&global_hotkey_manager, ¤t_hotkey, setup_data.global_shortcut); + // let current_global_hotkey = None; + // let global_assignment_result = anyhow::Ok(()); + let (current_global_hotkey, global_assignment_result) = + assign_global_shortcut(&global_hotkey_manager, None, setup_data.global_shortcut); - futures::executor::block_on( - backend_api.setup_response(assignment_result.map_err(|err| format!("{:#}", err)).err()), - ) + let mut global_entrypoint_assignment_results = HashMap::new(); + let mut current_entrypoint_global_hotkeys = HashMap::new(); + for ((plugin_id, entrypoint_id), shortcut) in setup_data.global_entrypoint_shortcuts { + let (global_hotkey, result) = assign_global_shortcut(&global_hotkey_manager, None, Some(shortcut)); + if let Some(global_hotkey) = global_hotkey { + current_entrypoint_global_hotkeys.insert((plugin_id.clone(), entrypoint_id.clone()), global_hotkey); + } + global_entrypoint_assignment_results.insert( + (plugin_id, entrypoint_id), + result.map_err(|err| format!("{:#}", err)).err(), + ); + } + + futures::executor::block_on(backend_api.setup_response( + global_assignment_result.map_err(|err| format!("{:#}", err)).err(), + global_entrypoint_assignment_results, + )) .expect("Unable to setup frontend"); let mut tasks = vec![font::load(BOOTSTRAP_FONT_BYTES).map(AppMsg::FontLoaded)]; @@ -689,8 +717,9 @@ fn new( AppModel { // logic backend_api, - global_hotkey_manager: Arc::new(StdRwLock::new(global_hotkey_manager)), - current_hotkey, + global_hotkey_manager, + current_global_hotkey, + current_entrypoint_global_hotkeys, frontend_receiver: Arc::new(TokioRwLock::new(frontend_receiver)), main_window_id, focused: false, @@ -1460,27 +1489,81 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { } } AppMsg::SetGlobalShortcut { shortcut, responder } => { - tracing::info!("Registering new global shortcut: {:?}", shortcut); - - let run = || { - let global_hotkey_manager = state.global_hotkey_manager.read().expect("lock is poisoned"); - - assign_global_shortcut(&global_hotkey_manager, &state.current_hotkey, shortcut) - }; - - // responder is not clone and send, and we need to consume it - // so we wrap it in arc mutex option let mut responder = responder .lock() .expect("lock is poisoned") .take() .expect("there should always be a responder here"); - match run() { + let (hotkey, error) = assign_global_shortcut( + &state.global_hotkey_manager, + state.current_global_hotkey, + shortcut.clone(), + ); + + state.current_global_hotkey = hotkey; + + match error { Ok(()) => { + tracing::info!("Successfully registered new global shortcut: {:?}", shortcut); responder.respond(UiResponseData::Nothing); } Err(err) => { + tracing::error!("Unable to register new global shortcut {:?}: {:?}", shortcut, err); + responder.respond(UiResponseData::Err(err)); + } + } + + Task::none() + } + AppMsg::SetGlobalEntrypointShortcut { + plugin_id, + entrypoint_id, + shortcut, + responder, + } => { + let mut responder = responder + .lock() + .expect("lock is poisoned") + .take() + .expect("there should always be a responder here"); + + let current_global_hotkey = state + .current_entrypoint_global_hotkeys + .get(&(plugin_id.clone(), entrypoint_id.clone())) + .cloned(); + + let (hotkey, error) = + assign_global_shortcut(&state.global_hotkey_manager, current_global_hotkey, shortcut.clone()); + + if let Some(hotkey) = hotkey { + state + .current_entrypoint_global_hotkeys + .insert((plugin_id.clone(), entrypoint_id.clone()), hotkey); + } else { + state + .current_entrypoint_global_hotkeys + .remove(&(plugin_id.clone(), entrypoint_id.clone())); + }; + + match error { + Ok(()) => { + tracing::info!( + "Successfully registered new global shortcut for plugin '{:?}' and entrypoint '{:?}' : {:?}", + plugin_id, + entrypoint_id, + shortcut + ); + responder.respond(UiResponseData::Nothing); + } + Err(err) => { + tracing::info!( + "Unable to register new global shortcut for plugin '{:?}' and entrypoint '{:?}' - {:?}: {:?}", + plugin_id, + entrypoint_id, + shortcut, + err + ); responder.respond(UiResponseData::Err(err)); } } @@ -1608,6 +1691,43 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { Task::none() } } + AppMsg::HandleGlobalShortcut(id) => { + if let Some(hotkey) = state.current_global_hotkey { + if hotkey.id == id { + return Task::done(AppMsg::ToggleWindow); + } + } + + let ids = state + .current_entrypoint_global_hotkeys + .iter() + .find(|(_, hotkey)| hotkey.id == id) + .map(|(ids, _)| ids); + + if let Some((plugin_id, entrypoint_id)) = ids { + return Task::done(AppMsg::RunEntrypoint { + plugin_id: plugin_id.clone(), + entrypoint_id: entrypoint_id.clone(), + }); + }; + + Task::none() + } + AppMsg::RunEntrypoint { + plugin_id, + entrypoint_id, + } => { + let mut backend_client = state.backend_api.clone(); + + Task::perform( + async move { + let result = backend_client.run_entrypoint(plugin_id, entrypoint_id).await?; + + Ok(result) + }, + |result| handle_backend_error(result, |action_shortcuts| AppMsg::Noop), + ) + } } } @@ -2214,13 +2334,11 @@ fn subscription(state: &AppModel) -> Subscription { fn assign_global_shortcut( global_hotkey_manager: &GlobalHotKeyManager, - current_hotkey: &Arc>>, - shortcut: Option, -) -> anyhow::Result<()> { - let mut hotkey_guard = current_hotkey.lock().expect("lock is poisoned"); - - if let Some(current_hotkey) = *hotkey_guard { - if let Err(err) = global_hotkey_manager.unregister(current_hotkey) { + current_hotkey: Option, + new_shortcut: Option, +) -> (Option, anyhow::Result<()>) { + if let Some(current_hotkey) = current_hotkey { + if let Err(err) = global_hotkey_manager.unregister(current_hotkey.clone()) { tracing::warn!( "error occurred when unregistering global shortcut {:?}: {:?}", current_hotkey, @@ -2229,22 +2347,15 @@ fn assign_global_shortcut( } } - if let Some(shortcut) = shortcut { - let hotkey = convert_physical_shortcut_to_hotkey(shortcut); - + if let Some(new_shortcut) = new_shortcut { + let hotkey = convert_physical_shortcut_to_hotkey(new_shortcut); match global_hotkey_manager.register(hotkey) { - Ok(()) => { - *hotkey_guard = Some(hotkey); - } - Err(err) => { - *hotkey_guard = None; - - Err(err)? - } + Ok(()) => (Some(hotkey), Ok(())), + Err(err) => (None, Err(anyhow!(err))), } + } else { + (None, Ok(())) } - - Ok(()) } impl AppModel { @@ -2959,6 +3070,18 @@ async fn request_loop( responder: Arc::new(Mutex::new(Some(responder))), } } + UiRequestData::SetGlobalEntrypointShortcut { + plugin_id, + entrypoint_id, + shortcut, + } => { + AppMsg::SetGlobalEntrypointShortcut { + plugin_id, + entrypoint_id, + shortcut, + responder: Arc::new(Mutex::new(Some(responder))), + } + } UiRequestData::UpdateLoadingBar { plugin_id, entrypoint_id, diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index a95ec26..2c6c2e6 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -217,6 +217,7 @@ pub struct UiSetupData { pub window_position_file: Option, pub theme: UiTheme, pub global_shortcut: Option, + pub global_entrypoint_shortcuts: HashMap<(PluginId, EntrypointId), PhysicalShortcut>, pub close_on_unfocus: bool, pub window_position_mode: WindowPositionMode, } @@ -272,6 +273,11 @@ pub enum UiRequestData { SetGlobalShortcut { shortcut: Option, }, + SetGlobalEntrypointShortcut { + plugin_id: PluginId, + entrypoint_id: EntrypointId, + shortcut: Option, + }, SetTheme { theme: UiTheme, }, @@ -361,6 +367,11 @@ pub enum BackendRequestData { InlineViewShortcuts, SetupResponse { global_shortcut_error: Option, + global_entrypoint_shortcuts_errors: HashMap<(PluginId, EntrypointId), Option>, + }, + RunEntrypoint { + plugin_id: PluginId, + entrypoint_id: EntrypointId, }, } diff --git a/rust/common/src/rpc/backend_api.rs b/rust/common/src/rpc/backend_api.rs index a006288..aa9359f 100644 --- a/rust/common/src/rpc/backend_api.rs +++ b/rust/common/src/rpc/backend_api.rs @@ -32,6 +32,7 @@ use crate::rpc::grpc::RpcDownloadPluginRequest; use crate::rpc::grpc::RpcDownloadStatus; use crate::rpc::grpc::RpcDownloadStatusRequest; use crate::rpc::grpc::RpcEntrypointTypeSettings; +use crate::rpc::grpc::RpcGetGlobalEntrypointShortcutRequest; use crate::rpc::grpc::RpcGetGlobalShortcutRequest; use crate::rpc::grpc::RpcGetThemeRequest; use crate::rpc::grpc::RpcGetWindowPositionModeRequest; @@ -41,6 +42,7 @@ use crate::rpc::grpc::RpcRemovePluginRequest; use crate::rpc::grpc::RpcRunActionRequest; use crate::rpc::grpc::RpcSaveLocalPluginRequest; use crate::rpc::grpc::RpcSetEntrypointStateRequest; +use crate::rpc::grpc::RpcSetGlobalEntrypointShortcutRequest; use crate::rpc::grpc::RpcSetGlobalShortcutRequest; use crate::rpc::grpc::RpcSetPluginStateRequest; use crate::rpc::grpc::RpcSetPreferenceValueRequest; @@ -79,6 +81,8 @@ pub struct BackendForFrontendApi { backend_sender: RequestSender, } +impl BackendForFrontendApi {} + impl BackendForFrontendApi { pub fn new(backend_sender: RequestSender) -> Self { Self { backend_sender } @@ -97,8 +101,12 @@ impl BackendForFrontendApi { pub async fn setup_response( &mut self, global_shortcut_error: Option, + global_entrypoint_shortcuts_errors: HashMap<(PluginId, EntrypointId), Option>, ) -> Result<(), BackendForFrontendApiError> { - let request = BackendRequestData::SetupResponse { global_shortcut_error }; + let request = BackendRequestData::SetupResponse { + global_shortcut_error, + global_entrypoint_shortcuts_errors, + }; let BackendResponseData::Nothing = self.backend_sender.send_receive(request).await? else { unreachable!() @@ -291,6 +299,23 @@ impl BackendForFrontendApi { Ok(shortcuts) } + + pub async fn run_entrypoint( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + ) -> Result<(), BackendForFrontendApiError> { + let request = BackendRequestData::RunEntrypoint { + plugin_id, + entrypoint_id, + }; + + let BackendResponseData::Nothing = self.backend_sender.send_receive(request).await? else { + unreachable!() + }; + + Ok(()) + } } #[derive(Error, Debug, Clone)] @@ -538,6 +563,67 @@ impl BackendApi { )) } + pub async fn set_global_entrypoint_shortcut( + &mut self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + shortcut: Option, + ) -> Result, BackendApiError> { + let request = RpcSetGlobalEntrypointShortcutRequest { + plugin_id: plugin_id.to_string(), + entrypoint_id: entrypoint_id.to_string(), + shortcut: shortcut.map(|shortcut| { + RpcShortcut { + physical_key: shortcut.physical_key.to_value(), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + } + }), + }; + + let error = self + .client + .set_global_entrypoint_shortcut(request) + .await? + .into_inner() + .error; + + Ok(error) + } + + pub async fn get_global_entrypoint_shortcuts( + &mut self, + ) -> Result)>, BackendApiError> { + let response = self + .client + .get_global_entrypoint_shortcut(Request::new(RpcGetGlobalEntrypointShortcutRequest::default())) + .await?; + + let response = response + .into_inner() + .shortcuts + .into_iter() + .map(|data| { + let plugin_id = PluginId::from_string(data.plugin_id); + let entrypoint_id = EntrypointId::from_string(data.entrypoint_id); + let shortcut = data.shortcut.unwrap(); + let shortcut = PhysicalShortcut { + physical_key: PhysicalKey::from_value(shortcut.physical_key), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + }; + + ((plugin_id, entrypoint_id), (shortcut, data.error)) + }) + .collect(); + + Ok(response) + } + pub async fn set_theme(&mut self, theme: SettingsTheme) -> Result<(), BackendApiError> { let theme = match theme { SettingsTheme::AutoDetect => "AutoDetect", diff --git a/rust/common/src/rpc/backend_server.rs b/rust/common/src/rpc/backend_server.rs index 9da185c..9553cc4 100644 --- a/rust/common/src/rpc/backend_server.rs +++ b/rust/common/src/rpc/backend_server.rs @@ -30,6 +30,9 @@ use crate::rpc::grpc::RpcDownloadStatusValue; use crate::rpc::grpc::RpcEntrypoint; use crate::rpc::grpc::RpcEntrypointTypeSettings; use crate::rpc::grpc::RpcGeneratedEntrypoint; +use crate::rpc::grpc::RpcGetGlobalEntrypointShortcut; +use crate::rpc::grpc::RpcGetGlobalEntrypointShortcutRequest; +use crate::rpc::grpc::RpcGetGlobalEntrypointShortcutResponse; use crate::rpc::grpc::RpcGetGlobalShortcutRequest; use crate::rpc::grpc::RpcGetGlobalShortcutResponse; use crate::rpc::grpc::RpcGetThemeRequest; @@ -49,6 +52,8 @@ use crate::rpc::grpc::RpcSaveLocalPluginRequest; use crate::rpc::grpc::RpcSaveLocalPluginResponse; use crate::rpc::grpc::RpcSetEntrypointStateRequest; use crate::rpc::grpc::RpcSetEntrypointStateResponse; +use crate::rpc::grpc::RpcSetGlobalEntrypointShortcutRequest; +use crate::rpc::grpc::RpcSetGlobalEntrypointShortcutResponse; use crate::rpc::grpc::RpcSetGlobalShortcutRequest; use crate::rpc::grpc::RpcSetGlobalShortcutResponse; use crate::rpc::grpc::RpcSetPluginStateRequest; @@ -128,6 +133,17 @@ pub trait BackendServer { async fn get_global_shortcut(&self) -> anyhow::Result<(Option, Option)>; + async fn set_global_entrypoint_shortcut( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + shortcut: Option, + ) -> anyhow::Result<()>; + + async fn get_global_entrypoint_shortcuts( + &self, + ) -> anyhow::Result)>>; + async fn set_theme(&self, theme: SettingsTheme) -> anyhow::Result<()>; async fn get_theme(&self) -> anyhow::Result; @@ -403,6 +419,68 @@ impl RpcBackend for RpcBackendServerImpl { })) } + async fn set_global_entrypoint_shortcut( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let plugin_id = request.plugin_id; + let entrypoint_id = request.entrypoint_id; + let shortcut = request.shortcut.map(|shortcut| { + PhysicalShortcut { + physical_key: PhysicalKey::from_value(shortcut.physical_key), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + } + }); + + let plugin_id = PluginId::from_string(plugin_id); + let entrypoint_id = EntrypointId::from_string(entrypoint_id); + + let error = self + .server + .set_global_entrypoint_shortcut(plugin_id, entrypoint_id, shortcut) + .await + .map_err(|err| format!("{:#}", err)) + .err(); + + Ok(Response::new(RpcSetGlobalEntrypointShortcutResponse { error })) + } + + async fn get_global_entrypoint_shortcut( + &self, + _request: Request, + ) -> Result, Status> { + let shortcuts = self + .server + .get_global_entrypoint_shortcuts() + .await + .map_err(|err| Status::internal(format!("{:#}", err)))?; + + let shortcuts = shortcuts + .into_iter() + .map(|((plugin_id, entrypoint_id), (shortcut, error))| { + RpcGetGlobalEntrypointShortcut { + plugin_id: plugin_id.to_string(), + entrypoint_id: entrypoint_id.to_string(), + shortcut: Some(RpcShortcut { + physical_key: shortcut.physical_key.to_value(), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + }), + error, + } + }) + .collect(); + + Ok(Response::new(RpcGetGlobalEntrypointShortcutResponse { shortcuts })) + } + async fn set_theme(&self, request: Request) -> Result, Status> { let theme = request.into_inner().theme; diff --git a/rust/common/src/rpc/frontend_api.rs b/rust/common/src/rpc/frontend_api.rs index 3c65ea2..975b83e 100644 --- a/rust/common/src/rpc/frontend_api.rs +++ b/rust/common/src/rpc/frontend_api.rs @@ -191,6 +191,30 @@ impl FrontendApi { } } + pub async fn set_global_entrypoint_shortcut( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + shortcut: Option, + ) -> anyhow::Result<()> { + let request = UiRequestData::SetGlobalEntrypointShortcut { + plugin_id, + entrypoint_id, + shortcut, + }; + + let data = self + .frontend_sender + .send_receive(request) + .await + .map_err(|err| anyhow!("error: {:?}", err))?; + + match data { + UiResponseData::Nothing => Ok(()), + UiResponseData::Err(err) => Err(err), + } + } + pub async fn set_theme(&self, theme: UiTheme) -> anyhow::Result<()> { let request = UiRequestData::SetTheme { theme }; diff --git a/rust/management_client/src/components/shortcut_selector.rs b/rust/management_client/src/components/shortcut_selector.rs index f46dcdc..d7e2488 100644 --- a/rust/management_client/src/components/shortcut_selector.rs +++ b/rust/management_client/src/components/shortcut_selector.rs @@ -1,6 +1,4 @@ -use gauntlet_common::model::EntrypointId; use gauntlet_common::model::PhysicalShortcut; -use gauntlet_common::model::PluginId; use gauntlet_common_ui::physical_key_model; use gauntlet_common_ui::shortcut_to_text; use iced::advanced::graphics::core::event; @@ -41,68 +39,56 @@ pub struct ShortcutData { pub error: Option, } -pub fn shortcut_selector<'a, 'b: 'a, 'c, Message: 'a, Id: 'a, F>( - shortcut_id: Id, +pub fn shortcut_selector<'a, 'b: 'a, 'c, Message: 'a, F>( current_shortcut: &'b ShortcutData, on_shortcut_captured: F, overlay_class: ::Class<'a>, + in_table: bool, ) -> Element<'a, Message> where - F: 'a + Fn(Id, Option) -> Message, - Id: Clone, + F: 'a + Fn(Option) -> Message, { Element::new(ShortcutSelector::new::( - shortcut_id, current_shortcut, on_shortcut_captured, overlay_class, + in_table, )) } -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum ShortcutId { - Global, - Entrypoint { - plugin_id: PluginId, - entrypoint_id: EntrypointId, - }, -} +pub struct ShortcutSelector<'a, 'b, Message> { + on_shortcut_captured: Box) -> Message + 'a>, -pub struct ShortcutSelector<'a, 'b, Message, Id> -where - Id: Clone, -{ - on_shortcut_captured: Box) -> Message + 'a>, - - shortcut_id: Id, current_shortcut: &'b ShortcutData, content: Element<'a, Message>, popup: Element<'a, Message>, overlay_class: ::Class<'a>, + in_table: bool, } -impl<'a, 'b, 'c, Message: 'a, Id> ShortcutSelector<'a, 'b, Message, Id> -where - Id: Clone, -{ +impl<'a, 'b, 'c, Message: 'a> ShortcutSelector<'a, 'b, Message> { pub fn new( - shortcut_id: Id, current_shortcut: &'b ShortcutData, on_shortcut_captured: F, overlay_class: ::Class<'a>, + in_table: bool, ) -> Self where - F: 'a + Fn(Id, Option) -> Message, + F: 'a + Fn(Option) -> Message, { - let content = render_shortcut(¤t_shortcut.shortcut); + let content = render_shortcut(¤t_shortcut.shortcut, in_table); - let content = container(content) + let mut content = container(content) .width(Length::Fill) .height(Length::Fill) - .center_x(Length::Fill) - .center_y(Length::Fill) - .into(); + .center_y(Length::Fill); + + if !in_table { + content = content.center_x(Length::Fill); + } + + let content = content.into(); let recording_text: Element<_> = text("Recording shortcut...").into(); @@ -126,12 +112,12 @@ where Self { on_shortcut_captured: Box::new(on_shortcut_captured), - shortcut_id, current_shortcut, content, popup, overlay_class, + in_table, } } } @@ -139,12 +125,10 @@ where #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] struct State { is_capturing: bool, + is_hovering: bool, } -impl<'a, 'b, Message: 'a, Id> Widget for ShortcutSelector<'a, 'b, Message, Id> -where - Id: Clone, -{ +impl<'a, 'b, Message: 'a> Widget for ShortcutSelector<'a, 'b, Message> { fn size(&self) -> Size { Size { width: Length::Fill, @@ -171,10 +155,19 @@ where let style = if state.is_capturing { Status::Capturing } else { - Status::Active + if state.is_hovering { + Status::Hovered + } else { + Status::Active + } }; - let style = Catalog::style(theme, &::default(), style); + let style = Catalog::style( + theme, + &::default(), + style, + self.in_table, + ); draw_background(renderer, &style, layout.bounds()); @@ -233,7 +226,7 @@ where keyboard::key::Code::Backspace if modifiers.is_empty() => { state.is_capturing = false; - let message = (self.on_shortcut_captured)(self.shortcut_id.clone(), None); + let message = (self.on_shortcut_captured)(None); shell.publish(message); event::Status::Ignored @@ -241,7 +234,7 @@ where keyboard::key::Code::Escape if modifiers.is_empty() => { state.is_capturing = false; - let message = (self.on_shortcut_captured)(self.shortcut_id.clone(), None); + let message = (self.on_shortcut_captured)(None); shell.publish(message); event::Status::Ignored @@ -252,10 +245,7 @@ where Some(shortcut) => { state.is_capturing = false; - let message = (self.on_shortcut_captured)( - self.shortcut_id.clone(), - Some(shortcut), - ); + let message = (self.on_shortcut_captured)(Some(shortcut)); shell.publish(message); event::Status::Captured @@ -286,6 +276,11 @@ where event::Status::Ignored } } + mouse::Event::CursorMoved { .. } => { + state.is_hovering = cursor.is_over(layout.bounds()); + + event::Status::Captured + } _ => event::Status::Ignored, } } @@ -347,6 +342,7 @@ where #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { Active, + Hovered, Capturing, } @@ -355,10 +351,10 @@ pub trait Catalog { fn default<'a>() -> Self::Class<'a>; - fn style(&self, class: &Self::Class<'_>, status: Status) -> container::Style; + fn style(&self, class: &Self::Class<'_>, status: Status, transparent_background: bool) -> container::Style; } -pub fn render_shortcut<'a, Message: 'a>(shortcut: &Option) -> Element<'a, Message> { +pub fn render_shortcut<'a, Message: 'a>(shortcut: &Option, in_table: bool) -> Element<'a, Message> { let mut content: Vec> = vec![]; if let Some(current_shortcut) = shortcut { @@ -382,9 +378,13 @@ pub fn render_shortcut<'a, Message: 'a>(shortcut: &Option) -> } content.push(key_name); + } else { + if in_table { + content.push(text("Record Shortcut").class(TextStyle::Subtitle).into()); + } } - let content: Element = row(content).spacing(8.0).into(); + let content: Element = row(content).padding(8.0).spacing(8.0).into(); content } diff --git a/rust/management_client/src/theme/shortcut_selector.rs b/rust/management_client/src/theme/shortcut_selector.rs index a5a6adc..68a227b 100644 --- a/rust/management_client/src/theme/shortcut_selector.rs +++ b/rust/management_client/src/theme/shortcut_selector.rs @@ -5,8 +5,10 @@ use crate::components::shortcut_selector; use crate::components::shortcut_selector::Status; use crate::theme::GauntletSettingsTheme; use crate::theme::BACKGROUND_DARKER; +use crate::theme::BACKGROUND_LIGHTER; use crate::theme::BUTTON_BORDER_RADIUS; use crate::theme::PRIMARY; +use crate::theme::TRANSPARENT; impl shortcut_selector::Catalog for GauntletSettingsTheme { type Class<'a> = (); @@ -15,11 +17,17 @@ impl shortcut_selector::Catalog for GauntletSettingsTheme { () } - fn style(&self, _class: &Self::Class<'_>, status: Status) -> Style { + fn style(&self, _class: &Self::Class<'_>, status: Status, transparent_background: bool) -> Style { + let background = if transparent_background { + TRANSPARENT.to_iced().into() + } else { + BACKGROUND_DARKER.to_iced().into() + }; + match status { Status::Active => { Style { - background: Some(BACKGROUND_DARKER.to_iced().into()), + background: Some(background), border: Border { radius: BUTTON_BORDER_RADIUS.into(), ..Default::default() @@ -29,7 +37,7 @@ impl shortcut_selector::Catalog for GauntletSettingsTheme { } Status::Capturing => { Style { - background: Some(BACKGROUND_DARKER.to_iced().into()), + background: Some(background), border: Border { radius: BUTTON_BORDER_RADIUS.into(), width: 2.0, @@ -38,6 +46,17 @@ impl shortcut_selector::Catalog for GauntletSettingsTheme { ..Default::default() } } + Status::Hovered => { + Style { + background: Some(background), + border: Border { + radius: BUTTON_BORDER_RADIUS.into(), + width: 2.0, + color: BACKGROUND_LIGHTER.to_iced(), + }, + ..Default::default() + } + } } } } diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index 79436ba..c311415 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::time::Duration; use gauntlet_common::model::DownloadStatus; +use gauntlet_common::model::EntrypointId; use gauntlet_common::model::PhysicalShortcut; use gauntlet_common::model::PluginId; use gauntlet_common::model::SettingsTheme; @@ -129,32 +130,35 @@ fn new() -> (ManagementAppModel, Task) { Task::batch([ font::load(BOOTSTRAP_FONT_BYTES).map(ManagementAppMsg::FontLoaded), Task::done(ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::FetchPlugins)), - Task::perform( - async { - match backend_api { - Some(backend_api) => Some(init_data(backend_api).await), - None => None, - } - }, - |shortcut| { - match shortcut { - None => ManagementAppMsg::General(ManagementAppGeneralMsgIn::Noop), - Some(init) => { - match init { - Ok(init) => { - ManagementAppMsg::General(ManagementAppGeneralMsgIn::InitSetting { + Task::future(async { + match backend_api { + Some(backend_api) => Some(init_data(backend_api).await), + None => None, + } + }) + .then(|init_data| { + match init_data { + None => Task::done(ManagementAppMsg::General(ManagementAppGeneralMsgIn::Noop)), + Some(init) => { + match init { + Ok(init) => { + Task::batch([ + Task::done(ManagementAppMsg::General(ManagementAppGeneralMsgIn::InitSetting { theme: init.theme, window_position_mode: init.window_position_mode, shortcut: init.global_shortcut, shortcut_error: init.global_shortcut_error, - }) - } - Err(err) => ManagementAppMsg::HandleBackendError(err), + })), + Task::done(ManagementAppMsg::Plugin(ManagementAppPluginMsgIn::InitSetting { + global_entrypoint_shortcuts: init.global_entrypoint_shortcuts, + })), + ]) } + Err(err) => Task::done(ManagementAppMsg::HandleBackendError(err)), } } - }, - ), + } + }), ]), ) } @@ -164,10 +168,12 @@ struct InitSettingsData { global_shortcut_error: Option, theme: SettingsTheme, window_position_mode: WindowPositionMode, + global_entrypoint_shortcuts: HashMap<(PluginId, EntrypointId), (PhysicalShortcut, Option)>, } async fn init_data(mut backend_api: BackendApi) -> Result { let (global_shortcut, global_shortcut_error) = backend_api.get_global_shortcut().await?; + let global_entrypoint_shortcuts = backend_api.get_global_entrypoint_shortcuts().await?; let theme = backend_api.get_theme().await?; @@ -176,6 +182,7 @@ async fn init_data(mut backend_api: BackendApi) -> Result Task), + ShortcutCaptured(Option), ThemeChanged(SettingsTheme), WindowPositionModeChanged(WindowPositionMode), HandleShortcutResponse { - id: ShortcutId, shortcut: Option, shortcut_error: Option, }, @@ -82,7 +80,7 @@ impl ManagementAppGeneralState { }; match message { - ManagementAppGeneralMsgIn::ShortcutCaptured(id, shortcut) => { + ManagementAppGeneralMsgIn::ShortcutCaptured(shortcut) => { let mut backend_api = backend_api.clone(); Task::perform( @@ -97,11 +95,9 @@ impl ManagementAppGeneralState { }, move |result| { let shortcut = shortcut.clone(); - let id = id.clone(); handle_backend_error(result, move |shortcut_error| { ManagementAppGeneralMsgOut::Inner(ManagementAppGeneralMsgIn::HandleShortcutResponse { - id, shortcut, shortcut_error, }) @@ -158,7 +154,6 @@ impl ManagementAppGeneralState { ) } ManagementAppGeneralMsgIn::HandleShortcutResponse { - id, shortcut, shortcut_error, } => { @@ -174,10 +169,10 @@ impl ManagementAppGeneralState { pub fn view(&self) -> Element { let global_shortcut_selector = shortcut_selector( - ShortcutId::Global, &self.current_shortcut, - move |id, shortcut| ManagementAppGeneralMsgIn::ShortcutCaptured(id, shortcut), + move |shortcut| ManagementAppGeneralMsgIn::ShortcutCaptured(shortcut), ContainerStyle::Box, + false, ); let global_shortcut_field: Element<_> = container(global_shortcut_selector) diff --git a/rust/management_client/src/views/plugins.rs b/rust/management_client/src/views/plugins.rs index ba3e620..3d67f7c 100644 --- a/rust/management_client/src/views/plugins.rs +++ b/rust/management_client/src/views/plugins.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::rc::Rc; use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::PhysicalShortcut; use gauntlet_common::model::PluginId; use gauntlet_common::model::PluginPreferenceUserData; use gauntlet_common::model::SettingsEntrypointType; @@ -48,10 +49,16 @@ mod table; #[derive(Debug, Clone)] pub enum ManagementAppPluginMsgIn { + InitSetting { + global_entrypoint_shortcuts: HashMap<(PluginId, EntrypointId), (PhysicalShortcut, Option)>, + }, PluginTableMsg(PluginTableMsgIn), PluginPreferenceMsg(PluginPreferencesMsg), FetchPlugins, - PluginsReloaded(HashMap), + PluginsReloaded( + HashMap, + HashMap<(PluginId, EntrypointId), (PhysicalShortcut, Option)>, + ), RemovePlugin { plugin_id: PluginId, }, @@ -80,6 +87,7 @@ pub struct ManagementAppPluginsState { plugin_data: Rc>, preference_user_data: HashMap<(PluginId, Option, String), PluginPreferenceUserDataState>, selected_item: SelectedItem, + global_entrypoint_shortcuts: HashMap<(PluginId, EntrypointId), (PhysicalShortcut, Option)>, } impl ManagementAppPluginsState { @@ -115,6 +123,7 @@ impl ManagementAppPluginsState { preference_user_data: HashMap::new(), selected_item: select_item, table_state: PluginTableState::new(), + global_entrypoint_shortcuts: HashMap::new(), } } @@ -125,6 +134,13 @@ impl ManagementAppPluginsState { }; match message { + ManagementAppPluginMsgIn::InitSetting { + global_entrypoint_shortcuts, + } => { + self.global_entrypoint_shortcuts = global_entrypoint_shortcuts; + + Task::none() + } ManagementAppPluginMsgIn::PluginTableMsg(message) => { self.table_state.update(message).then(move |msg| { match msg { @@ -136,13 +152,16 @@ impl ManagementAppPluginsState { backend_client.set_plugin_state(plugin_id, enabled).await?; let plugins = backend_client.plugins().await?; + let global_entrypoint_shortcuts = + backend_client.get_global_entrypoint_shortcuts().await?; - Ok(plugins) + Ok((plugins, global_entrypoint_shortcuts)) }, |result| { - handle_backend_error(result, |plugins| { + handle_backend_error(result, |(plugins, global_entrypoint_shortcuts)| { ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded( plugins, + global_entrypoint_shortcuts, )) }) }, @@ -162,13 +181,16 @@ impl ManagementAppPluginsState { .await?; let plugins = backend_client.plugins().await?; + let global_entrypoint_shortcuts = + backend_client.get_global_entrypoint_shortcuts().await?; - Ok(plugins) + Ok((plugins, global_entrypoint_shortcuts)) }, |result| { - handle_backend_error(result, |plugins| { + handle_backend_error(result, |(plugins, global_entrypoint_shortcuts)| { ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded( plugins, + global_entrypoint_shortcuts, )) }) }, @@ -195,6 +217,31 @@ impl ManagementAppPluginsState { }, )) } + PluginTableMsgOut::ShortcutCaptured(plugin_id, entrypoint_id, shortcut) => { + let mut backend_client = backend_api.clone(); + + Task::perform( + async move { + backend_client + .set_global_entrypoint_shortcut(plugin_id, entrypoint_id, shortcut) + .await?; + + let plugins = backend_client.plugins().await?; + let global_entrypoint_shortcuts = + backend_client.get_global_entrypoint_shortcuts().await?; + + Ok((plugins, global_entrypoint_shortcuts)) + }, + |result| { + handle_backend_error(result, |(plugins, global_entrypoint_shortcuts)| { + ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded( + plugins, + global_entrypoint_shortcuts, + )) + }) + }, + ) + } } }) } @@ -207,7 +254,7 @@ impl ManagementAppPluginsState { plugin_data.plugins.clone() }; - self.apply_plugin_fetch(plugins); + self.apply_plugin_fetch(plugins, self.global_entrypoint_shortcuts.clone()); Task::none() } @@ -228,7 +275,7 @@ impl ManagementAppPluginsState { plugin_data.plugins.clone() }; - self.apply_plugin_fetch(plugins); + self.apply_plugin_fetch(plugins, self.global_entrypoint_shortcuts.clone()); Task::none() } @@ -270,18 +317,22 @@ impl ManagementAppPluginsState { Task::perform( async move { let plugins = backend_api.plugins().await?; + let global_entrypoint_shortcuts = backend_api.get_global_entrypoint_shortcuts().await?; - Ok(plugins) + Ok((plugins, global_entrypoint_shortcuts)) }, |result| { - handle_backend_error(result, |plugins| { - ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded(plugins)) + handle_backend_error(result, |(plugins, global_entrypoint_shortcuts)| { + ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded( + plugins, + global_entrypoint_shortcuts, + )) }) }, ) } - ManagementAppPluginMsgIn::PluginsReloaded(plugins) => { - self.apply_plugin_fetch(plugins); + ManagementAppPluginMsgIn::PluginsReloaded(plugins, shortcuts) => { + self.apply_plugin_fetch(plugins, shortcuts); Task::none() } @@ -295,12 +346,16 @@ impl ManagementAppPluginsState { backend_client.remove_plugin(plugin_id).await?; let plugins = backend_client.plugins().await?; + let global_entrypoint_shortcuts = backend_client.get_global_entrypoint_shortcuts().await?; - Ok(plugins) + Ok((plugins, global_entrypoint_shortcuts)) }, |result| { - handle_backend_error(result, |plugins| { - ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded(plugins)) + handle_backend_error(result, |(plugins, global_entrypoint_shortcuts)| { + ManagementAppPluginMsgOut::Inner(ManagementAppPluginMsgIn::PluginsReloaded( + plugins, + global_entrypoint_shortcuts, + )) }) }, ) @@ -319,7 +374,13 @@ impl ManagementAppPluginsState { } } - fn apply_plugin_fetch(&mut self, plugins: HashMap) { + fn apply_plugin_fetch( + &mut self, + plugins: HashMap, + global_entrypoint_shortcuts: HashMap<(PluginId, EntrypointId), (PhysicalShortcut, Option)>, + ) { + self.global_entrypoint_shortcuts = global_entrypoint_shortcuts.clone(); + self.preference_user_data = plugins .iter() .map(|(plugin_id, plugin)| { @@ -395,7 +456,7 @@ impl ManagementAppPluginsState { plugin_refs.sort_by_key(|(plugin, _)| &plugin.plugin_name); self.table_state - .apply_plugin_reload(self.plugin_data.clone(), plugin_refs) + .apply_plugin_reload(self.plugin_data.clone(), plugin_refs, global_entrypoint_shortcuts) } pub fn view(&self) -> Element { diff --git a/rust/management_client/src/views/plugins/table.rs b/rust/management_client/src/views/plugins/table.rs index c2c2bee..aa325fe 100644 --- a/rust/management_client/src/views/plugins/table.rs +++ b/rust/management_client/src/views/plugins/table.rs @@ -1,7 +1,9 @@ use std::cell::RefCell; +use std::collections::HashMap; use std::rc::Rc; use gauntlet_common::model::EntrypointId; +use gauntlet_common::model::PhysicalShortcut; use gauntlet_common::model::PluginId; use gauntlet_common::model::SettingsEntrypointType; use gauntlet_common::model::SettingsPlugin; @@ -25,7 +27,10 @@ use iced_fonts::Bootstrap; use iced_fonts::BOOTSTRAP_FONT; use iced_table::table; +use crate::components::shortcut_selector::shortcut_selector; +use crate::components::shortcut_selector::ShortcutData; use crate::theme::button::ButtonStyle; +use crate::theme::container::ContainerStyle; use crate::theme::Element; use crate::theme::GauntletSettingsTheme; use crate::views::plugins::PluginDataContainer; @@ -44,6 +49,7 @@ pub enum PluginTableMsgIn { plugin_id: PluginId, entrypoint_id: EntrypointId, }, + ShortcutCaptured(PluginId, EntrypointId, Option), } pub enum PluginTableMsgOut { @@ -64,6 +70,7 @@ pub enum PluginTableMsgOut { plugin_id: PluginId, entrypoint_id: EntrypointId, }, + ShortcutCaptured(PluginId, EntrypointId, Option), } pub struct PluginTableState { @@ -79,6 +86,7 @@ impl PluginTableState { columns: vec![ Column::new(ColumnKind::Name), Column::new(ColumnKind::Type), + Column::new(ColumnKind::Shortcut), Column::new(ColumnKind::EnableToggle), ], rows: vec![], @@ -121,6 +129,9 @@ impl PluginTableState { entrypoint_id, }) } + PluginTableMsgIn::ShortcutCaptured(plugin_id, entrypoint_id, shortcut) => { + Task::done(PluginTableMsgOut::ShortcutCaptured(plugin_id, entrypoint_id, shortcut)) + } } } @@ -128,6 +139,7 @@ impl PluginTableState { &mut self, plugin_data: Rc>, plugin_refs: Vec<(&SettingsPlugin, &SettingsPluginData)>, + global_entrypoint_shortcuts: HashMap<(PluginId, EntrypointId), (PhysicalShortcut, Option)>, ) { self.rows = plugin_refs .iter() @@ -145,10 +157,16 @@ impl PluginTableState { entrypoints.sort_by_key(|entrypoint| &entrypoint.entrypoint_name); for entrypoint in entrypoints { + let global_entrypoint_shortcut = global_entrypoint_shortcuts + .get(&(plugin.plugin_id.clone(), entrypoint.entrypoint_id.clone())); + let shortcut = global_entrypoint_shortcut.map(|(shortcut, _)| shortcut).cloned(); + let error = global_entrypoint_shortcut.map(|(_, error)| error).cloned().flatten(); + let entrypoint_row = Row::Entrypoint { plugin_data: plugin_data.clone(), plugin_id: plugin.plugin_id.clone(), entrypoint_id: entrypoint.entrypoint_id.clone(), + shortcut_data: ShortcutData { shortcut, error }, }; result.push(entrypoint_row); @@ -169,11 +187,17 @@ impl PluginTableState { generated_entrypoints.sort_by_key(|entrypoint| &entrypoint.entrypoint_name); for data in generated_entrypoints { + let global_entrypoint_shortcut = global_entrypoint_shortcuts + .get(&(plugin.plugin_id.clone(), data.entrypoint_id.clone())); + let shortcut = global_entrypoint_shortcut.map(|(shortcut, _)| shortcut).cloned(); + let error = global_entrypoint_shortcut.map(|(_, error)| error).cloned().flatten(); + let generated_entrypoint_row = Row::GeneratedEntrypoint { plugin_data: plugin_data.clone(), plugin_id: plugin.plugin_id.clone(), generator_entrypoint_id: entrypoint.entrypoint_id.clone(), generated_entrypoint_id: data.entrypoint_id.clone(), + shortcut_data: ShortcutData { shortcut, error }, }; result.push(generated_entrypoint_row); @@ -222,18 +246,21 @@ enum Row { plugin_data: Rc>, plugin_id: PluginId, entrypoint_id: EntrypointId, + shortcut_data: ShortcutData, }, GeneratedEntrypoint { plugin_data: Rc>, plugin_id: PluginId, generator_entrypoint_id: EntrypointId, generated_entrypoint_id: EntrypointId, + shortcut_data: ShortcutData, }, } enum ColumnKind { Name, Type, + Shortcut, EnableToggle, } @@ -256,6 +283,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo container(text("Name")) .height(Length::Fixed(30.0)) .align_y(Alignment::Center) + .padding(padding::left(8.0)) .into() } ColumnKind::Type => { @@ -264,6 +292,12 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo .align_y(Alignment::Center) .into() } + ColumnKind::Shortcut => { + container(text("Shortcut")) + .height(Length::Fixed(30.0)) + .align_y(Alignment::Center) + .into() + } ColumnKind::EnableToggle => { container(text("Enabled")) .height(Length::Fixed(30.0)) @@ -304,6 +338,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo plugin_data, plugin_id, entrypoint_id, + .. } => { let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); @@ -358,6 +393,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo plugin_data, plugin_id, entrypoint_id, + .. } => { let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); @@ -383,6 +419,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo plugin_id, generator_entrypoint_id, generated_entrypoint_id, + .. } => { let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); @@ -444,11 +481,12 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo } ColumnKind::Type => { let content: Element<_> = match row_entry { - Row::Plugin { .. } => horizontal_space().into(), + Row::Plugin { .. } => container(text("Plugin")).align_y(Alignment::Center).into(), Row::Entrypoint { plugin_data, plugin_id, entrypoint_id, + .. } => { let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); @@ -465,7 +503,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo .align_y(Alignment::Center) .into() } - Row::GeneratedEntrypoint { .. } => horizontal_space().into(), + Row::GeneratedEntrypoint { .. } => container(text("Generated")).align_y(Alignment::Center).into(), }; let msg = match &row_entry { @@ -506,6 +544,69 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo .padding(8.0) .into() } + ColumnKind::Shortcut => { + match row_entry { + Row::Plugin { .. } => horizontal_space().into(), + Row::Entrypoint { + plugin_data, + plugin_id, + entrypoint_id, + shortcut_data, + } => { + let plugin_data = plugin_data.borrow(); + let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); + let entrypoint = plugin.entrypoints.get(&entrypoint_id).unwrap(); + + if let SettingsEntrypointType::View | SettingsEntrypointType::Command = + entrypoint.entrypoint_type + { + let shortcut_selector = shortcut_selector( + shortcut_data, + move |shortcut| { + PluginTableMsgIn::ShortcutCaptured( + plugin_id.clone(), + entrypoint_id.clone(), + shortcut, + ) + }, + ContainerStyle::Box, + true, + ); + + container(shortcut_selector) + .height(Length::Fixed(40.0)) + .width(Length::Fill) + .into() + } else { + horizontal_space().into() + } + } + Row::GeneratedEntrypoint { + plugin_id, + generated_entrypoint_id, + shortcut_data, + .. + } => { + let shortcut_selector = shortcut_selector( + shortcut_data, + move |shortcut| { + PluginTableMsgIn::ShortcutCaptured( + plugin_id.clone(), + generated_entrypoint_id.clone(), + shortcut, + ) + }, + ContainerStyle::Box, + true, + ); + + container(shortcut_selector) + .height(Length::Fixed(40.0)) + .width(Length::Fill) + .into() + } + } + } ColumnKind::EnableToggle => { let (enabled, show_checkbox, plugin_id, entrypoint_id) = match &row_entry { Row::Plugin { plugin_data, plugin_id } => { @@ -518,6 +619,7 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo plugin_data, entrypoint_id, plugin_id, + .. } => { let plugin_data = plugin_data.borrow(); let plugin = plugin_data.plugins.get(&plugin_id).unwrap(); @@ -571,7 +673,8 @@ impl<'a> table::Column<'a, PluginTableMsgIn, GauntletSettingsTheme, Renderer> fo fn width(&self) -> f32 { match self.kind { ColumnKind::Name => 300.0, - ColumnKind::Type => 300.0, + ColumnKind::Type => 100.0, + ColumnKind::Shortcut => 200.0, ColumnKind::EnableToggle => 75.0, } } diff --git a/rust/scenario_runner/src/frontend_mock.rs b/rust/scenario_runner/src/frontend_mock.rs index e3d2d19..8923324 100644 --- a/rust/scenario_runner/src/frontend_mock.rs +++ b/rust/scenario_runner/src/frontend_mock.rs @@ -163,6 +163,7 @@ async fn request_loop( unreachable!() } UiRequestData::SetGlobalShortcut { .. } + | UiRequestData::SetGlobalEntrypointShortcut { .. } | UiRequestData::SetWindowPositionMode { .. } | UiRequestData::RequestSearchResultUpdate => { // noop diff --git a/rust/server/db_migrations/13_remove_old_global_shortcut.sql b/rust/server/db_migrations/13_remove_old_global_shortcut.sql new file mode 100644 index 0000000..c90ac04 --- /dev/null +++ b/rust/server/db_migrations/13_remove_old_global_shortcut.sql @@ -0,0 +1 @@ +ALTER TABLE settings_data DROP COLUMN global_shortcut; diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index 842e724..d70cfb0 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -256,8 +256,13 @@ async fn handle_request( BackendResponseData::SetupData { data } } - BackendRequestData::SetupResponse { global_shortcut_error } => { - application_manager.setup_response(global_shortcut_error).await?; + BackendRequestData::SetupResponse { + global_shortcut_error, + global_entrypoint_shortcuts_errors, + } => { + application_manager + .setup_response(global_shortcut_error, global_entrypoint_shortcuts_errors) + .await?; BackendResponseData::Nothing } @@ -359,6 +364,16 @@ async fn handle_request( BackendResponseData::InlineViewShortcuts { shortcuts } } + BackendRequestData::RunEntrypoint { + plugin_id, + entrypoint_id, + } => { + application_manager + .run_action(plugin_id, entrypoint_id, ":primary".to_string()) + .await?; + + BackendResponseData::Nothing + } }; Ok(response_data) diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 9d1144d..c91388c 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -8,6 +8,7 @@ use futures::future::join_all; use futures::StreamExt; use futures::TryStreamExt; use gauntlet_common::dirs::Dirs; +use gauntlet_common::model::EntrypointId; use gauntlet_common::model::PhysicalKey; use gauntlet_common::model::PhysicalShortcut; use gauntlet_common::model::PluginId; @@ -215,31 +216,73 @@ pub struct DbPluginActionUserData { #[derive(sqlx::FromRow)] struct DbSettingsDataContainer { - #[sqlx(json)] - pub global_shortcut: DbSettingsGlobalShortcutData, // separate field because legacy // #[sqlx(json)] // https://github.com/launchbadge/sqlx/issues/2849 pub settings: Option>, } #[derive(Debug, Deserialize, Serialize)] -pub struct DbSettingsGlobalShortcutData { +pub struct DbSettingsShortcut { pub physical_key: String, pub modifier_shift: bool, pub modifier_control: bool, pub modifier_alt: bool, pub modifier_meta: bool, - #[serde(default)] - pub unset: bool, - #[serde(default)] +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DbSettingsGlobalShortcutData { + pub shortcut: DbSettingsShortcut, pub error: Option, } -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] +pub struct DbSettingsGlobalEntrypointShortcutData { + pub plugin_id: String, + pub entrypoint_id: String, + pub shortcut: DbSettingsGlobalShortcutData, +} + +#[derive(Debug, Deserialize, Serialize)] pub struct DbSettings { // none means auto-detect pub theme: Option, - // none is static + // none is static mode pub window_position_mode: Option, + // none is unset, if whole settings object is unset, it is likely a first start and default shortcut will be used + pub global_shortcut: Option, + pub global_entrypoint_shortcuts: Option>, +} + +impl Default for DbSettings { + fn default() -> Self { + let default_global_shortcut = if cfg!(target_os = "windows") { + DbSettingsShortcut { + physical_key: PhysicalKey::Space.to_value(), + modifier_shift: false, + modifier_control: false, + modifier_alt: true, + modifier_meta: false, + } + } else { + DbSettingsShortcut { + physical_key: PhysicalKey::Space.to_value(), + modifier_shift: false, + modifier_control: false, + modifier_alt: false, + modifier_meta: true, + } + }; + + DbSettings { + theme: None, + window_position_mode: None, + global_shortcut: Some(DbSettingsGlobalShortcutData { + shortcut: default_global_shortcut, + error: None, + }), + global_entrypoint_shortcuts: None, + } + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -358,17 +401,28 @@ impl DataDbRepository { .await .context("Unable to open database connection")?; - // TODO backup before migration? up to 5 backups? - MIGRATOR.run(&pool).await.context("Unable apply database migration")?; - let db_repository = Self { pool }; + let _ = db_repository.migrate_global_shortcut_to_settings().await; + + db_repository.run_migrations().await?; + db_repository.apply_uuid_default_value().await?; db_repository.remove_legacy_bundled_plugins().await?; Ok(db_repository) } + async fn run_migrations(&self) -> anyhow::Result<()> { + // TODO backup before migration? up to 5 backups? + MIGRATOR + .run(&self.pool) + .await + .context("Unable apply database migration")?; + + Ok(()) + } + async fn apply_uuid_default_value(&self) -> anyhow::Result<()> { // language=SQLite let mut stream = self.pool.fetch(sqlx::query("SELECT id FROM plugin WHERE uuid IS NULL")); @@ -411,6 +465,71 @@ impl DataDbRepository { Ok(()) } + async fn migrate_global_shortcut_to_settings(&self) -> anyhow::Result<()> { + #[derive(sqlx::FromRow)] + struct DbSettingsDataOldContainer { + #[sqlx(json)] + pub global_shortcut: DbSettingsGlobalShortcutOldData, + pub settings: Option>, + } + + #[derive(Debug, Deserialize, Serialize)] + pub struct DbSettingsGlobalShortcutOldData { + pub physical_key: String, + pub modifier_shift: bool, + pub modifier_control: bool, + pub modifier_alt: bool, + pub modifier_meta: bool, + #[serde(default)] + pub unset: bool, + #[serde(default)] + pub error: Option, + } + + // language=SQLite + let mut data = sqlx::query_as::<_, DbSettingsDataOldContainer>("SELECT * FROM settings_data") + .fetch_one(&self.pool) + .await?; + + let shortcut_data = &data.global_shortcut; + + let shortcut = if shortcut_data.unset { + None + } else { + Some(DbSettingsGlobalShortcutData { + shortcut: DbSettingsShortcut { + physical_key: shortcut_data.physical_key.clone(), + modifier_shift: shortcut_data.modifier_shift, + modifier_control: shortcut_data.modifier_control, + modifier_alt: shortcut_data.modifier_alt, + modifier_meta: shortcut_data.modifier_meta, + }, + error: None, + }) + }; + + if let Some(settings) = &mut data.settings { + settings.global_shortcut = shortcut; + } + + // language=SQLite + let sql = r#" + INSERT INTO settings_data (id, global_shortcut, settings) + VALUES(?1, ?2, ?3) + ON CONFLICT (id) + DO UPDATE SET settings = ?3 + "#; + + sqlx::query(sql) + .bind(SETTINGS_DATA_ID) + .bind(Json(data.global_shortcut)) + .bind(data.settings) + .execute(&self.pool) + .await?; + + Ok(()) + } + pub async fn list_plugins(&self) -> anyhow::Result> { // language=SQLite let plugins = sqlx::query_as::<_, DbReadPlugin>("SELECT * FROM plugin") @@ -932,85 +1051,9 @@ impl DataDbRepository { Ok(()) } - pub async fn set_global_shortcut( - &self, - shortcut: Option, - error: Option, - ) -> anyhow::Result<()> { - // language=SQLite - let sql = r#" - INSERT INTO settings_data (id, global_shortcut) - VALUES(?1, ?2) - ON CONFLICT (id) - DO UPDATE SET global_shortcut = ?2 - "#; - - let shortcut_data = match shortcut { - None => { - DbSettingsGlobalShortcutData { - physical_key: "".to_string(), - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: false, - unset: true, - error, - } - } - Some(shortcut) => { - DbSettingsGlobalShortcutData { - physical_key: shortcut.physical_key.to_value(), - modifier_shift: shortcut.modifier_shift, - modifier_control: shortcut.modifier_control, - modifier_alt: shortcut.modifier_alt, - modifier_meta: shortcut.modifier_meta, - unset: false, - error, - } - } - }; - - sqlx::query(sql) - .bind(SETTINGS_DATA_ID) - .bind(Json(shortcut_data)) - .execute(&self.pool) - .await?; - - Ok(()) - } - - pub async fn get_global_shortcut(&self) -> anyhow::Result, Option)>> { - // language=SQLite - let data = sqlx::query_as::<_, DbSettingsDataContainer>("SELECT * FROM settings_data") - .fetch_optional(&self.pool) - .await; - - match data { - Ok(Some(data)) => { - let shortcut_data = data.global_shortcut; - - let shortcut = if shortcut_data.unset { - None - } else { - Some(PhysicalShortcut { - physical_key: PhysicalKey::from_value(shortcut_data.physical_key), - modifier_shift: shortcut_data.modifier_shift, - modifier_control: shortcut_data.modifier_control, - modifier_alt: shortcut_data.modifier_alt, - modifier_meta: shortcut_data.modifier_meta, - }) - }; - - Ok(Some((shortcut, shortcut_data.error))) - } - Ok(None) => Ok(None), - Err(err) => Err(anyhow!("Unable to get global shortcut from db: {:?}", err)), - } - } - pub async fn get_settings(&self) -> anyhow::Result { // language=SQLite - let settings = sqlx::query_as::<_, DbSettingsDataContainer>("SELECT * FROM settings_data") + let settings = sqlx::query_as::<_, DbSettingsDataContainer>("SELECT settings FROM settings_data") .fetch_optional(&self.pool) .await?; @@ -1022,31 +1065,14 @@ impl DataDbRepository { pub async fn set_settings(&self, value: DbSettings) -> anyhow::Result<()> { // language=SQLite let sql = r#" - INSERT INTO settings_data (id, global_shortcut, settings) - VALUES(?1, ?2, ?3) + INSERT INTO settings_data (id, settings) + VALUES(?1, ?2) ON CONFLICT (id) - DO UPDATE SET settings = ?3 + DO UPDATE SET settings = ?2 "#; - let data = sqlx::query_as::<_, DbSettingsDataContainer>("SELECT * FROM settings_data") - .fetch_optional(&self.pool) - .await? - .unwrap_or(DbSettingsDataContainer { - global_shortcut: DbSettingsGlobalShortcutData { - physical_key: "".to_string(), - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: false, - unset: true, - error: None, - }, - settings: None, - }); - sqlx::query(sql) .bind(SETTINGS_DATA_ID) - .bind(Json(data.global_shortcut)) .bind(Json(value)) .execute(&self.pool) .await?; diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 1d193f2..7929b09 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -143,7 +143,14 @@ impl ApplicationManager { pub async fn setup_data(&self) -> anyhow::Result { let window_position_file = self.dirs.window_position(); let theme = self.settings.effective_theme().await?; - let global_shortcut = self.settings.effective_global_shortcut().await?; + let global_shortcut = self.settings.global_shortcut().await?.map(|(shortcut, _)| shortcut); + let global_entrypoint_shortcuts = self + .settings + .global_entrypoint_shortcuts() + .await? + .into_iter() + .map(|((plugin_id, entrypoint_id), (shortcut, _))| ((plugin_id, entrypoint_id), shortcut)) + .collect(); let window_position_mode = self.settings.window_position_mode_setting().await?; let close_on_unfocus = self.config_reader.close_on_unfocus(); @@ -151,14 +158,25 @@ impl ApplicationManager { window_position_file: Some(window_position_file), theme, global_shortcut, + global_entrypoint_shortcuts, close_on_unfocus, window_position_mode, }) } - pub async fn setup_response(&self, global_shortcut_error: Option) -> anyhow::Result<()> { + pub async fn setup_response( + &self, + global_shortcut_error: Option, + global_entrypoint_shortcuts_errors: HashMap<(PluginId, EntrypointId), Option>, + ) -> anyhow::Result<()> { self.settings.set_global_shortcut_error(global_shortcut_error).await?; + for ((plugin_id, entrypoint_id), error) in global_entrypoint_shortcuts_errors { + self.settings + .set_global_entrypoint_shortcut_error(plugin_id, entrypoint_id, error) + .await?; + } + Ok(()) } @@ -571,10 +589,27 @@ impl ApplicationManager { self.settings.set_global_shortcut(shortcut).await } - pub async fn get_global_shortcut(&self) -> anyhow::Result, Option)>> { + pub async fn get_global_shortcut(&self) -> anyhow::Result)>> { self.settings.global_shortcut().await } + pub async fn set_global_entrypoint_shortcut( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + shortcut: Option, + ) -> anyhow::Result<()> { + self.settings + .set_global_entrypoint_shortcut(plugin_id, entrypoint_id, shortcut) + .await + } + + pub async fn get_global_entrypoint_shortcut( + &self, + ) -> anyhow::Result)>> { + self.settings.global_entrypoint_shortcuts().await + } + pub async fn set_theme(&self, theme: SettingsTheme) -> anyhow::Result<()> { self.settings.set_theme_setting(theme).await } diff --git a/rust/server/src/plugins/settings.rs b/rust/server/src/plugins/settings.rs index 1ec4d33..7ec00b2 100644 --- a/rust/server/src/plugins/settings.rs +++ b/rust/server/src/plugins/settings.rs @@ -1,16 +1,22 @@ -use std::env::consts::OS; +use std::collections::hash_map::Entry; +use std::collections::HashMap; use anyhow::anyhow; use dark_light::Mode; use gauntlet_common::dirs::Dirs; +use gauntlet_common::model::EntrypointId; use gauntlet_common::model::PhysicalKey; use gauntlet_common::model::PhysicalShortcut; +use gauntlet_common::model::PluginId; use gauntlet_common::model::SettingsTheme; use gauntlet_common::model::UiTheme; use gauntlet_common::model::WindowPositionMode; use gauntlet_common::rpc::frontend_api::FrontendApi; use crate::plugins::data_db_repository::DataDbRepository; +use crate::plugins::data_db_repository::DbSettingsGlobalEntrypointShortcutData; +use crate::plugins::data_db_repository::DbSettingsGlobalShortcutData; +use crate::plugins::data_db_repository::DbSettingsShortcut; use crate::plugins::data_db_repository::DbTheme; use crate::plugins::data_db_repository::DbWindowPositionMode; use crate::plugins::theme::read_theme_file; @@ -33,33 +39,22 @@ impl Settings { }) } - pub async fn effective_global_shortcut(&self) -> anyhow::Result> { - match self.global_shortcut().await? { - None => { - if cfg!(target_os = "windows") { - Ok(Some(PhysicalShortcut { - physical_key: PhysicalKey::Space, - modifier_shift: false, - modifier_control: false, - modifier_alt: true, - modifier_meta: false, - })) - } else { - Ok(Some(PhysicalShortcut { - physical_key: PhysicalKey::Space, - modifier_shift: false, - modifier_control: false, - modifier_alt: false, - modifier_meta: true, - })) - } - } - Some((shortcut, _)) => Ok(shortcut), - } - } + pub async fn global_shortcut(&self) -> anyhow::Result)>> { + let settings = self.repository.get_settings().await?; - pub async fn global_shortcut(&self) -> anyhow::Result, Option)>> { - self.repository.get_global_shortcut().await + let data = settings.global_shortcut.map(|data| { + let shortcut = PhysicalShortcut { + physical_key: PhysicalKey::from_value(data.shortcut.physical_key), + modifier_shift: data.shortcut.modifier_shift, + modifier_control: data.shortcut.modifier_control, + modifier_alt: data.shortcut.modifier_alt, + modifier_meta: data.shortcut.modifier_meta, + }; + + (shortcut, data.error) + }); + + Ok(data) } pub async fn set_global_shortcut(&self, shortcut: Option) -> anyhow::Result<()> { @@ -67,19 +62,182 @@ impl Settings { let db_err = err.as_ref().map_err(|err| format!("{:#}", err)).err(); - self.repository.set_global_shortcut(shortcut, db_err).await?; + let mut settings = self.repository.get_settings().await?; + + settings.global_shortcut = shortcut.map(|shortcut| { + DbSettingsGlobalShortcutData { + shortcut: DbSettingsShortcut { + physical_key: shortcut.physical_key.to_value(), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + }, + error: db_err, + } + }); + + self.repository.set_settings(settings).await?; err } pub async fn set_global_shortcut_error(&self, error: Option) -> anyhow::Result<()> { - match self.repository.get_global_shortcut().await? { - None => {} - Some((shortcut, _)) => { - self.repository.set_global_shortcut(shortcut, error).await?; + let mut settings = self.repository.get_settings().await?; + + if let Some(data) = &mut settings.global_shortcut { + data.error = error + } + + self.repository.set_settings(settings).await?; + + Ok(()) + } + + pub async fn global_entrypoint_shortcuts( + &self, + ) -> anyhow::Result)>> { + let settings = self.repository.get_settings().await?; + let data: HashMap<_, _> = settings + .global_entrypoint_shortcuts + .unwrap_or_default() + .into_iter() + .map(|data| { + let shortcut = data.shortcut.shortcut; + let error = data.shortcut.error; + + let shortcut = PhysicalShortcut { + physical_key: PhysicalKey::from_value(shortcut.physical_key), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + }; + + ( + ( + PluginId::from_string(data.plugin_id), + EntrypointId::from_string(data.entrypoint_id), + ), + (shortcut, error), + ) + }) + .collect(); + + Ok(data) + } + + pub async fn set_global_entrypoint_shortcut( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + shortcut: Option, + ) -> anyhow::Result<()> { + let err = self + .frontend_api + .set_global_entrypoint_shortcut(plugin_id.clone(), entrypoint_id.clone(), shortcut.clone()) + .await; + + let db_err = err.as_ref().map_err(|err| format!("{:#}", err)).err(); + + let mut settings = self.repository.get_settings().await?; + + let mut shortcuts: HashMap<_, _> = settings + .global_entrypoint_shortcuts + .unwrap_or_default() + .into_iter() + .map(|data| { + ( + ( + PluginId::from_string(data.plugin_id), + EntrypointId::from_string(data.entrypoint_id), + ), + data.shortcut, + ) + }) + .collect(); + + match shortcut { + None => { + shortcuts.remove(&(plugin_id, entrypoint_id)); } + Some(shortcut) => { + shortcuts.insert( + (plugin_id, entrypoint_id), + DbSettingsGlobalShortcutData { + shortcut: DbSettingsShortcut { + physical_key: shortcut.physical_key.to_value(), + modifier_shift: shortcut.modifier_shift, + modifier_control: shortcut.modifier_control, + modifier_alt: shortcut.modifier_alt, + modifier_meta: shortcut.modifier_meta, + }, + error: db_err, + }, + ); + } + } + + let global_entrypoint_shortcuts = shortcuts + .into_iter() + .map(|((plugin_id, entrypoint_id), data)| { + DbSettingsGlobalEntrypointShortcutData { + plugin_id: plugin_id.to_string(), + entrypoint_id: entrypoint_id.to_string(), + shortcut: data, + } + }) + .collect(); + + settings.global_entrypoint_shortcuts = Some(global_entrypoint_shortcuts); + + self.repository.set_settings(settings).await?; + + err + } + + pub async fn set_global_entrypoint_shortcut_error( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + error: Option, + ) -> anyhow::Result<()> { + let mut settings = self.repository.get_settings().await?; + + let mut shortcuts: HashMap<_, _> = settings + .global_entrypoint_shortcuts + .unwrap_or_default() + .into_iter() + .map(|data| { + ( + ( + PluginId::from_string(data.plugin_id), + EntrypointId::from_string(data.entrypoint_id), + ), + data.shortcut, + ) + }) + .collect(); + + if let Some(data) = shortcuts.get_mut(&(plugin_id, entrypoint_id)) { + data.error = error; }; + let global_entrypoint_shortcuts = shortcuts + .into_iter() + .map(|((plugin_id, entrypoint_id), data)| { + DbSettingsGlobalEntrypointShortcutData { + plugin_id: plugin_id.to_string(), + entrypoint_id: entrypoint_id.to_string(), + shortcut: data, + } + }) + .collect(); + + settings.global_entrypoint_shortcuts = Some(global_entrypoint_shortcuts); + + self.repository.set_settings(settings).await?; + Ok(()) } diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index 179c635..e5390cf 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -126,11 +126,29 @@ impl BackendServer for BackendServerImpl { .application_manager .get_global_shortcut() .await? + .map(|(shortcut, error)| (Some(shortcut), error)) .unwrap_or((None, None)); Ok(result) } + async fn set_global_entrypoint_shortcut( + &self, + plugin_id: PluginId, + entrypoint_id: EntrypointId, + shortcut: Option, + ) -> anyhow::Result<()> { + self.application_manager + .set_global_entrypoint_shortcut(plugin_id, entrypoint_id, shortcut) + .await + } + + async fn get_global_entrypoint_shortcuts( + &self, + ) -> anyhow::Result)>> { + self.application_manager.get_global_entrypoint_shortcut().await + } + async fn set_theme(&self, theme: SettingsTheme) -> anyhow::Result<()> { self.application_manager.set_theme(theme).await } diff --git a/schema/backend.proto b/schema/backend.proto index 1ea2351..ab8154a 100644 --- a/schema/backend.proto +++ b/schema/backend.proto @@ -23,6 +23,9 @@ service RpcBackend { rpc SetGlobalShortcut (RpcSetGlobalShortcutRequest) returns (RpcSetGlobalShortcutResponse); rpc GetGlobalShortcut (RpcGetGlobalShortcutRequest) returns (RpcGetGlobalShortcutResponse); + rpc SetGlobalEntrypointShortcut (RpcSetGlobalEntrypointShortcutRequest) returns (RpcSetGlobalEntrypointShortcutResponse); + rpc GetGlobalEntrypointShortcut (RpcGetGlobalEntrypointShortcutRequest) returns (RpcGetGlobalEntrypointShortcutResponse); + rpc SetTheme (RpcSetThemeRequest) returns (RpcSetThemeResponse); rpc GetTheme (RpcGetThemeRequest) returns (RpcGetThemeResponse); @@ -107,6 +110,30 @@ message RpcGetGlobalShortcutResponse { optional string error = 2; } +message RpcSetGlobalEntrypointShortcutRequest { + string plugin_id = 1; + string entrypoint_id = 2; + optional RpcShortcut shortcut = 3; +} + +message RpcSetGlobalEntrypointShortcutResponse { + optional string error = 1; +} + +message RpcGetGlobalEntrypointShortcutRequest { +} + +message RpcGetGlobalEntrypointShortcutResponse { + repeated RpcGetGlobalEntrypointShortcut shortcuts = 1; +} + +message RpcGetGlobalEntrypointShortcut { + string plugin_id = 1; + string entrypoint_id = 2; + RpcShortcut shortcut = 3; + optional string error = 4; +} + message RpcSetThemeRequest { string theme = 1; } From 02f9d366fe8d75dfdd03548b6bbfe38bc565c012 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:32:41 +0200 Subject: [PATCH 416/540] Pin wix toolkit version in github actions --- .github/workflows/setup-windows.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/setup-windows.yaml b/.github/workflows/setup-windows.yaml index e71f519..7da7923 100644 --- a/.github/workflows/setup-windows.yaml +++ b/.github/workflows/setup-windows.yaml @@ -27,7 +27,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - run: choco install protoc - - run: dotnet tool install --global wix + - run: dotnet tool install --global wix --version 5.0.2 - run: wix extension add -g WixToolset.Util.wixext/5.0.2 - uses: Swatinem/rust-cache@v2 From 6fafe236584d442bf0816135345eab561c8d52a4 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Fri, 11 Apr 2025 18:02:10 +0200 Subject: [PATCH 417/540] Fix window sometimes not being hidden on x11 --- rust/client/src/ui/mod.rs | 13 +++++++++++-- rust/client/src/ui/platform/linux.rs | 8 ++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 5cff3b4..6bee303 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -365,6 +365,7 @@ pub enum AppMsg { #[cfg(target_os = "linux")] X11ActiveWindowChanged { window: u32, + wm_name: Option, }, RunEntrypoint { plugin_id: PluginId, @@ -1683,10 +1684,18 @@ fn update(state: &mut AppModel, message: AppMsg) -> Task { ]) } #[cfg(target_os = "linux")] - AppMsg::X11ActiveWindowChanged { window } => { + AppMsg::X11ActiveWindowChanged { window, wm_name } => { if state.x11_active_window != Some(window) { state.x11_active_window = Some(window); - Task::done(AppMsg::HideWindow) + if let Some(wm_name) = &wm_name { + if wm_name != "gauntlet" { + Task::done(AppMsg::HideWindow) + } else { + Task::none() + } + } else { + Task::none() + } } else { Task::none() } diff --git a/rust/client/src/ui/platform/linux.rs b/rust/client/src/ui/platform/linux.rs index ea66203..0948866 100644 --- a/rust/client/src/ui/platform/linux.rs +++ b/rust/client/src/ui/platform/linux.rs @@ -31,14 +31,10 @@ pub fn listen_on_x11_active_window_change(sender: Sender, handle: Handle continue; }; - if let Ok(wm_name) = fetch_app_wm_name(&conn, window) { - if wm_name == "gauntlet" { - continue; - } - } + let wm_name = fetch_app_wm_name(&conn, window).ok(); let mut sender = sender.clone(); - handle.spawn(async move { sender.send(AppMsg::X11ActiveWindowChanged { window }).await }); + handle.spawn(async move { sender.send(AppMsg::X11ActiveWindowChanged { window, wm_name }).await }); } } } From 8439f96430011a01418f751f415efb1a8fd01d35 Mon Sep 17 00:00:00 2001 From: Benno <107504783+BennoCrafter@users.noreply.github.com> Date: Sun, 13 Apr 2025 18:29:29 +0200 Subject: [PATCH 418/540] fix: ensure window state resets correctly (#65) --- rust/client/src/ui/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 6bee303..a957b28 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -2441,6 +2441,10 @@ impl AppModel { GlobalState::PendingPluginView { .. } => {} } + if self.pending_window_state_reset { + commands.push(self.reset_window_state()); + } + Task::batch(commands) } @@ -2473,11 +2477,7 @@ impl AppModel { window::change_mode(self.main_window_id, Mode::Windowed), ]); - let mut commands = vec![open_task]; - - if self.pending_window_state_reset { - commands.push(self.reset_window_state()); - } + let commands = vec![open_task]; Task::batch(commands) } From 4a99160427a59cee3f60699512cc7f3760b28474 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 13 Apr 2025 18:59:48 +0200 Subject: [PATCH 419/540] Fix plugin view being empty if the window is closed by toggling global shortcut --- rust/client/src/ui/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index a957b28..9d78025 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -2430,7 +2430,9 @@ impl AppModel { plugin_view_data: PluginViewData { plugin_id, .. }, .. } => { - commands.push(self.close_plugin_view(plugin_id.clone())); + if reset_state { + commands.push(self.close_plugin_view(plugin_id.clone())); + } } GlobalState::MainView { focused_search_result, .. @@ -2477,9 +2479,7 @@ impl AppModel { window::change_mode(self.main_window_id, Mode::Windowed), ]); - let commands = vec![open_task]; - - Task::batch(commands) + open_task } fn reset_window_state(&mut self) -> Task { From bdda05f579ff1cc028cb59ddbfd84d3b2bf2d154 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 13 Apr 2025 19:32:35 +0200 Subject: [PATCH 420/540] Update CHANGELOG.md --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8090ebf..449f537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,30 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +- Entrypoints can now be assigned a global shortcut in Settings UI + - On Linux X11 there is a known bug which will cause a crash and prevent server startup if attempted to register global shortcut that is already registered by another application +- Generated shortcuts are now listed in the Settings UI + +### Plugins +- Added authors field to Plugin Manifest + - `gauntlet.authors.*.name` - String + - `gauntlet.authors.*.uris` - List of strings. URIs that identify the author. Can be a link to social media page or an email (if email it should begin with `mailto:` schema) +- Added `$schema` field to Plugin Manifest which takes URL to the JSON Schema file + - Some editors use it to validate the content of the file + - Currently, the schema file is located inside the repository at path `https://raw.githubusercontent.com/project-gauntlet/gauntlet/refs/heads/main/docs/schema/plugin_manifest.schema.json` but at some point this will change + +### Themes +- Tweaked window border color of macOS Dark theme on non-macos platforms not as bright + +### UI/UX improvements +- Reworked shortcut selector widget in Setting UI + +### Fixes +- Fixed window sometimes not being hidden on X11 +- Main view search list is now refreshed when window being hidden instead of when it is shown, to avoid the list being changed after window was opened +- Fixed plugin view being empty if the window is closed by toggling global shortcut +- Fixes plugin view not being opened properly if it is created using global shortcut or cli command + ## [17] - 2025-03-15 - Fixed crash when typing/clicking fast in React-created plugin ui - Fixed events sometimes overwriting other parallel events when typing/clicking fast in React-created plugin ui From 25a10d573ce4508e5a7efc6c9dde01349a157509 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 13 Apr 2025 19:36:16 +0200 Subject: [PATCH 421/540] Bump nix version --- nix/overlay.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/overlay.nix b/nix/overlay.nix index 37cf374..401929d 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -5,7 +5,7 @@ }: { flake.overlays.default = final: _: let # These must be updated following the instructions in ./nix/README.md when dependencies are updated or the version bumped - version = "v17"; + version = "v18"; npmDepsHash = "sha256-BOnKpFS0ofIZbmXcyEzHzHgvbgeg3QUXnC79BwKO6P8="; RUSTY_V8_ARCHIVE = fetchRustyV8 "130.0.2" { aarch64-darwin = "sha256-aWZ/4Q4Wttx37xOdBmTCPGP+eYGhr4CM1UkYq8pC7Qs="; From 7143ed2471a1ab5d86226ad0e5bd23dadf9989dd Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 13 Apr 2025 17:41:09 +0000 Subject: [PATCH 422/540] Prepare for v18 release --- CHANGELOG.md | 2 ++ VERSION | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 449f537..4a66fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [Unreleased] +## [18] - 2025-04-13 + - Entrypoints can now be assigned a global shortcut in Settings UI - On Linux X11 there is a known bug which will cause a crash and prevent server startup if attempted to register global shortcut that is already registered by another application - Generated shortcuts are now listed in the Settings UI diff --git a/VERSION b/VERSION index 8e2afd3..25bf17f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -17 \ No newline at end of file +18 \ No newline at end of file From 61eca05f4253e71ae6769db7f60f8866ddcc55a0 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 13 Apr 2025 19:52:35 +0200 Subject: [PATCH 423/540] Add contributions to CHANGELOG.md --- CHANGELOG.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a66fe1..1d21e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,11 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git ## [18] - 2025-04-13 -- Entrypoints can now be assigned a global shortcut in Settings UI +### General + +- Entrypoints can now be run or opened using global shortcut set Settings UI - On Linux X11 there is a known bug which will cause a crash and prevent server startup if attempted to register global shortcut that is already registered by another application -- Generated shortcuts are now listed in the Settings UI +- Generated Entrypoints are now listed in the Settings UI ### Plugins - Added authors field to Plugin Manifest @@ -24,16 +26,16 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git - Currently, the schema file is located inside the repository at path `https://raw.githubusercontent.com/project-gauntlet/gauntlet/refs/heads/main/docs/schema/plugin_manifest.schema.json` but at some point this will change ### Themes -- Tweaked window border color of macOS Dark theme on non-macos platforms not as bright +- Tweaked window border color of macOS Dark theme on non-macos platforms to be not as bright ### UI/UX improvements - Reworked shortcut selector widget in Setting UI ### Fixes - Fixed window sometimes not being hidden on X11 -- Main view search list is now refreshed when window being hidden instead of when it is shown, to avoid the list being changed after window was opened +- Main view search list is now refreshed when window being hidden instead of when it is shown, to avoid the list being changed after window was opened (contributed by @BennoCrafter) - Fixed plugin view being empty if the window is closed by toggling global shortcut -- Fixes plugin view not being opened properly if it is created using global shortcut or cli command +- Fixes plugin view not being opened properly if it is created using global shortcut or cli command (contributed by @BennoCrafter) ## [17] - 2025-03-15 - Fixed crash when typing/clicking fast in React-created plugin ui From b8f07d502818668df246d266da58c4e98d87dfd4 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 20 Apr 2025 16:15:08 +0200 Subject: [PATCH 424/540] Unify Vec usage to ArrayBuffer in js. Fixes icon in EntrypointGenerator requiring number[] --- bundled_plugins/gauntlet/gauntlet.toml | 2 +- .../gauntlet/src/window/shared.tsx | 7 ++-- js/api/src/helpers.ts | 8 ++-- js/react_renderer/src/renderer.ts | 6 +-- js/typings/index.d.ts | 8 ++-- rust/plugin_runtime/src/assets.rs | 2 +- rust/plugin_runtime/src/clipboard.rs | 22 ++++------ rust/plugin_runtime/src/model.rs | 40 ++++++++++++++++++- .../src/plugins/applications.rs | 11 ++--- .../src/plugins/applications/linux/mod.rs | 4 +- .../src/plugins/applications/macos.rs | 5 ++- .../src/plugins/applications/windows.rs | 5 ++- rust/plugin_runtime/src/search.rs | 20 +++++++++- 13 files changed, 94 insertions(+), 46 deletions(-) diff --git a/bundled_plugins/gauntlet/gauntlet.toml b/bundled_plugins/gauntlet/gauntlet.toml index 28bc064..1859162 100644 --- a/bundled_plugins/gauntlet/gauntlet.toml +++ b/bundled_plugins/gauntlet/gauntlet.toml @@ -29,7 +29,7 @@ enum_values = [ [[entrypoint]] id = 'windows' -name = 'All Open Windows' +name = 'Open Windows' path = 'src/windows.tsx' type = 'view' description = 'Show all open windows' diff --git a/bundled_plugins/gauntlet/src/window/shared.tsx b/bundled_plugins/gauntlet/src/window/shared.tsx index 136d25d..eecb927 100644 --- a/bundled_plugins/gauntlet/src/window/shared.tsx +++ b/bundled_plugins/gauntlet/src/window/shared.tsx @@ -18,7 +18,6 @@ export function ListOfWindows({ windows, focus }: { windows: OpenWindowData[], f onAction={(id: string | undefined) => { if (id) { focus(id) - console.log("focus: " + id) return { close: true } } }} @@ -123,9 +122,9 @@ export function applicationAccessories(id: string, experimentalWindowTracking: b if (appWindows.length == 0) { return [] } else if (appWindows.length == 1) { - return [{ text: "1 window open" }] + return [{ text: "1 window" }] } else if (appWindows.length > 1) { - return [{ text: `${appWindows.length} windows open` }] + return [{ text: `${appWindows.length} windows` }] } else { return [] } @@ -182,4 +181,4 @@ export function openLinuxApplication(appId: string) { return () => { linux_open_application(appId) } -} \ No newline at end of file +} diff --git a/js/api/src/helpers.ts b/js/api/src/helpers.ts index fa9c3b4..b662267 100644 --- a/js/api/src/helpers.ts +++ b/js/api/src/helpers.ts @@ -84,7 +84,7 @@ export const Clipboard: Clipboard = { } if (data.png_data) { - result["image/png"] = new Uint8Array(data.png_data).buffer; + result["image/png"] = data.png_data; } return result @@ -92,18 +92,18 @@ export const Clipboard: Clipboard = { readText: async function (): Promise { return await clipboard_read_text() }, - write: async function (data: { "text/plain"?: string | undefined; "image/png"?: ArrayBuffer | undefined; }): Promise { + write: async function (data: { "text/plain"?: string | undefined; "image/png"?: Uint8Array | undefined; }): Promise { const text_data = data["text/plain"]; const png_data = data["image/png"]; - const write_data: { text_data?: string, png_data?: number[] } = {}; + const write_data: { text_data?: string, png_data?: ArrayBuffer } = {}; if (text_data) { write_data.text_data = text_data; } if (png_data) { - write_data.png_data = Array.from(new Uint8Array(png_data)); + write_data.png_data = png_data; } return await clipboard_write(write_data) diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index 0ac0984..6e8fb7a 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -117,13 +117,11 @@ export function useGauntletContext() { } export async function getAssetData(path: string): Promise { - const vecU8 = await asset_data(path); - return new Uint8Array(vecU8).buffer; // FIXME move array creation into rust if possible + return await asset_data(path); } export function getAssetDataSync(path: string): ArrayBuffer { - const vecU8 = asset_data_blocking(path); - return new Uint8Array(vecU8).buffer; + return asset_data_blocking(path); } export function getPluginPreferences(): Record { diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index 4f8652a..66c0542 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -219,8 +219,8 @@ declare module "ext:core/ops" { function op_log_error(target: string, message: string): void; function op_component_model(): Record; - function asset_data(path: string): Promise; - function asset_data_blocking(path: string): number[]; + function asset_data(path: string): Promise; + function asset_data_blocking(path: string): ArrayBuffer; function op_inline_view_entrypoint_id(): string | null; function op_entrypoint_names(): Record; @@ -246,9 +246,9 @@ declare module "ext:core/ops" { function fetch_action_id_for_shortcut(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean): Promise; - function clipboard_read(): Promise<{ text_data?: string, png_data?: number[] }>; + function clipboard_read(): Promise<{ text_data?: string, png_data?: ArrayBuffer }>; function clipboard_read_text(): Promise; - function clipboard_write(data: { text_data?: string, png_data?: number[] }): Promise; + function clipboard_write(data: { text_data?: string, png_data?: ArrayBuffer }): Promise; function clipboard_write_text(data: string): Promise; function clipboard_clear(): Promise; diff --git a/rust/plugin_runtime/src/assets.rs b/rust/plugin_runtime/src/assets.rs index cfa8aee..89a2090 100644 --- a/rust/plugin_runtime/src/assets.rs +++ b/rust/plugin_runtime/src/assets.rs @@ -43,6 +43,6 @@ pub fn asset_data_blocking(state: Rc>, #[string] path: String) outer_handle.block_on(async { let data = api.get_asset_data(&path).await?; - Ok(data) + Ok(data.into()) }) } diff --git a/rust/plugin_runtime/src/clipboard.rs b/rust/plugin_runtime/src/clipboard.rs index 22e8298..cad0bb4 100644 --- a/rust/plugin_runtime/src/clipboard.rs +++ b/rust/plugin_runtime/src/clipboard.rs @@ -3,22 +3,16 @@ use std::rc::Rc; use deno_core::op2; use deno_core::OpState; -use serde::Deserialize; -use serde::Serialize; use crate::api::BackendForPluginRuntimeApi; use crate::api::BackendForPluginRuntimeApiProxy; -use crate::model::JsClipboardData; - -#[derive(Debug, Serialize, Deserialize)] -struct JSClipboardData { - text_data: Option, - png_data: Option>, -} +use crate::DenoInClipboardData; +use crate::DenoOutClipboardData; +use crate::JsClipboardData; #[op2(async)] #[serde] -pub async fn clipboard_read(state: Rc>) -> anyhow::Result { +pub async fn clipboard_read(state: Rc>) -> anyhow::Result { let api = { let state = state.borrow(); @@ -29,9 +23,9 @@ pub async fn clipboard_read(state: Rc>) -> anyhow::Result>) -> anyhow::Result< } #[op2(async)] -pub async fn clipboard_write(state: Rc>, #[serde] data: JSClipboardData) -> anyhow::Result<()> { +pub async fn clipboard_write(state: Rc>, #[serde] data: DenoInClipboardData) -> anyhow::Result<()> { let api = { let state = state.borrow(); @@ -61,7 +55,7 @@ pub async fn clipboard_write(state: Rc>, #[serde] data: JSClipb let clipboard_data = JsClipboardData { text_data: data.text_data, - png_data: data.png_data, + png_data: data.png_data.map(|buffer| buffer.to_vec()), }; api.clipboard_write(clipboard_data).await diff --git a/rust/plugin_runtime/src/model.rs b/rust/plugin_runtime/src/model.rs index 1294429..108dacf 100644 --- a/rust/plugin_runtime/src/model.rs +++ b/rust/plugin_runtime/src/model.rs @@ -3,6 +3,8 @@ use std::fmt; use bincode::Decode; use bincode::Encode; +use deno_core::JsBuffer; +use deno_core::ToJsBuffer; use gauntlet_common::model::EntrypointId; use gauntlet_common::model::Icons; use gauntlet_common::model::PluginId; @@ -174,7 +176,7 @@ pub enum JsRequest { }, } -#[derive(Deserialize, Serialize, Encode, Decode)] +#[derive(Encode, Decode)] pub struct JsGeneratedSearchItem { pub entrypoint_name: String, pub generator_entrypoint_id: String, @@ -199,6 +201,28 @@ impl fmt::Debug for JsGeneratedSearchItem { } } +#[derive(Serialize)] +pub struct DenoOutGeneratedSearchItem { + pub entrypoint_name: String, + pub generator_entrypoint_id: String, + pub entrypoint_id: String, + pub entrypoint_uuid: String, + pub entrypoint_icon: Option, + pub entrypoint_actions: Vec, + pub entrypoint_accessories: Vec, +} + +#[derive(Deserialize)] +pub struct DenoInGeneratedSearchItem { + pub entrypoint_name: String, + pub generator_entrypoint_id: String, + pub entrypoint_id: String, + pub entrypoint_uuid: String, + pub entrypoint_icon: Option, + pub entrypoint_actions: Vec, + pub entrypoint_accessories: Vec, +} + #[derive(Debug, Deserialize, Serialize, Encode, Decode)] pub struct JsGeneratedSearchItemAction { pub id: Option, @@ -236,8 +260,20 @@ pub enum JsGeneratedSearchItemAccessory { }, } -#[derive(Debug, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Encode, Decode)] pub struct JsClipboardData { pub text_data: Option, pub png_data: Option>, } + +#[derive(Serialize)] +pub struct DenoOutClipboardData { + pub text_data: Option, + pub png_data: Option, +} + +#[derive(Deserialize)] +pub struct DenoInClipboardData { + pub text_data: Option, + pub png_data: Option, +} diff --git a/rust/plugin_runtime/src/plugins/applications.rs b/rust/plugin_runtime/src/plugins/applications.rs index 264622b..0d7dd23 100644 --- a/rust/plugin_runtime/src/plugins/applications.rs +++ b/rust/plugin_runtime/src/plugins/applications.rs @@ -5,6 +5,7 @@ use std::rc::Rc; use anyhow::anyhow; use deno_core::op2; use deno_core::OpState; +use deno_core::ToJsBuffer; use gauntlet_common::detached_process::CommandExt; use image::imageops::FilterType; use image::ImageFormat; @@ -46,7 +47,7 @@ pub enum DesktopPathAction { pub struct DesktopApplication { name: String, desktop_file_path: String, - icon: Option>, + icon: Option, startup_wm_class: Option, } @@ -55,7 +56,7 @@ pub struct DesktopApplication { pub struct DesktopApplication { name: String, path: String, - icon: Option>, + icon: Option, } #[cfg(target_os = "windows")] @@ -63,7 +64,7 @@ pub struct DesktopApplication { pub struct DesktopApplication { name: String, path: String, - icon: Option>, + icon: Option, } #[cfg(target_os = "macos")] @@ -71,7 +72,7 @@ pub struct DesktopApplication { pub struct DesktopSettingsPre13Data { name: String, path: String, - icon: Option>, + icon: Option, } #[cfg(target_os = "macos")] @@ -79,7 +80,7 @@ pub struct DesktopSettingsPre13Data { pub struct DesktopSettings13AndPostData { name: String, preferences_id: String, - icon: Option>, + icon: Option, } #[op2] diff --git a/rust/plugin_runtime/src/plugins/applications/linux/mod.rs b/rust/plugin_runtime/src/plugins/applications/linux/mod.rs index 2e1f195..7b64a1b 100644 --- a/rust/plugin_runtime/src/plugins/applications/linux/mod.rs +++ b/rust/plugin_runtime/src/plugins/applications/linux/mod.rs @@ -8,6 +8,7 @@ use std::rc::Rc; use deno_core::op2; use deno_core::OpState; +use deno_core::ToJsBuffer; use freedesktop_entry_parser::parse_entry; use freedesktop_icons::lookup; use gauntlet_common::detached_process::CommandExt; @@ -267,7 +268,8 @@ fn create_app_entry(desktop_file_path: &Path) -> Option { res.inspect_err(|err| tracing::warn!("error processing icon of {:?}: {:?}", desktop_file_path, err)) .ok() }) - .flatten(); + .flatten() + .map(|buffer| buffer.into()); Some(DesktopApplication { name: name.to_string(), diff --git a/rust/plugin_runtime/src/plugins/applications/macos.rs b/rust/plugin_runtime/src/plugins/applications/macos.rs index 9db37c3..59af25b 100644 --- a/rust/plugin_runtime/src/plugins/applications/macos.rs +++ b/rust/plugin_runtime/src/plugins/applications/macos.rs @@ -11,6 +11,7 @@ use cacao::filesystem::FileManager; use cacao::filesystem::SearchPathDirectory; use cacao::filesystem::SearchPathDomainMask; use cacao::url::Url; +use deno_core::ToJsBuffer; use objc2::ClassType; use objc2_app_kit::NSBitmapImageRep; use objc2_app_kit::NSCalibratedWhiteColorSpace; @@ -464,7 +465,7 @@ unsafe fn resize_ns_image(source_image: &NSImage, width: NSInteger, height: NSIn Some(data.bytes().to_vec()) } -fn get_application_icon(app_path: &Path) -> anyhow::Result> { +fn get_application_icon(app_path: &Path) -> anyhow::Result { unsafe { let workspace = NSWorkspace::sharedWorkspace(); @@ -478,7 +479,7 @@ fn get_application_icon(app_path: &Path) -> anyhow::Result> { let bytes = resize_ns_image(&image, 40, 40).ok_or(anyhow!("Unable to resize the image"))?; - Ok(bytes) + Ok(bytes.into()) } } diff --git a/rust/plugin_runtime/src/plugins/applications/windows.rs b/rust/plugin_runtime/src/plugins/applications/windows.rs index febb913..9d6a25c 100644 --- a/rust/plugin_runtime/src/plugins/applications/windows.rs +++ b/rust/plugin_runtime/src/plugins/applications/windows.rs @@ -7,6 +7,7 @@ use std::ptr; use anyhow::anyhow; use anyhow::Context; use deno_core::op2; +use deno_core::ToJsBuffer; use image::RgbaImage; use tokio::task::spawn_blocking; use windows::core::GUID; @@ -89,7 +90,7 @@ fn windows_app_from_path_blocking(file_path: String) -> anyhow::Result anyhow::Result> { +fn extract_icon(file_path: &str) -> anyhow::Result { unsafe { let mut shfileinfow = Shell::SHFILEINFOW::default(); @@ -188,7 +189,7 @@ fn extract_icon(file_path: &str) -> anyhow::Result> { let data = resize_icon(data)?; - Ok(data) + Ok(data.into()) } } diff --git a/rust/plugin_runtime/src/search.rs b/rust/plugin_runtime/src/search.rs index aa0f560..0c999f8 100644 --- a/rust/plugin_runtime/src/search.rs +++ b/rust/plugin_runtime/src/search.rs @@ -6,12 +6,13 @@ use deno_core::OpState; use crate::api::BackendForPluginRuntimeApi; use crate::api::BackendForPluginRuntimeApiProxy; -use crate::model::JsGeneratedSearchItem; +use crate::DenoInGeneratedSearchItem; +use crate::JsGeneratedSearchItem; #[op2(async)] pub async fn reload_search_index( state: Rc>, - #[serde] generated_entrypoints: Vec, + #[serde] generated_entrypoints: Vec, refresh_search_list: bool, ) -> anyhow::Result<()> { let api = { @@ -22,6 +23,21 @@ pub async fn reload_search_index( api }; + let generated_entrypoints = generated_entrypoints + .into_iter() + .map(|item| { + JsGeneratedSearchItem { + entrypoint_name: item.entrypoint_name, + generator_entrypoint_id: item.generator_entrypoint_id, + entrypoint_id: item.entrypoint_id, + entrypoint_uuid: item.entrypoint_uuid, + entrypoint_icon: item.entrypoint_icon.map(|buffer| buffer.to_vec()), + entrypoint_actions: item.entrypoint_actions, + entrypoint_accessories: item.entrypoint_accessories, + } + }) + .collect(); + api.reload_search_index(generated_entrypoints, refresh_search_list) .await?; From a72329aeb7b971ea95af646d2c02792d711eeaf1 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 20 Apr 2025 17:16:00 +0200 Subject: [PATCH 425/540] Add a bunch of plugin examples for entrypoint generators --- example_plugins/plugins/command/.gitignore | 2 + example_plugins/plugins/command/gauntlet.toml | 35 ++++++++++++++++ example_plugins/plugins/command/package.json | 17 ++++++++ .../plugins/command/src/preferences.tsx | 8 ++++ .../plugins/command/src/simple.tsx | 3 ++ example_plugins/plugins/command/tsconfig.json | 11 +++++ .../.gitignore | 2 + .../gauntlet.toml | 12 ++++++ .../package.json | 17 ++++++++ .../src/accessories.tsx | 17 ++++++++ .../tsconfig.json | 11 +++++ .../.gitignore | 2 + .../gauntlet.toml | 17 ++++++++ .../package.json | 17 ++++++++ .../src/action-shortcut.tsx | 30 ++++++++++++++ .../tsconfig.json | 11 +++++ .../entrypoint_generator_fs_events/.gitignore | 2 + .../gauntlet.toml | 19 +++++++++ .../package.json | 17 ++++++++ .../src/fs-events.tsx | 40 +++++++++++++++++++ .../tsconfig.json | 11 +++++ .../entrypoint_generator_icons/.gitignore | 2 + .../entrypoint_generator_icons/gauntlet.toml | 15 +++++++ .../entrypoint_generator_icons/package.json | 17 ++++++++ .../entrypoint_generator_icons/src/icons.tsx | 20 ++++++++++ .../entrypoint_generator_icons/tsconfig.json | 11 +++++ .../.gitignore | 2 + .../gauntlet.toml | 26 ++++++++++++ .../package.json | 17 ++++++++ .../src/preferences.tsx | 17 ++++++++ .../tsconfig.json | 11 +++++ .../entrypoint_generator_simple/.gitignore | 2 + .../entrypoint_generator_simple/gauntlet.toml | 12 ++++++ .../entrypoint_generator_simple/package.json | 17 ++++++++ .../src/simple.tsx | 35 ++++++++++++++++ .../entrypoint_generator_simple/tsconfig.json | 11 +++++ 36 files changed, 516 insertions(+) create mode 100644 example_plugins/plugins/command/.gitignore create mode 100644 example_plugins/plugins/command/gauntlet.toml create mode 100644 example_plugins/plugins/command/package.json create mode 100644 example_plugins/plugins/command/src/preferences.tsx create mode 100644 example_plugins/plugins/command/src/simple.tsx create mode 100644 example_plugins/plugins/command/tsconfig.json create mode 100644 example_plugins/plugins/entrypoint_generator_accessories/.gitignore create mode 100644 example_plugins/plugins/entrypoint_generator_accessories/gauntlet.toml create mode 100644 example_plugins/plugins/entrypoint_generator_accessories/package.json create mode 100644 example_plugins/plugins/entrypoint_generator_accessories/src/accessories.tsx create mode 100644 example_plugins/plugins/entrypoint_generator_accessories/tsconfig.json create mode 100644 example_plugins/plugins/entrypoint_generator_action_shortcut/.gitignore create mode 100644 example_plugins/plugins/entrypoint_generator_action_shortcut/gauntlet.toml create mode 100644 example_plugins/plugins/entrypoint_generator_action_shortcut/package.json create mode 100644 example_plugins/plugins/entrypoint_generator_action_shortcut/src/action-shortcut.tsx create mode 100644 example_plugins/plugins/entrypoint_generator_action_shortcut/tsconfig.json create mode 100644 example_plugins/plugins/entrypoint_generator_fs_events/.gitignore create mode 100644 example_plugins/plugins/entrypoint_generator_fs_events/gauntlet.toml create mode 100644 example_plugins/plugins/entrypoint_generator_fs_events/package.json create mode 100644 example_plugins/plugins/entrypoint_generator_fs_events/src/fs-events.tsx create mode 100644 example_plugins/plugins/entrypoint_generator_fs_events/tsconfig.json create mode 100644 example_plugins/plugins/entrypoint_generator_icons/.gitignore create mode 100644 example_plugins/plugins/entrypoint_generator_icons/gauntlet.toml create mode 100644 example_plugins/plugins/entrypoint_generator_icons/package.json create mode 100644 example_plugins/plugins/entrypoint_generator_icons/src/icons.tsx create mode 100644 example_plugins/plugins/entrypoint_generator_icons/tsconfig.json create mode 100644 example_plugins/plugins/entrypoint_generator_preferences/.gitignore create mode 100644 example_plugins/plugins/entrypoint_generator_preferences/gauntlet.toml create mode 100644 example_plugins/plugins/entrypoint_generator_preferences/package.json create mode 100644 example_plugins/plugins/entrypoint_generator_preferences/src/preferences.tsx create mode 100644 example_plugins/plugins/entrypoint_generator_preferences/tsconfig.json create mode 100644 example_plugins/plugins/entrypoint_generator_simple/.gitignore create mode 100644 example_plugins/plugins/entrypoint_generator_simple/gauntlet.toml create mode 100644 example_plugins/plugins/entrypoint_generator_simple/package.json create mode 100644 example_plugins/plugins/entrypoint_generator_simple/src/simple.tsx create mode 100644 example_plugins/plugins/entrypoint_generator_simple/tsconfig.json diff --git a/example_plugins/plugins/command/.gitignore b/example_plugins/plugins/command/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/command/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/command/gauntlet.toml b/example_plugins/plugins/command/gauntlet.toml new file mode 100644 index 0000000..9355b6b --- /dev/null +++ b/example_plugins/plugins/command/gauntlet.toml @@ -0,0 +1,35 @@ +[gauntlet] +name = 'Docs Command' +description = '' + +# docs-code-segment:start simple +[[entrypoint]] +id = 'simple' +name = 'Simple' +path = 'src/simple.tsx' +type = 'command' +description = '' +# docs-code-segment:end + +# docs-code-segment:start preferences +[[preferences]] +id = 'testBool' +name = 'Test Boolean Preference' +type = 'bool' +default = true +description = "" + +[[entrypoint]] +id = 'preferences' +name = 'Preferences' +path = 'src/preferences.tsx' +type = 'command' +description = '' + +[[entrypoint.preferences]] +id = 'testStr' +name = 'Test String Preference' +type = 'string' +default = 'test_value' +description = "" +# docs-code-segment:end diff --git a/example_plugins/plugins/command/package.json b/example_plugins/plugins/command/package.json new file mode 100644 index 0000000..b5bdf4e --- /dev/null +++ b/example_plugins/plugins/command/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-command", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/command/src/preferences.tsx b/example_plugins/plugins/command/src/preferences.tsx new file mode 100644 index 0000000..b8d6a6e --- /dev/null +++ b/example_plugins/plugins/command/src/preferences.tsx @@ -0,0 +1,8 @@ +import { CommandContext } from "@project-gauntlet/api/helpers"; + +type PluginCommandContext = CommandContext<{ testBool: boolean }, {testStr: string }>; + +export default async function Command({ pluginPreferences, entrypointPreferences }: PluginCommandContext) { + console.log(pluginPreferences.testBool); + console.log(entrypointPreferences.testStr); +} diff --git a/example_plugins/plugins/command/src/simple.tsx b/example_plugins/plugins/command/src/simple.tsx new file mode 100644 index 0000000..d682be1 --- /dev/null +++ b/example_plugins/plugins/command/src/simple.tsx @@ -0,0 +1,3 @@ +export default async function Command() { + console.log("Running the Gauntlet...") +} diff --git a/example_plugins/plugins/command/tsconfig.json b/example_plugins/plugins/command/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/command/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/entrypoint_generator_accessories/.gitignore b/example_plugins/plugins/entrypoint_generator_accessories/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_accessories/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/entrypoint_generator_accessories/gauntlet.toml b/example_plugins/plugins/entrypoint_generator_accessories/gauntlet.toml new file mode 100644 index 0000000..9ffef6a --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_accessories/gauntlet.toml @@ -0,0 +1,12 @@ +[gauntlet] +name = 'Docs Entrypoint Generator Accessories' +description = '' + +# docs-code-segment:start accessories +[[entrypoint]] +id = 'accessories' +name = 'Accessories' +path = 'src/accessories.tsx' +type = 'entrypoint-generator' +description = '' +# docs-code-segment:end diff --git a/example_plugins/plugins/entrypoint_generator_accessories/package.json b/example_plugins/plugins/entrypoint_generator_accessories/package.json new file mode 100644 index 0000000..f646518 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_accessories/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-entrypoint-generator-accessories", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/entrypoint_generator_accessories/src/accessories.tsx b/example_plugins/plugins/entrypoint_generator_accessories/src/accessories.tsx new file mode 100644 index 0000000..784207b --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_accessories/src/accessories.tsx @@ -0,0 +1,17 @@ +import { GeneratorContext } from "@project-gauntlet/api/helpers"; +import { Icons } from "@project-gauntlet/api/components"; + +export default function EntrypointGenerator({ add }: GeneratorContext): void { + add('generated', { + name: 'Generated Command', + actions: [ + { + label: "Run the Gauntlet", + run: () => { + console.log('Running the Gauntlet...') + } + } + ], + accessories: [{ icon: Icons.Battery, text: "100 %" }] + }) +} diff --git a/example_plugins/plugins/entrypoint_generator_accessories/tsconfig.json b/example_plugins/plugins/entrypoint_generator_accessories/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_accessories/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/entrypoint_generator_action_shortcut/.gitignore b/example_plugins/plugins/entrypoint_generator_action_shortcut/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_action_shortcut/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/entrypoint_generator_action_shortcut/gauntlet.toml b/example_plugins/plugins/entrypoint_generator_action_shortcut/gauntlet.toml new file mode 100644 index 0000000..d3455c3 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_action_shortcut/gauntlet.toml @@ -0,0 +1,17 @@ +[gauntlet] +name = 'Docs Entrypoint Generator Action Shortcut' +description = '' + +# docs-code-segment:start action-shortcut +[[entrypoint]] +id = 'action-shortcut' +name = 'Action Shortcut' +path = 'src/action-shortcut.tsx' +type = 'entrypoint-generator' +description = '' + +[[entrypoint.actions]] +id = 'otherAction' +description = '' +shortcut = { key = 'g', kind = 'main'} +# docs-code-segment:end diff --git a/example_plugins/plugins/entrypoint_generator_action_shortcut/package.json b/example_plugins/plugins/entrypoint_generator_action_shortcut/package.json new file mode 100644 index 0000000..8727092 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_action_shortcut/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-entrypoint-generator-action-shortcuts", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/entrypoint_generator_action_shortcut/src/action-shortcut.tsx b/example_plugins/plugins/entrypoint_generator_action_shortcut/src/action-shortcut.tsx new file mode 100644 index 0000000..51fcbf4 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_action_shortcut/src/action-shortcut.tsx @@ -0,0 +1,30 @@ +import { GeneratorContext } from "@project-gauntlet/api/helpers"; + +type PluginGeneratorContext = GeneratorContext<{ testBool: boolean }, { testStr: string }>; + +export default function EntrypointGenerator({ add }: PluginGeneratorContext): void { + add('generated', { + name: 'Generated Command', + actions: [ + { + label: "Primary Action", // Executed when Enter is pressed + run: () => { + console.log('Running the Gauntlet... - Primary Action') + } + }, + { + label: "Secondary Action", // Executed when Shift+Enter is pressed + run: () => { + console.log('Running the Gauntlet... - Secondary Action') + } + }, + { + ref: "otherAction", // Executed when pressing shortcut specified in Plugin Manifest + label: "Other Action", + run: () => { + console.log('Running the Gauntlet... - Other Action') + } + } + ], + }); +} diff --git a/example_plugins/plugins/entrypoint_generator_action_shortcut/tsconfig.json b/example_plugins/plugins/entrypoint_generator_action_shortcut/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_action_shortcut/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/entrypoint_generator_fs_events/.gitignore b/example_plugins/plugins/entrypoint_generator_fs_events/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_fs_events/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/entrypoint_generator_fs_events/gauntlet.toml b/example_plugins/plugins/entrypoint_generator_fs_events/gauntlet.toml new file mode 100644 index 0000000..6329476 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_fs_events/gauntlet.toml @@ -0,0 +1,19 @@ +[gauntlet] +name = 'Docs Entrypoint Generator FS Events' +description = '' + +# docs-code-segment:start fs-events +[[entrypoint]] +id = 'fs-events' +name = 'FS Events' +path = 'src/fs-events.tsx' +type = 'entrypoint-generator' +description = '' + +[permissions.filesystem] +read = ["/tmp/gauntlet-example"] +write = ["/tmp/gauntlet-example"] + +[[supported_system]] +os = 'linux' +# docs-code-segment:end diff --git a/example_plugins/plugins/entrypoint_generator_fs_events/package.json b/example_plugins/plugins/entrypoint_generator_fs_events/package.json new file mode 100644 index 0000000..0c69da1 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_fs_events/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-entrypoint-generator-fs-events", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/entrypoint_generator_fs_events/src/fs-events.tsx b/example_plugins/plugins/entrypoint_generator_fs_events/src/fs-events.tsx new file mode 100644 index 0000000..7f6dcaf --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_fs_events/src/fs-events.tsx @@ -0,0 +1,40 @@ +import { GeneratorContext } from "@project-gauntlet/api/helpers"; + +export default async function EntrypointGenerator({ add, remove }: GeneratorContext): Promise void)> { + const id = "generated"; + const path = "/tmp/gauntlet-example"; + + await Deno.create(path) + add(id, generatedItem("default")) + + const watcher = Deno.watchFs(path); + + (async () => { + for await (const _event of watcher) { + try { + const value = await Deno.readTextFile(path); + add(id, generatedItem(value)) + } catch (err) { + remove(id) + } + } + })(); + + return () => { + watcher.close() + } +} + +function generatedItem(value: string) { + return { + name: `Generated Command - ${value}`, + actions: [ + { + label: "Run the Gauntlet", + run: () => { + console.log('Running the Gauntlet...') + } + } + ] + }; +} diff --git a/example_plugins/plugins/entrypoint_generator_fs_events/tsconfig.json b/example_plugins/plugins/entrypoint_generator_fs_events/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_fs_events/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/entrypoint_generator_icons/.gitignore b/example_plugins/plugins/entrypoint_generator_icons/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_icons/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/entrypoint_generator_icons/gauntlet.toml b/example_plugins/plugins/entrypoint_generator_icons/gauntlet.toml new file mode 100644 index 0000000..c5cb3aa --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_icons/gauntlet.toml @@ -0,0 +1,15 @@ +[gauntlet] +name = 'Docs Entrypoint Generator Icons' +description = '' + +# docs-code-segment:start icons +[[entrypoint]] +id = 'icons' +name = 'Icons' +path = 'src/icons.tsx' +type = 'entrypoint-generator' +description = '' + +[permissions] +network = ["img.icons8.com"] +# docs-code-segment:end diff --git a/example_plugins/plugins/entrypoint_generator_icons/package.json b/example_plugins/plugins/entrypoint_generator_icons/package.json new file mode 100644 index 0000000..9848338 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_icons/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-entrypoint-generator", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/entrypoint_generator_icons/src/icons.tsx b/example_plugins/plugins/entrypoint_generator_icons/src/icons.tsx new file mode 100644 index 0000000..50e8478 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_icons/src/icons.tsx @@ -0,0 +1,20 @@ +import { GeneratorContext } from "@project-gauntlet/api/helpers"; + +export default async function EntrypointGenerator({ add }: GeneratorContext): Promise { + + const response = await fetch("https://img.icons8.com/?size=32&id=21276&format=png"); + const arrayBuffer = await response.bytes(); + + add('generated', { + name: 'Generated Command', + actions: [ + { + label: "Run the Gauntlet", + run: () => { + console.log('Running the Gauntlet...') + } + } + ], + icon: arrayBuffer + }); +} diff --git a/example_plugins/plugins/entrypoint_generator_icons/tsconfig.json b/example_plugins/plugins/entrypoint_generator_icons/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_icons/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/entrypoint_generator_preferences/.gitignore b/example_plugins/plugins/entrypoint_generator_preferences/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_preferences/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/entrypoint_generator_preferences/gauntlet.toml b/example_plugins/plugins/entrypoint_generator_preferences/gauntlet.toml new file mode 100644 index 0000000..e834c8c --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_preferences/gauntlet.toml @@ -0,0 +1,26 @@ +[gauntlet] +name = 'Docs Entrypoint Generator Preferences' +description = '' + +# docs-code-segment:start preferences +[[preferences]] +id = 'testBool' +name = 'Test Boolean Preference' +type = 'bool' +default = true +description = "" + +[[entrypoint]] +id = 'preferences' +name = 'Preferences' +path = 'src/preferences.tsx' +type = 'entrypoint-generator' +description = '' + +[[entrypoint.preferences]] +id = 'testStr' +name = 'Test String Preference' +type = 'string' +default = 'test_value' +description = "" +# docs-code-segment:end diff --git a/example_plugins/plugins/entrypoint_generator_preferences/package.json b/example_plugins/plugins/entrypoint_generator_preferences/package.json new file mode 100644 index 0000000..834ca3c --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_preferences/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-entrypoint-generator-preferences", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/entrypoint_generator_preferences/src/preferences.tsx b/example_plugins/plugins/entrypoint_generator_preferences/src/preferences.tsx new file mode 100644 index 0000000..becf895 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_preferences/src/preferences.tsx @@ -0,0 +1,17 @@ +import { GeneratorContext } from "@project-gauntlet/api/helpers"; + +type PluginGeneratorContext = GeneratorContext<{ testBool: boolean }, { testStr: string }>; + +export default function EntrypointGenerator({ add, pluginPreferences, entrypointPreferences }: PluginGeneratorContext): void { + add('generated', { + name: 'Generated Command - ' + entrypointPreferences.testStr, + actions: [ + { + label: "Run the Gauntlet", + run: () => { + console.log('Running the Gauntlet... ' + pluginPreferences.testBool) + } + } + ], + }); +} diff --git a/example_plugins/plugins/entrypoint_generator_preferences/tsconfig.json b/example_plugins/plugins/entrypoint_generator_preferences/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_preferences/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/entrypoint_generator_simple/.gitignore b/example_plugins/plugins/entrypoint_generator_simple/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_simple/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/entrypoint_generator_simple/gauntlet.toml b/example_plugins/plugins/entrypoint_generator_simple/gauntlet.toml new file mode 100644 index 0000000..8be1da9 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_simple/gauntlet.toml @@ -0,0 +1,12 @@ +[gauntlet] +name = 'Docs Entrypoint Generator Simple' +description = '' + +# docs-code-segment:start simple +[[entrypoint]] +id = 'simple' +name = 'Simple' +path = 'src/simple.tsx' +type = 'entrypoint-generator' +description = '' +# docs-code-segment:end diff --git a/example_plugins/plugins/entrypoint_generator_simple/package.json b/example_plugins/plugins/entrypoint_generator_simple/package.json new file mode 100644 index 0000000..3b313df --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_simple/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/docs-entrypoint-generator-simple", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/entrypoint_generator_simple/src/simple.tsx b/example_plugins/plugins/entrypoint_generator_simple/src/simple.tsx new file mode 100644 index 0000000..f43c7fc --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_simple/src/simple.tsx @@ -0,0 +1,35 @@ +import { GeneratorContext } from "@project-gauntlet/api/helpers"; +import { ReactElement } from "react"; +import { List } from "@project-gauntlet/api/components"; + +function ListView(): ReactElement { + return ( + + + + ) +} + +export default function EntrypointGenerator({ add }: GeneratorContext): void { + add('generated', { + name: 'Generated Command', + actions: [ + { + label: "Run the Gauntlet", + run: () => { + console.log('Running the Gauntlet...') + } + } + ] + }) + + add('generated', { + name: 'Generated View', + actions: [ + { + label: "Open generated view", + view: () => + } + ] + }) +} diff --git a/example_plugins/plugins/entrypoint_generator_simple/tsconfig.json b/example_plugins/plugins/entrypoint_generator_simple/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/entrypoint_generator_simple/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file From 87d6b1abb8cbf67d4c0ef03c1b9445e817bfb1a7 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Sun, 20 Apr 2025 17:27:21 +0200 Subject: [PATCH 426/540] Fix typo in plugin manifest json schema --- docs/schema/plugin_manifest.schema.json | 2 +- package-lock.json | 140 +++++++++++++++++++++ rust/server/src/plugins/plugin_manifest.rs | 2 +- 3 files changed, 142 insertions(+), 2 deletions(-) diff --git a/docs/schema/plugin_manifest.schema.json b/docs/schema/plugin_manifest.schema.json index 7189275..0b12326 100644 --- a/docs/schema/plugin_manifest.schema.json +++ b/docs/schema/plugin_manifest.schema.json @@ -350,7 +350,7 @@ ] }, { - "description": "Entrypoint that dynamically generatepointsd", + "description": "Entrypoint that can dynamically generates endpoints", "type": "string", "enum": [ "entrypoint-generator" diff --git a/package-lock.json b/package-lock.json index 3575bb3..69ee1ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,22 @@ } }, "example_plugins/js/api": {}, + "example_plugins/plugins/command": { + "name": "@project-gauntlet/docs-command", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/command/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, "example_plugins/plugins/docs_detail": { "name": "@project-gauntlet/docs-detailt", "extraneous": true, @@ -140,6 +156,102 @@ "typescript": "*" } }, + "example_plugins/plugins/entrypoint_generator_accessories": { + "name": "@project-gauntlet/docs-entrypoint-generator-accessories", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/entrypoint_generator_accessories/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/entrypoint_generator_action_shortcut": { + "name": "@project-gauntlet/docs-entrypoint-generator-action-shortcuts", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/entrypoint_generator_action_shortcut/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/entrypoint_generator_fs_events": { + "name": "@project-gauntlet/docs-entrypoint-generator-fs-events", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/entrypoint_generator_fs_events/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/entrypoint_generator_icons": { + "name": "@project-gauntlet/docs-entrypoint-generator", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/entrypoint_generator_icons/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/entrypoint_generator_preferences": { + "name": "@project-gauntlet/docs-entrypoint-generator-preferences", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/entrypoint_generator_preferences/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, + "example_plugins/plugins/entrypoint_generator_simple": { + "name": "@project-gauntlet/docs-entrypoint-generator-simple", + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@project-gauntlet/tools": "*", + "@types/deno": "*", + "@types/react": "*", + "typescript": "*" + } + }, + "example_plugins/plugins/entrypoint_generator_simple/node_modules/@project-gauntlet/api": { + "resolved": "example_plugins/js/api", + "link": true + }, "example_plugins/plugins/ui_detail": { "name": "@project-gauntlet/docs-detail", "dependencies": { @@ -974,10 +1086,38 @@ "resolved": "dev_plugin", "link": true }, + "node_modules/@project-gauntlet/docs-command": { + "resolved": "example_plugins/plugins/command", + "link": true + }, "node_modules/@project-gauntlet/docs-detail": { "resolved": "example_plugins/plugins/ui_detail", "link": true }, + "node_modules/@project-gauntlet/docs-entrypoint-generator": { + "resolved": "example_plugins/plugins/entrypoint_generator_icons", + "link": true + }, + "node_modules/@project-gauntlet/docs-entrypoint-generator-accessories": { + "resolved": "example_plugins/plugins/entrypoint_generator_accessories", + "link": true + }, + "node_modules/@project-gauntlet/docs-entrypoint-generator-action-shortcuts": { + "resolved": "example_plugins/plugins/entrypoint_generator_action_shortcut", + "link": true + }, + "node_modules/@project-gauntlet/docs-entrypoint-generator-fs-events": { + "resolved": "example_plugins/plugins/entrypoint_generator_fs_events", + "link": true + }, + "node_modules/@project-gauntlet/docs-entrypoint-generator-preferences": { + "resolved": "example_plugins/plugins/entrypoint_generator_preferences", + "link": true + }, + "node_modules/@project-gauntlet/docs-entrypoint-generator-simple": { + "resolved": "example_plugins/plugins/entrypoint_generator_simple", + "link": true + }, "node_modules/@project-gauntlet/docs-form": { "resolved": "example_plugins/plugins/ui_form", "link": true diff --git a/rust/server/src/plugins/plugin_manifest.rs b/rust/server/src/plugins/plugin_manifest.rs index 133bb25..f311b0b 100644 --- a/rust/server/src/plugins/plugin_manifest.rs +++ b/rust/server/src/plugins/plugin_manifest.rs @@ -163,7 +163,7 @@ pub enum PluginManifestEntrypointTypes { #[schemars(description = "A view-based entrypoint displayed under main search bar")] InlineView, #[serde(rename = "entrypoint-generator")] - #[schemars(description = "Entrypoint that dynamically generatepointsd")] + #[schemars(description = "Entrypoint that can dynamically generates endpoints")] EntrypointGenerator, } From 1a1c86297318b95ec2705c54959e8eaa800750e8 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+exidex@users.noreply.github.com> Date: Thu, 24 Apr 2025 21:13:43 +0200 Subject: [PATCH 427/540] More plugin examples --- docs/js/components/action/props/onAction.md | 2 +- .../plugins/command/src/preferences.tsx | 2 +- .../gauntlet.toml | 2 +- .../src/action-shortcut.tsx | 6 +- .../plugins/ui_action_panel/.gitignore | 2 + .../plugins/ui_action_panel/gauntlet.toml | 26 +++++++++ .../plugins/ui_action_panel/package.json | 17 ++++++ .../plugins/ui_action_panel/src/action.tsx | 31 ++++++++++ .../plugins/ui_action_panel/src/section.tsx | 33 +++++++++++ .../plugins/ui_action_panel/tsconfig.json | 11 ++++ .../plugins/ui_icons_and_images/.gitignore | 2 + .../ui_icons_and_images/assets/logo.png | Bin 0 -> 18959 bytes .../plugins/ui_icons_and_images/gauntlet.toml | 33 +++++++++++ .../plugins/ui_icons_and_images/package.json | 17 ++++++ .../plugins/ui_icons_and_images/src/asset.tsx | 12 ++++ .../plugins/ui_icons_and_images/src/icon.tsx | 12 ++++ .../plugins/ui_icons_and_images/src/url.tsx | 12 ++++ .../plugins/ui_icons_and_images/tsconfig.json | 11 ++++ .../plugins/ui_navigation/.gitignore | 2 + .../plugins/ui_navigation/gauntlet.toml | 12 ++++ .../plugins/ui_navigation/package.json | 17 ++++++ .../plugins/ui_navigation/src/navigation.tsx | 53 ++++++++++++++++++ .../plugins/ui_navigation/tsconfig.json | 11 ++++ .../plugins/ui_preferences/.gitignore | 2 + .../plugins/ui_preferences/gauntlet.toml | 26 +++++++++ .../plugins/ui_preferences/package.json | 17 ++++++ .../ui_preferences/src/preferences.tsx | 23 ++++++++ .../plugins/ui_preferences/tsconfig.json | 11 ++++ .../ui_action_panel/action/default.json | 3 + .../ui_action_panel/section/default.json | 3 + .../ui_icons_and_images/asset/default.json | 3 + .../ui_icons_and_images/icon/default.json | 3 + .../ui_icons_and_images/url/default.json | 3 + js/scenario_runner_cli/src/main.ts | 5 ++ rust/client/src/ui/mod.rs | 9 +++ rust/scenario_runner/src/lib.rs | 1 + 36 files changed, 429 insertions(+), 6 deletions(-) create mode 100644 example_plugins/plugins/ui_action_panel/.gitignore create mode 100644 example_plugins/plugins/ui_action_panel/gauntlet.toml create mode 100644 example_plugins/plugins/ui_action_panel/package.json create mode 100644 example_plugins/plugins/ui_action_panel/src/action.tsx create mode 100644 example_plugins/plugins/ui_action_panel/src/section.tsx create mode 100644 example_plugins/plugins/ui_action_panel/tsconfig.json create mode 100644 example_plugins/plugins/ui_icons_and_images/.gitignore create mode 100644 example_plugins/plugins/ui_icons_and_images/assets/logo.png create mode 100644 example_plugins/plugins/ui_icons_and_images/gauntlet.toml create mode 100644 example_plugins/plugins/ui_icons_and_images/package.json create mode 100644 example_plugins/plugins/ui_icons_and_images/src/asset.tsx create mode 100644 example_plugins/plugins/ui_icons_and_images/src/icon.tsx create mode 100644 example_plugins/plugins/ui_icons_and_images/src/url.tsx create mode 100644 example_plugins/plugins/ui_icons_and_images/tsconfig.json create mode 100644 example_plugins/plugins/ui_navigation/.gitignore create mode 100644 example_plugins/plugins/ui_navigation/gauntlet.toml create mode 100644 example_plugins/plugins/ui_navigation/package.json create mode 100644 example_plugins/plugins/ui_navigation/src/navigation.tsx create mode 100644 example_plugins/plugins/ui_navigation/tsconfig.json create mode 100644 example_plugins/plugins/ui_preferences/.gitignore create mode 100644 example_plugins/plugins/ui_preferences/gauntlet.toml create mode 100644 example_plugins/plugins/ui_preferences/package.json create mode 100644 example_plugins/plugins/ui_preferences/src/preferences.tsx create mode 100644 example_plugins/plugins/ui_preferences/tsconfig.json create mode 100644 example_plugins/scenarios/ui_action_panel/action/default.json create mode 100644 example_plugins/scenarios/ui_action_panel/section/default.json create mode 100644 example_plugins/scenarios/ui_icons_and_images/asset/default.json create mode 100644 example_plugins/scenarios/ui_icons_and_images/icon/default.json create mode 100644 example_plugins/scenarios/ui_icons_and_images/url/default.json diff --git a/docs/js/components/action/props/onAction.md b/docs/js/components/action/props/onAction.md index 2346de8..fdad01a 100644 --- a/docs/js/components/action/props/onAction.md +++ b/docs/js/components/action/props/onAction.md @@ -1 +1 @@ -Function that is called when action button is clicked or the shortcut is pressed (including primary and secondary shortcuts). ID parameter is an id of currently focused grid or list item \ No newline at end of file +Function that is called when action button is clicked or the shortcut is pressed (including primary and secondary shortcuts). ID parameter is an id of currently focused grid or list item. Returning `{ close: true }` object from the action will close the window diff --git a/example_plugins/plugins/command/src/preferences.tsx b/example_plugins/plugins/command/src/preferences.tsx index b8d6a6e..9257eac 100644 --- a/example_plugins/plugins/command/src/preferences.tsx +++ b/example_plugins/plugins/command/src/preferences.tsx @@ -1,6 +1,6 @@ import { CommandContext } from "@project-gauntlet/api/helpers"; -type PluginCommandContext = CommandContext<{ testBool: boolean }, {testStr: string }>; +type PluginCommandContext = CommandContext<{ testBool: boolean }, { testStr: string }>; export default async function Command({ pluginPreferences, entrypointPreferences }: PluginCommandContext) { console.log(pluginPreferences.testBool); diff --git a/example_plugins/plugins/entrypoint_generator_action_shortcut/gauntlet.toml b/example_plugins/plugins/entrypoint_generator_action_shortcut/gauntlet.toml index d3455c3..1c9bb2d 100644 --- a/example_plugins/plugins/entrypoint_generator_action_shortcut/gauntlet.toml +++ b/example_plugins/plugins/entrypoint_generator_action_shortcut/gauntlet.toml @@ -11,7 +11,7 @@ type = 'entrypoint-generator' description = '' [[entrypoint.actions]] -id = 'otherAction' +id = 'anotherAction' description = '' shortcut = { key = 'g', kind = 'main'} # docs-code-segment:end diff --git a/example_plugins/plugins/entrypoint_generator_action_shortcut/src/action-shortcut.tsx b/example_plugins/plugins/entrypoint_generator_action_shortcut/src/action-shortcut.tsx index 51fcbf4..68df383 100644 --- a/example_plugins/plugins/entrypoint_generator_action_shortcut/src/action-shortcut.tsx +++ b/example_plugins/plugins/entrypoint_generator_action_shortcut/src/action-shortcut.tsx @@ -19,10 +19,10 @@ export default function EntrypointGenerator({ add }: PluginGeneratorContext): vo } }, { - ref: "otherAction", // Executed when pressing shortcut specified in Plugin Manifest - label: "Other Action", + ref: "anotherAction", // Executed when pressing shortcut specified in Plugin Manifest + label: "Another Action", run: () => { - console.log('Running the Gauntlet... - Other Action') + console.log('Running the Gauntlet... - Another Action') } } ], diff --git a/example_plugins/plugins/ui_action_panel/.gitignore b/example_plugins/plugins/ui_action_panel/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/ui_action_panel/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/ui_action_panel/gauntlet.toml b/example_plugins/plugins/ui_action_panel/gauntlet.toml new file mode 100644 index 0000000..d53558b --- /dev/null +++ b/example_plugins/plugins/ui_action_panel/gauntlet.toml @@ -0,0 +1,26 @@ +[gauntlet] +name = 'Docs Action Panel' +description = '' + +# docs-code-segment:start action +[[entrypoint]] +id = 'action' +name = 'Action' +path = 'src/action.tsx' +type = 'view' +description = '' + +[[entrypoint.actions]] +id = 'actionId' +description = '' +shortcut = { key = 'g', kind = 'main'} +# docs-code-segment:end + +# docs-code-segment:start section +[[entrypoint]] +id = 'section' +name = 'Section' +path = 'src/section.tsx' +type = 'view' +description = '' +# docs-code-segment:end diff --git a/example_plugins/plugins/ui_action_panel/package.json b/example_plugins/plugins/ui_action_panel/package.json new file mode 100644 index 0000000..775e96f --- /dev/null +++ b/example_plugins/plugins/ui_action_panel/package.json @@ -0,0 +1,17 @@ +{ + "name": "@project-gauntlet/ui-action-panel", + "private": true, + "scripts": { + "build": "gauntlet build", + "dev": "gauntlet dev" + }, + "dependencies": { + "@project-gauntlet/api": "file:../../js/api" + }, + "devDependencies": { + "@types/react": "*", + "@types/deno": "*", + "@project-gauntlet/tools": "*", + "typescript": "*" + } +} diff --git a/example_plugins/plugins/ui_action_panel/src/action.tsx b/example_plugins/plugins/ui_action_panel/src/action.tsx new file mode 100644 index 0000000..8dafdf3 --- /dev/null +++ b/example_plugins/plugins/ui_action_panel/src/action.tsx @@ -0,0 +1,31 @@ +import { ReactElement } from "react"; +import { ActionPanel, List } from "@project-gauntlet/api/components"; + +export default function View(): ReactElement { + return ( + + { + console.log(`Primary action for item with id '${id}' was executed`) + // returning object with close property set to true will close the window + return { close: true } + }} + /> + console.log(`Secondary action for item with id '${id}' was executed`)} + /> + console.log(`Another action for item with id '${id}' was executed`)} + /> + + }> + + + ) +} + diff --git a/example_plugins/plugins/ui_action_panel/src/section.tsx b/example_plugins/plugins/ui_action_panel/src/section.tsx new file mode 100644 index 0000000..7990070 --- /dev/null +++ b/example_plugins/plugins/ui_action_panel/src/section.tsx @@ -0,0 +1,33 @@ +import { ReactElement } from "react"; +import { ActionPanel, List } from "@project-gauntlet/api/components"; + +export default function View(): ReactElement { + return ( + + console.log(`Primary action for item with id '${id}' was executed`)} + /> + + console.log(`Secondary action for item with id '${id}' was executed`)} + /> + console.log(`Another action for item with id '${id}' was executed`)} + /> + + + console.log(`Yet another action for item with id '${id}' was executed`)} + /> + + + }> + + + ) +} diff --git a/example_plugins/plugins/ui_action_panel/tsconfig.json b/example_plugins/plugins/ui_action_panel/tsconfig.json new file mode 100644 index 0000000..f9bb627 --- /dev/null +++ b/example_plugins/plugins/ui_action_panel/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "ES2022", + "esModuleInterop": true, + "target": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx" + }, + "lib": ["ES2020"] +} \ No newline at end of file diff --git a/example_plugins/plugins/ui_icons_and_images/.gitignore b/example_plugins/plugins/ui_icons_and_images/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/example_plugins/plugins/ui_icons_and_images/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/example_plugins/plugins/ui_icons_and_images/assets/logo.png b/example_plugins/plugins/ui_icons_and_images/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f677191ee3a6fc167b08c4a0a390a6e4ecdc86c6 GIT binary patch literal 18959 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